diff --git a/Directory.Build.props b/Directory.Build.props index 12a4a5c2a..50c09fbb3 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -31,21 +31,21 @@ - $(DefineConstants);SUPPORTS_MATHF;SUPPORTS_HASHCODE;SUPPORTS_EXTENDED_INTRINSICS;SUPPORTS_SPAN_STREAM;SUPPORTS_ENCODING_STRING;SUPPORTS_RUNTIME_INTRINSICS;SUPPORTS_CODECOVERAGE + $(DefineConstants);SUPPORTS_MATHF;SUPPORTS_HASHCODE;SUPPORTS_EXTENDED_INTRINSICS;SUPPORTS_SPAN_STREAM;SUPPORTS_ENCODING_STRING;SUPPORTS_RUNTIME_INTRINSICS;SUPPORTS_CODECOVERAGE;SUPPORTS_HOTPATH $(DefineConstants);SUPPORTS_MATHF;SUPPORTS_HASHCODE;SUPPORTS_EXTENDED_INTRINSICS;SUPPORTS_SPAN_STREAM;SUPPORTS_ENCODING_STRING;SUPPORTS_CODECOVERAGE diff --git a/src/ImageSharp/Common/Helpers/InliningOptions.cs b/src/ImageSharp/Common/Helpers/InliningOptions.cs index 069a426d7..895b6250f 100644 --- a/src/ImageSharp/Common/Helpers/InliningOptions.cs +++ b/src/ImageSharp/Common/Helpers/InliningOptions.cs @@ -1,4 +1,4 @@ -// Copyright (c) Six Labors and contributors. +// Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. // Uncomment this for verbose profiler results. DO NOT PUSH TO MAIN! @@ -13,10 +13,16 @@ namespace SixLabors.ImageSharp internal static class InliningOptions { #if PROFILING + public const MethodImplOptions HotPath = MethodImplOptions.NoInlining; public const MethodImplOptions ShortMethod = MethodImplOptions.NoInlining; #else +#if SUPPORTS_HOTPATH + public const MethodImplOptions HotPath = MethodImplOptions.AggressiveOptimization; +#else + public const MethodImplOptions HotPath = MethodImplOptions.AggressiveInlining; +#endif public const MethodImplOptions ShortMethod = MethodImplOptions.AggressiveInlining; #endif public const MethodImplOptions ColdPath = MethodImplOptions.NoInlining; } -} \ No newline at end of file +} diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/ProfileResolver.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/ProfileResolver.cs index 87b486ea6..325d7780a 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Decoder/ProfileResolver.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/ProfileResolver.cs @@ -28,6 +28,30 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder (byte)'I', (byte)'L', (byte)'E', (byte)'\0' }; + /// + /// Gets the adobe photoshop APP13 marker which can contain IPTC meta data. + /// + public static ReadOnlySpan AdobePhotoshopApp13Marker => new[] + { + (byte)'P', (byte)'h', (byte)'o', (byte)'t', (byte)'o', (byte)'s', (byte)'h', (byte)'o', (byte)'p', (byte)' ', (byte)'3', (byte)'.', (byte)'0', (byte)'\0' + }; + + /// + /// Gets the 8BIM marker, which signals the start of a adobe specific image resource block. + /// + public static ReadOnlySpan AdobeImageResourceBlockMarker => new[] + { + (byte)'8', (byte)'B', (byte)'I', (byte)'M' + }; + + /// + /// Gets a IPTC Image resource ID. + /// + public static ReadOnlySpan AdobeIptcMarker => new[] + { + (byte)4, (byte)4 + }; + /// /// Gets the EXIF specific markers. /// diff --git a/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs b/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs index 951fec1d4..4000fa0f6 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs @@ -14,6 +14,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.Iptc; using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Formats.Jpeg @@ -46,7 +47,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg private readonly byte[] markerBuffer = new byte[2]; /// - /// The DC Huffman tables + /// The DC Huffman tables. /// private HuffmanTable[] dcHuffmanTables; @@ -56,37 +57,47 @@ namespace SixLabors.ImageSharp.Formats.Jpeg private HuffmanTable[] acHuffmanTables; /// - /// The reset interval determined by RST markers + /// The reset interval determined by RST markers. /// private ushort resetInterval; /// - /// Whether the image has an EXIF marker + /// Whether the image has an EXIF marker. /// private bool isExif; /// - /// Contains exif data + /// Contains exif data. /// private byte[] exifData; /// - /// Whether the image has an ICC marker + /// Whether the image has an ICC marker. /// private bool isIcc; /// - /// Contains ICC data + /// Contains ICC data. /// private byte[] iccData; /// - /// Contains information about the JFIF marker + /// Whether the image has a IPTC data. + /// + private bool isIptc; + + /// + /// Contains IPTC data. + /// + private byte[] iptcData; + + /// + /// Contains information about the JFIF marker. /// private JFifMarker jFif; /// - /// Contains information about the Adobe marker + /// Contains information about the Adobe marker. /// private AdobeMarker adobe; @@ -213,6 +224,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg this.ParseStream(stream); this.InitExifProfile(); this.InitIccProfile(); + this.InitIptcProfile(); this.InitDerivedMetadataProperties(); return this.PostProcessIntoImage(); } @@ -226,6 +238,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg this.ParseStream(stream, true); this.InitExifProfile(); this.InitIccProfile(); + this.InitIptcProfile(); this.InitDerivedMetadataProperties(); return new ImageInfo(new PixelTypeInfo(this.BitsPerPixel), this.ImageWidth, this.ImageHeight, this.Metadata); @@ -344,10 +357,13 @@ namespace SixLabors.ImageSharp.Formats.Jpeg case JpegConstants.Markers.APP10: case JpegConstants.Markers.APP11: case JpegConstants.Markers.APP12: - case JpegConstants.Markers.APP13: this.InputStream.Skip(remaining); break; + case JpegConstants.Markers.APP13: + this.ProcessApp13Marker(remaining); + break; + case JpegConstants.Markers.APP14: this.ProcessApp14Marker(remaining); break; @@ -437,6 +453,18 @@ namespace SixLabors.ImageSharp.Formats.Jpeg } } + /// + /// Initializes the IPTC profile. + /// + private void InitIptcProfile() + { + if (this.isIptc) + { + var profile = new IptcProfile(this.iptcData); + this.Metadata.IptcProfile = profile; + } + } + /// /// Assigns derived metadata properties to , eg. horizontal and vertical resolution if it has a JFIF header. /// @@ -582,6 +610,95 @@ namespace SixLabors.ImageSharp.Formats.Jpeg } } + /// + /// Processes a App13 marker, which contains IPTC data stored with Adobe Photoshop. + /// The content of an APP13 segment is formed by an identifier string followed by a sequence of resource data blocks. + /// + /// The remaining bytes in the segment block. + private void ProcessApp13Marker(int remaining) + { + if (remaining < ProfileResolver.AdobePhotoshopApp13Marker.Length || this.IgnoreMetadata) + { + this.InputStream.Skip(remaining); + return; + } + + this.InputStream.Read(this.temp, 0, ProfileResolver.AdobePhotoshopApp13Marker.Length); + remaining -= ProfileResolver.AdobePhotoshopApp13Marker.Length; + if (ProfileResolver.IsProfile(this.temp, ProfileResolver.AdobePhotoshopApp13Marker)) + { + var resourceBlockData = new byte[remaining]; + this.InputStream.Read(resourceBlockData, 0, remaining); + Span blockDataSpan = resourceBlockData.AsSpan(); + + while (blockDataSpan.Length > 12) + { + if (!ProfileResolver.IsProfile(blockDataSpan.Slice(0, 4), ProfileResolver.AdobeImageResourceBlockMarker)) + { + return; + } + + blockDataSpan = blockDataSpan.Slice(4); + Span imageResourceBlockId = blockDataSpan.Slice(0, 2); + if (ProfileResolver.IsProfile(imageResourceBlockId, ProfileResolver.AdobeIptcMarker)) + { + var resourceBlockNameLength = ReadImageResourceNameLength(blockDataSpan); + var resourceDataSize = ReadResourceDataLength(blockDataSpan, resourceBlockNameLength); + int dataStartIdx = 2 + resourceBlockNameLength + 4; + if (resourceDataSize > 0 && blockDataSpan.Length >= dataStartIdx + resourceDataSize) + { + this.isIptc = true; + this.iptcData = blockDataSpan.Slice(dataStartIdx, resourceDataSize).ToArray(); + break; + } + } + else + { + var resourceBlockNameLength = ReadImageResourceNameLength(blockDataSpan); + var resourceDataSize = ReadResourceDataLength(blockDataSpan, resourceBlockNameLength); + int dataStartIdx = 2 + resourceBlockNameLength + 4; + if (blockDataSpan.Length < dataStartIdx + resourceDataSize) + { + // Not enough data or the resource data size is wrong. + break; + } + + blockDataSpan = blockDataSpan.Slice(dataStartIdx + resourceDataSize); + } + } + } + } + + /// + /// Reads the adobe image resource block name: a Pascal string (padded to make size even). + /// + /// The span holding the block resource data. + /// The length of the name. + [MethodImpl(InliningOptions.ShortMethod)] + private static int ReadImageResourceNameLength(Span blockDataSpan) + { + byte nameLength = blockDataSpan[2]; + var nameDataSize = nameLength == 0 ? 2 : nameLength; + if (nameDataSize % 2 != 0) + { + nameDataSize++; + } + + return nameDataSize; + } + + /// + /// Reads the length of a adobe image resource data block. + /// + /// The span holding the block resource data. + /// The length of the block name. + /// The block length. + [MethodImpl(InliningOptions.ShortMethod)] + private static int ReadResourceDataLength(Span blockDataSpan, int resourceBlockNameLength) + { + return BinaryPrimitives.ReadInt32BigEndian(blockDataSpan.Slice(2 + resourceBlockNameLength, 4)); + } + /// /// Processes the application header containing the Adobe identifier /// which stores image encoding information for DCT filters. diff --git a/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs b/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs index 32f4d2287..eed95c6b0 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs @@ -4,6 +4,7 @@ using System; using System.Buffers.Binary; using System.IO; +using System.Linq; using System.Runtime.CompilerServices; using SixLabors.ImageSharp.Common.Helpers; using SixLabors.ImageSharp.Formats.Jpeg.Components; @@ -13,6 +14,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.Iptc; using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Formats.Jpeg @@ -231,7 +233,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg // Write the Start Of Image marker. this.WriteApplicationHeader(metadata); - // Write Exif and ICC profiles + // Write Exif, ICC and IPTC profiles this.WriteProfiles(metadata); // Write the quantization tables. @@ -647,9 +649,6 @@ namespace SixLabors.ImageSharp.Formats.Jpeg /// Writes the EXIF profile. /// /// The exif profile. - /// - /// Thrown if the EXIF profile size exceeds the limit - /// private void WriteExifProfile(ExifProfile exifProfile) { if (exifProfile is null || exifProfile.Values.Count == 0) @@ -697,16 +696,68 @@ namespace SixLabors.ImageSharp.Formats.Jpeg } } + /// + /// Writes the IPTC metadata. + /// + /// The iptc metadata to write. + /// + /// Thrown if the IPTC profile size exceeds the limit of 65533 bytes. + /// + private void WriteIptcProfile(IptcProfile iptcProfile) + { + const int Max = 65533; + if (iptcProfile is null || !iptcProfile.Values.Any()) + { + return; + } + + iptcProfile.UpdateData(); + byte[] data = iptcProfile.Data; + if (data.Length == 0) + { + return; + } + + if (data.Length > Max) + { + throw new ImageFormatException($"Iptc profile size exceeds limit of {Max} bytes"); + } + + var app13Length = 2 + ProfileResolver.AdobePhotoshopApp13Marker.Length + + ProfileResolver.AdobeImageResourceBlockMarker.Length + + ProfileResolver.AdobeIptcMarker.Length + + 2 + 4 + data.Length; + this.WriteAppHeader(app13Length, JpegConstants.Markers.APP13); + this.outputStream.Write(ProfileResolver.AdobePhotoshopApp13Marker); + this.outputStream.Write(ProfileResolver.AdobeImageResourceBlockMarker); + this.outputStream.Write(ProfileResolver.AdobeIptcMarker); + this.outputStream.WriteByte(0); // a empty pascal string (padded to make size even) + this.outputStream.WriteByte(0); + BinaryPrimitives.WriteInt32BigEndian(this.buffer, data.Length); + this.outputStream.Write(this.buffer, 0, 4); + this.outputStream.Write(data, 0, data.Length); + } + /// /// Writes the App1 header. /// - /// The length of the data the app1 marker contains + /// The length of the data the app1 marker contains. private void WriteApp1Header(int app1Length) + { + this.WriteAppHeader(app1Length, JpegConstants.Markers.APP1); + } + + /// + /// Writes a AppX header. + /// + /// The length of the data the app marker contains. + /// The app marker to write. + private void WriteAppHeader(int length, byte appMarker) { this.buffer[0] = JpegConstants.Markers.XFF; - this.buffer[1] = JpegConstants.Markers.APP1; // Application Marker - this.buffer[2] = (byte)((app1Length >> 8) & 0xFF); - this.buffer[3] = (byte)(app1Length & 0xFF); + this.buffer[1] = appMarker; + this.buffer[2] = (byte)((length >> 8) & 0xFF); + this.buffer[3] = (byte)(length & 0xFF); this.outputStream.Write(this.buffer, 0, 4); } @@ -805,6 +856,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg metadata.SyncProfiles(); this.WriteExifProfile(metadata.ExifProfile); this.WriteIccProfile(metadata.IccProfile); + this.WriteIptcProfile(metadata.IptcProfile); } /// diff --git a/src/ImageSharp/Formats/Png/PngDecoderCore.cs b/src/ImageSharp/Formats/Png/PngDecoderCore.cs index 4d7de4161..bc7b9d815 100644 --- a/src/ImageSharp/Formats/Png/PngDecoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngDecoderCore.cs @@ -380,7 +380,12 @@ namespace SixLabors.ImageSharp.Formats.Png private void InitializeImage(ImageMetadata metadata, out Image image) where TPixel : unmanaged, IPixel { - image = new Image(this.configuration, this.header.Width, this.header.Height, metadata); + image = Image.CreateUninitialized( + this.configuration, + this.header.Width, + this.header.Height, + metadata); + this.bytesPerPixel = this.CalculateBytesPerPixel(); this.bytesPerScanline = this.CalculateScanlineLength(this.header.Width) + 1; this.bytesPerSample = 1; diff --git a/src/ImageSharp/Formats/Png/PngEncoderCore.cs b/src/ImageSharp/Formats/Png/PngEncoderCore.cs index 45e1ffd2d..fcbbc6697 100644 --- a/src/ImageSharp/Formats/Png/PngEncoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngEncoderCore.cs @@ -5,10 +5,8 @@ using System; using System.Buffers; using System.Buffers.Binary; using System.IO; -using System.Linq; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; - using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Formats.Png.Chunks; using SixLabors.ImageSharp.Formats.Png.Filters; @@ -16,7 +14,6 @@ using SixLabors.ImageSharp.Formats.Png.Zlib; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.PixelFormats; -using SixLabors.ImageSharp.Processing.Processors.Quantization; namespace SixLabors.ImageSharp.Formats.Png { @@ -633,10 +630,21 @@ namespace SixLabors.ImageSharp.Formats.Png private void WriteTextChunks(Stream stream, PngMetadata meta) { const int MaxLatinCode = 255; - foreach (PngTextData textData in meta.TextData) + for (int i = 0; i < meta.TextData.Count; i++) { - bool hasUnicodeCharacters = textData.Value.Any(c => c > MaxLatinCode); - if (hasUnicodeCharacters || (!string.IsNullOrWhiteSpace(textData.LanguageTag) || !string.IsNullOrWhiteSpace(textData.TranslatedKeyword))) + PngTextData textData = meta.TextData[i]; + bool hasUnicodeCharacters = false; + foreach (var c in textData.Value) + { + if (c > MaxLatinCode) + { + hasUnicodeCharacters = true; + break; + } + } + + if (hasUnicodeCharacters || (!string.IsNullOrWhiteSpace(textData.LanguageTag) || + !string.IsNullOrWhiteSpace(textData.TranslatedKeyword))) { // Write iTXt chunk. byte[] keywordBytes = PngConstants.Encoding.GetBytes(textData.Keyword); @@ -647,7 +655,8 @@ 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]; + Span outputBytes = new byte[keywordBytes.Length + textBytes.Length + + translatedKeyword.Length + languageTag.Length + 5]; keywordBytes.CopyTo(outputBytes); if (textData.Value.Length > this.options.TextCompressionThreshold) { @@ -667,7 +676,8 @@ namespace SixLabors.ImageSharp.Formats.Png if (textData.Value.Length > this.options.TextCompressionThreshold) { // Write zTXt chunk. - byte[] compressedData = this.GetCompressedTextBytes(PngConstants.Encoding.GetBytes(textData.Value)); + 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)); @@ -678,7 +688,8 @@ namespace SixLabors.ImageSharp.Formats.Png // 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)); + PngConstants.Encoding.GetBytes(textData.Value) + .CopyTo(outputBytes.Slice(textData.Keyword.Length + 1)); this.WriteChunk(stream, PngChunkType.Text, outputBytes.ToArray()); } } diff --git a/src/ImageSharp/Formats/Png/Zlib/Deflater.cs b/src/ImageSharp/Formats/Png/Zlib/Deflater.cs index 7398b089b..2083edab1 100644 --- a/src/ImageSharp/Formats/Png/Zlib/Deflater.cs +++ b/src/ImageSharp/Formats/Png/Zlib/Deflater.cs @@ -288,8 +288,6 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib this.engine = null; this.isDisposed = true; } - - GC.SuppressFinalize(this); } } } diff --git a/src/ImageSharp/Formats/Png/Zlib/DeflaterEngine.cs b/src/ImageSharp/Formats/Png/Zlib/DeflaterEngine.cs index 1a8bb4ab0..c1c86a98b 100644 --- a/src/ImageSharp/Formats/Png/Zlib/DeflaterEngine.cs +++ b/src/ImageSharp/Formats/Png/Zlib/DeflaterEngine.cs @@ -362,7 +362,7 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib more = this.inputEnd - this.inputOff; } - Array.Copy(this.inputBuf, this.inputOff, this.window, this.strstart + this.lookahead, more); + Buffer.BlockCopy(this.inputBuf, this.inputOff, this.window, this.strstart + this.lookahead, more); this.inputOff += more; this.lookahead += more; @@ -397,8 +397,6 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib this.isDisposed = true; } - - GC.SuppressFinalize(this); } [MethodImpl(InliningOptions.ShortMethod)] @@ -464,6 +462,7 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib /// /// The current match. /// True if a match greater than the minimum length is found + [MethodImpl(InliningOptions.HotPath)] private bool FindLongestMatch(int curMatch) { int match; diff --git a/src/ImageSharp/Formats/Png/Zlib/DeflaterHuffman.cs b/src/ImageSharp/Formats/Png/Zlib/DeflaterHuffman.cs index 543a1fe30..022d3d4d0 100644 --- a/src/ImageSharp/Formats/Png/Zlib/DeflaterHuffman.cs +++ b/src/ImageSharp/Formats/Png/Zlib/DeflaterHuffman.cs @@ -36,11 +36,6 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib private const int EofSymbol = 256; - private static readonly short[] StaticLCodes; - private static readonly byte[] StaticLLength; - private static readonly short[] StaticDCodes; - private static readonly byte[] StaticDLength; - private Tree literalTree; private Tree distTree; private Tree blTree; @@ -58,49 +53,6 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib private int extraBits; private bool isDisposed; - // TODO: These should be pre-generated array/readonlyspans. - static DeflaterHuffman() - { - // See RFC 1951 3.2.6 - // Literal codes - StaticLCodes = new short[LiteralNumber]; - StaticLLength = new byte[LiteralNumber]; - - int i = 0; - while (i < 144) - { - StaticLCodes[i] = BitReverse((0x030 + i) << 8); - StaticLLength[i++] = 8; - } - - while (i < 256) - { - StaticLCodes[i] = BitReverse((0x190 - 144 + i) << 7); - StaticLLength[i++] = 9; - } - - while (i < 280) - { - StaticLCodes[i] = BitReverse((0x000 - 256 + i) << 9); - StaticLLength[i++] = 7; - } - - while (i < LiteralNumber) - { - StaticLCodes[i] = BitReverse((0x0c0 - 280 + i) << 8); - StaticLLength[i++] = 8; - } - - // Distance codes - StaticDCodes = new short[DistanceNumber]; - StaticDLength = new byte[DistanceNumber]; - for (i = 0; i < DistanceNumber; i++) - { - StaticDCodes[i] = BitReverse(i << 11); - StaticDLength[i] = 5; - } - } - /// /// Initializes a new instance of the class. /// @@ -122,12 +74,80 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib this.pinnedLiteralBuffer = (short*)this.literalBufferHandle.Pointer; } +#pragma warning disable SA1201 // Elements should appear in the correct order + + // See RFC 1951 3.2.6 + // Literal codes + private static readonly short[] StaticLCodes = new short[] + { + 12, 140, 76, 204, 44, 172, 108, 236, 28, 156, 92, 220, 60, 188, 124, 252, + 2, 130, 66, 194, 34, 162, 98, 226, 18, 146, 82, 210, 50, 178, 114, 242, + 10, 138, 74, 202, 42, 170, 106, 234, 26, 154, 90, 218, 58, 186, 122, 250, + 6, 134, 70, 198, 38, 166, 102, 230, 22, 150, 86, 214, 54, 182, 118, 246, + 14, 142, 78, 206, 46, 174, 110, 238, 30, 158, 94, 222, 62, 190, 126, 254, + 1, 129, 65, 193, 33, 161, 97, 225, 17, 145, 81, 209, 49, 177, 113, 241, 9, + 137, 73, 201, 41, 169, 105, 233, 25, 153, 89, 217, 57, 185, 121, 249, 5, + 133, 69, 197, 37, 165, 101, 229, 21, 149, 85, 213, 53, 181, 117, 245, 13, + 141, 77, 205, 45, 173, 109, 237, 29, 157, 93, 221, 61, 189, 125, 253, 19, + 275, 147, 403, 83, 339, 211, 467, 51, 307, 179, 435, 115, 371, 243, 499, + 11, 267, 139, 395, 75, 331, 203, 459, 43, 299, 171, 427, 107, 363, 235, 491, + 27, 283, 155, 411, 91, 347, 219, 475, 59, 315, 187, 443, 123, 379, 251, 507, + 7, 263, 135, 391, 71, 327, 199, 455, 39, 295, 167, 423, 103, 359, 231, 487, + 23, 279, 151, 407, 87, 343, 215, 471, 55, 311, 183, 439, 119, 375, 247, 503, + 15, 271, 143, 399, 79, 335, 207, 463, 47, 303, 175, 431, 111, 367, 239, 495, + 31, 287, 159, 415, 95, 351, 223, 479, 63, 319, 191, 447, 127, 383, 255, 511, + 0, 64, 32, 96, 16, 80, 48, 112, 8, 72, 40, 104, 24, 88, 56, 120, 4, 68, 36, + 100, 20, 84, 52, 116, 3, 131, 67, 195, 35, 163 + }; + + private static ReadOnlySpan StaticLLength => new byte[] + { + 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, + 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, + 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, + 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, + 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, + 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, + 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, + 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, + 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, + 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, + 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, + 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, + 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, + 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, + 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, + 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, + 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, + 7, 7, 7, 7, 7, 7, 7, 7, 8, 8, 8, 8, 8, 8 + }; + + // Distance codes and lengths. + private static readonly short[] StaticDCodes = new short[] + { + 0, 16, 8, 24, 4, 20, 12, 28, 2, 18, 10, 26, 6, 22, 14, + 30, 1, 17, 9, 25, 5, 21, 13, 29, 3, 19, 11, 27, 7, 23 + }; + + private static ReadOnlySpan StaticDLength => new byte[] + { + 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, + 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5 + }; +#pragma warning restore SA1201 // Elements should appear in the correct order + /// /// Gets the lengths of the bit length codes are sent in order of decreasing probability, to avoid transmitting the lengths for unused bit length codes. /// - private static ReadOnlySpan BitLengthOrder => new byte[] { 16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15 }; + private static ReadOnlySpan BitLengthOrder => new byte[] + { + 16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15 + }; - private static ReadOnlySpan Bit4Reverse => new byte[] { 0, 8, 4, 12, 2, 10, 6, 14, 1, 9, 5, 13, 3, 11, 7, 15 }; + private static ReadOnlySpan Bit4Reverse => new byte[] + { + 0, 8, 4, 12, 2, 10, 6, 14, 1, 9, 5, 13, 3, 11, 7, 15 + }; /// /// Gets the pending buffer to use. @@ -413,8 +433,6 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib this.distTree = null; this.isDisposed = true; } - - GC.SuppressFinalize(this); } [MethodImpl(InliningOptions.ShortMethod)] @@ -553,6 +571,7 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib } } + [MethodImpl(InliningOptions.HotPath)] public void BuildTree() { int numSymbols = this.elementCount; @@ -964,8 +983,6 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib this.isDisposed = true; } - - GC.SuppressFinalize(this); } } } diff --git a/src/ImageSharp/Formats/Png/Zlib/DeflaterPendingBuffer.cs b/src/ImageSharp/Formats/Png/Zlib/DeflaterPendingBuffer.cs index 731c9e80f..0414ca2f8 100644 --- a/src/ImageSharp/Formats/Png/Zlib/DeflaterPendingBuffer.cs +++ b/src/ImageSharp/Formats/Png/Zlib/DeflaterPendingBuffer.cs @@ -172,8 +172,6 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib this.bufferMemoryOwner = null; this.isDisposed = true; } - - GC.SuppressFinalize(this); } } } diff --git a/src/ImageSharp/ImageExtensions.cs b/src/ImageSharp/ImageExtensions.cs index 0bdbcc4ab..62ac449b7 100644 --- a/src/ImageSharp/ImageExtensions.cs +++ b/src/ImageSharp/ImageExtensions.cs @@ -103,14 +103,18 @@ namespace SixLabors.ImageSharp /// /// Returns a Base64 encoded string from the given image. + /// The result is prepended with a Data URI + /// + /// + /// For example: + /// + /// + /// /// - /// - /// The pixel format. /// The source image /// The format. /// The - public static string ToBase64String(this Image source, IImageFormat format) - where TPixel : unmanaged, IPixel + public static string ToBase64String(this Image source, IImageFormat format) { using var stream = new MemoryStream(); source.Save(stream, format); diff --git a/src/ImageSharp/Metadata/ImageMetadata.cs b/src/ImageSharp/Metadata/ImageMetadata.cs index b3751bfbd..716e89e68 100644 --- a/src/ImageSharp/Metadata/ImageMetadata.cs +++ b/src/ImageSharp/Metadata/ImageMetadata.cs @@ -5,6 +5,7 @@ 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; namespace SixLabors.ImageSharp.Metadata { @@ -65,6 +66,7 @@ namespace SixLabors.ImageSharp.Metadata this.ExifProfile = other.ExifProfile?.DeepClone(); this.IccProfile = other.IccProfile?.DeepClone(); + this.IptcProfile = other.IptcProfile?.DeepClone(); } /// @@ -122,6 +124,11 @@ namespace SixLabors.ImageSharp.Metadata /// public IccProfile IccProfile { get; set; } + /// + /// Gets or sets the iptc profile. + /// + public IptcProfile IptcProfile { get; set; } + /// /// Gets the metadata value associated with the specified key. /// diff --git a/src/ImageSharp/Metadata/Profiles/Exif/ExifProfile.cs b/src/ImageSharp/Metadata/Profiles/Exif/ExifProfile.cs index 11d0bd01b..29c21d611 100644 --- a/src/ImageSharp/Metadata/Profiles/Exif/ExifProfile.cs +++ b/src/ImageSharp/Metadata/Profiles/Exif/ExifProfile.cs @@ -57,8 +57,11 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif /// by making a copy from another EXIF profile. /// /// The other EXIF profile, where the clone should be made from. + /// is null.> private ExifProfile(ExifProfile other) { + Guard.NotNull(other, nameof(other)); + this.Parts = other.Parts; this.thumbnailLength = other.thumbnailLength; this.thumbnailOffset = other.thumbnailOffset; diff --git a/src/ImageSharp/Metadata/Profiles/IPTC/IIMV4.2_IPTC.pdf b/src/ImageSharp/Metadata/Profiles/IPTC/IIMV4.2_IPTC.pdf new file mode 100644 index 000000000..b00355181 Binary files /dev/null and b/src/ImageSharp/Metadata/Profiles/IPTC/IIMV4.2_IPTC.pdf differ diff --git a/src/ImageSharp/Metadata/Profiles/IPTC/IptcProfile.cs b/src/ImageSharp/Metadata/Profiles/IPTC/IptcProfile.cs new file mode 100644 index 000000000..9206e4377 --- /dev/null +++ b/src/ImageSharp/Metadata/Profiles/IPTC/IptcProfile.cs @@ -0,0 +1,298 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Buffers.Binary; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Text; + +namespace SixLabors.ImageSharp.Metadata.Profiles.Iptc +{ + /// + /// Represents an IPTC profile providing access to the collection of values. + /// + public sealed class IptcProfile : IDeepCloneable + { + private Collection values; + + /// + /// Initializes a new instance of the class. + /// + public IptcProfile() + : this((byte[])null) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The byte array to read the iptc profile from. + public IptcProfile(byte[] data) + { + this.Data = data; + this.Initialize(); + } + + /// + /// Initializes a new instance of the class + /// by making a copy from another IPTC profile. + /// + /// The other IPTC profile, from which the clone should be made from. + private IptcProfile(IptcProfile other) + { + Guard.NotNull(other, nameof(other)); + + if (other.values != null) + { + this.values = new Collection(); + + foreach (IptcValue value in other.Values) + { + this.values.Add(value.DeepClone()); + } + } + + if (other.Data != null) + { + this.Data = new byte[other.Data.Length]; + other.Data.AsSpan().CopyTo(this.Data); + } + } + + /// + /// Gets the byte data of the IPTC profile. + /// + public byte[] Data { get; private set; } + + /// + /// Gets the values of this iptc profile. + /// + public IEnumerable Values + { + get + { + this.Initialize(); + return this.values; + } + } + + /// + public IptcProfile DeepClone() => new IptcProfile(this); + + /// + /// Returns all value with the specified tag. + /// + /// The tag of the iptc value. + /// The values found with the specified tag. + public List GetValues(IptcTag tag) + { + var iptcValues = new List(); + foreach (IptcValue iptcValue in this.Values) + { + if (iptcValue.Tag == tag) + { + iptcValues.Add(iptcValue); + } + } + + return iptcValues; + } + + /// + /// Removes all values with the specified tag. + /// + /// The tag of the iptc value to remove. + /// True when the value was found and removed. + public bool RemoveValue(IptcTag tag) + { + this.Initialize(); + + bool removed = false; + for (int i = this.values.Count - 1; i >= 0; i--) + { + if (this.values[i].Tag == tag) + { + this.values.RemoveAt(i); + removed = true; + } + } + + return removed; + } + + /// + /// Removes values with the specified tag and value. + /// + /// The tag of the iptc value to remove. + /// The value of the iptc item to remove. + /// True when the value was found and removed. + public bool RemoveValue(IptcTag tag, string value) + { + this.Initialize(); + + bool removed = false; + for (int i = this.values.Count - 1; i >= 0; i--) + { + if (this.values[i].Tag == tag && this.values[i].Value.Equals(value)) + { + this.values.RemoveAt(i); + removed = true; + } + } + + return removed; + } + + /// + /// Changes the encoding for all the values. + /// + /// The encoding to use when storing the bytes. + public void SetEncoding(Encoding encoding) + { + Guard.NotNull(encoding, nameof(encoding)); + + foreach (IptcValue value in this.Values) + { + value.Encoding = encoding; + } + } + + /// + /// Sets the value for the specified tag. + /// + /// The tag of the iptc value. + /// The encoding to use when storing the bytes. + /// The value. + /// + /// Indicates if length restrictions from the specification should be followed strictly. + /// Defaults to true. + /// + public void SetValue(IptcTag tag, Encoding encoding, string value, bool strict = true) + { + Guard.NotNull(encoding, nameof(encoding)); + Guard.NotNull(value, nameof(value)); + + if (!tag.IsRepeatable()) + { + foreach (IptcValue iptcValue in this.Values) + { + if (iptcValue.Tag == tag) + { + iptcValue.Strict = strict; + iptcValue.Encoding = encoding; + iptcValue.Value = value; + return; + } + } + } + + this.values.Add(new IptcValue(tag, encoding, value, strict)); + } + + /// + /// Makes sure the datetime is formatted according to the iptc specification. + /// + /// A date will be formatted as CCYYMMDD, e.g. "19890317" for 17 March 1989. + /// A time value will be formatted as HHMMSS±HHMM, e.g. "090000+0200" for 9 o'clock Berlin time, + /// two hours ahead of UTC. + /// + /// + /// The tag of the iptc value. + /// The datetime. + public void SetDateTimeValue(IptcTag tag, DateTimeOffset dateTimeOffset) + { + if (!tag.IsDate() && !tag.IsTime()) + { + throw new ArgumentException("iptc tag is not a time or date type"); + } + + var formattedDate = tag.IsDate() + ? dateTimeOffset.ToString("yyyyMMdd", System.Globalization.CultureInfo.InvariantCulture) + : dateTimeOffset.ToString("HHmmsszzzz", System.Globalization.CultureInfo.InvariantCulture) + .Replace(":", string.Empty); + + this.SetValue(tag, Encoding.UTF8, formattedDate); + } + + /// + /// Sets the value of the specified tag. + /// + /// The tag of the iptc value. + /// The value. + /// + /// Indicates if length restrictions from the specification should be followed strictly. + /// Defaults to true. + /// + public void SetValue(IptcTag tag, string value, bool strict = true) => this.SetValue(tag, Encoding.UTF8, value, strict); + + /// + /// Updates the data of the profile. + /// + public void UpdateData() + { + var length = 0; + foreach (IptcValue value in this.Values) + { + length += value.Length + 5; + } + + this.Data = new byte[length]; + + int i = 0; + foreach (IptcValue value in this.Values) + { + this.Data[i++] = 28; + this.Data[i++] = 2; + this.Data[i++] = (byte)value.Tag; + this.Data[i++] = (byte)(value.Length >> 8); + this.Data[i++] = (byte)value.Length; + if (value.Length > 0) + { + Buffer.BlockCopy(value.ToByteArray(), 0, this.Data, i, value.Length); + i += value.Length; + } + } + } + + private void Initialize() + { + if (this.values != null) + { + return; + } + + this.values = new Collection(); + + if (this.Data == null || this.Data[0] != 0x1c) + { + return; + } + + int i = 0; + while (i + 4 < this.Data.Length) + { + if (this.Data[i++] != 28) + { + continue; + } + + i++; + + var tag = (IptcTag)this.Data[i++]; + + int count = BinaryPrimitives.ReadInt16BigEndian(this.Data.AsSpan(i, 2)); + i += 2; + + var iptcData = new byte[count]; + if ((count > 0) && (i + count <= this.Data.Length)) + { + Buffer.BlockCopy(this.Data, i, iptcData, 0, count); + this.values.Add(new IptcValue(tag, iptcData, false)); + } + + i += count; + } + } + } +} diff --git a/src/ImageSharp/Metadata/Profiles/IPTC/IptcTag.cs b/src/ImageSharp/Metadata/Profiles/IPTC/IptcTag.cs new file mode 100644 index 000000000..7258a0291 --- /dev/null +++ b/src/ImageSharp/Metadata/Profiles/IPTC/IptcTag.cs @@ -0,0 +1,397 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +namespace SixLabors.ImageSharp.Metadata.Profiles.Iptc +{ + /// + /// Provides enumeration of all IPTC tags relevant for images. + /// + public enum IptcTag + { + /// + /// Unknown. + /// + Unknown = -1, + + /// + /// Record version identifying the version of the Information Interchange Model. + /// Not repeatable. Max length is 2. + /// + RecordVersion = 0, + + /// + /// Object type, not repeatable. Max Length is 67. + /// + ObjectType = 3, + + /// + /// Object attribute. Max length is 68. + /// + ObjectAttribute = 4, + + /// + /// Object Name, not repeatable. Max length is 64. + /// + Name = 5, + + /// + /// Edit status, not repeatable. Max length is 64. + /// + EditStatus = 7, + + /// + /// Editorial update, not repeatable. Max length is 2. + /// + EditorialUpdate = 8, + + /// + /// Urgency, not repeatable. Max length is 2. + /// + Urgency = 10, + + /// + /// Subject Reference. Max length is 236. + /// + SubjectReference = 12, + + /// + /// Category, not repeatable. Max length is 3. + /// + Category = 15, + + /// + /// Supplemental categories. Max length is 32. + /// + SupplementalCategories = 20, + + /// + /// Fixture identifier, not repeatable. Max length is 32. + /// + FixtureIdentifier = 22, + + /// + /// Keywords. Max length is 64. + /// + Keywords = 25, + + /// + /// Location code. Max length is 3. + /// + LocationCode = 26, + + /// + /// Location name. Max length is 64. + /// + LocationName = 27, + + /// + /// Release date. Format should be CCYYMMDD. + /// Not repeatable, max length is 8. + /// + /// A date will be formatted as CCYYMMDD, e.g. "19890317" for 17 March 1989. + /// + /// + ReleaseDate = 30, + + /// + /// Release time. Format should be HHMMSS±HHMM. + /// Not repeatable, max length is 11. + /// + /// A time value will be formatted as HHMMSS±HHMM, e.g. "090000+0200" for 9 o'clock Berlin time, + /// two hours ahead of UTC. + /// + /// + ReleaseTime = 35, + + /// + /// Expiration date. Format should be CCYYMMDD. + /// Not repeatable, max length is 8. + /// + /// A date will be formatted as CCYYMMDD, e.g. "19890317" for 17 March 1989. + /// + /// + ExpirationDate = 37, + + /// + /// Expiration time. Format should be HHMMSS±HHMM. + /// Not repeatable, max length is 11. + /// + /// A time value will be formatted as HHMMSS±HHMM, e.g. "090000+0200" for 9 o'clock Berlin time, + /// two hours ahead of UTC. + /// + /// + ExpirationTime = 38, + + /// + /// Special instructions, not repeatable. Max length is 256. + /// + SpecialInstructions = 40, + + /// + /// Action advised, not repeatable. Max length is 2. + /// + ActionAdvised = 42, + + /// + /// Reference service. Max length is 10. + /// + ReferenceService = 45, + + /// + /// Reference date. Format should be CCYYMMDD. + /// Not repeatable, max length is 8. + /// + /// A date will be formatted as CCYYMMDD, e.g. "19890317" for 17 March 1989. + /// + /// + ReferenceDate = 47, + + /// + /// ReferenceNumber. Max length is 8. + /// + ReferenceNumber = 50, + + /// + /// Created date. Format should be CCYYMMDD. + /// Not repeatable, max length is 8. + /// + /// A date will be formatted as CCYYMMDD, e.g. "19890317" for 17 March 1989. + /// + /// + CreatedDate = 55, + + /// + /// Created time. Format should be HHMMSS±HHMM. + /// Not repeatable, max length is 11. + /// + /// A time value will be formatted as HHMMSS±HHMM, e.g. "090000+0200" for 9 o'clock Berlin time, + /// two hours ahead of UTC. + /// + /// + CreatedTime = 60, + + /// + /// Digital creation date. Format should be CCYYMMDD. + /// Not repeatable, max length is 8. + /// + /// A date will be formatted as CCYYMMDD, e.g. "19890317" for 17 March 1989. + /// + /// + DigitalCreationDate = 62, + + /// + /// Digital creation time. Format should be HHMMSS±HHMM. + /// Not repeatable, max length is 11. + /// + /// A time value will be formatted as HHMMSS±HHMM, e.g. "090000+0200" for 9 o'clock Berlin time, + /// two hours ahead of UTC. + /// + /// + DigitalCreationTime = 63, + + /// + /// Originating program, not repeatable. Max length is 32. + /// + OriginatingProgram = 65, + + /// + /// Program version, not repeatable. Max length is 10. + /// + ProgramVersion = 70, + + /// + /// Object cycle, not repeatable. Max length is 1. + /// + ObjectCycle = 75, + + /// + /// Byline. Max length is 32. + /// + Byline = 80, + + /// + /// Byline title. Max length is 32. + /// + BylineTitle = 85, + + /// + /// City, not repeatable. Max length is 32. + /// + City = 90, + + /// + /// Sub location, not repeatable. Max length is 32. + /// + SubLocation = 92, + + /// + /// Province/State, not repeatable. Max length is 32. + /// + ProvinceState = 95, + + /// + /// Country code, not repeatable. Max length is 3. + /// + CountryCode = 100, + + /// + /// Country, not repeatable. Max length is 64. + /// + Country = 101, + + /// + /// Original transmission reference, not repeatable. Max length is 32. + /// + OriginalTransmissionReference = 103, + + /// + /// Headline, not repeatable. Max length is 256. + /// + Headline = 105, + + /// + /// Credit, not repeatable. Max length is 32. + /// + Credit = 110, + + /// + /// Source, not repeatable. Max length is 32. + /// + Source = 115, + + /// + /// Copyright notice, not repeatable. Max length is 128. + /// + CopyrightNotice = 116, + + /// + /// Contact. Max length 128. + /// + Contact = 118, + + /// + /// Caption, not repeatable. Max length is 2000. + /// + Caption = 120, + + /// + /// Local caption. + /// + LocalCaption = 121, + + /// + /// Caption writer. Max length is 32. + /// + CaptionWriter = 122, + + /// + /// Image type, not repeatable. Max length is 2. + /// + ImageType = 130, + + /// + /// Image orientation, not repeatable. Max length is 1. + /// + ImageOrientation = 131, + + /// + /// Custom field 1 + /// + CustomField1 = 200, + + /// + /// Custom field 2 + /// + CustomField2 = 201, + + /// + /// Custom field 3 + /// + CustomField3 = 202, + + /// + /// Custom field 4 + /// + CustomField4 = 203, + + /// + /// Custom field 5 + /// + CustomField5 = 204, + + /// + /// Custom field 6 + /// + CustomField6 = 205, + + /// + /// Custom field 7 + /// + CustomField7 = 206, + + /// + /// Custom field 8 + /// + CustomField8 = 207, + + /// + /// Custom field 9 + /// + CustomField9 = 208, + + /// + /// Custom field 10 + /// + CustomField10 = 209, + + /// + /// Custom field 11 + /// + CustomField11 = 210, + + /// + /// Custom field 12 + /// + CustomField12 = 211, + + /// + /// Custom field 13 + /// + CustomField13 = 212, + + /// + /// Custom field 14 + /// + CustomField14 = 213, + + /// + /// Custom field 15 + /// + CustomField15 = 214, + + /// + /// Custom field 16 + /// + CustomField16 = 215, + + /// + /// Custom field 17 + /// + CustomField17 = 216, + + /// + /// Custom field 18 + /// + CustomField18 = 217, + + /// + /// Custom field 19 + /// + CustomField19 = 218, + + /// + /// Custom field 20 + /// + CustomField20 = 219, + } +} diff --git a/src/ImageSharp/Metadata/Profiles/IPTC/IptcTagExtensions.cs b/src/ImageSharp/Metadata/Profiles/IPTC/IptcTagExtensions.cs new file mode 100644 index 000000000..6b39769a7 --- /dev/null +++ b/src/ImageSharp/Metadata/Profiles/IPTC/IptcTagExtensions.cs @@ -0,0 +1,162 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +namespace SixLabors.ImageSharp.Metadata.Profiles.Iptc +{ + /// + /// Extension methods for IPTC tags. + /// + public static class IptcTagExtensions + { + /// + /// Maximum length of the IPTC value with the given tag according to the specification. + /// + /// The tag to check the max length for. + /// The maximum length. + public static int MaxLength(this IptcTag tag) + { + return tag switch + { + IptcTag.RecordVersion => 2, + IptcTag.ObjectType => 67, + IptcTag.ObjectAttribute => 68, + IptcTag.Name => 64, + IptcTag.EditStatus => 64, + IptcTag.EditorialUpdate => 2, + IptcTag.Urgency => 1, + IptcTag.SubjectReference => 236, + IptcTag.Category => 3, + IptcTag.SupplementalCategories => 32, + IptcTag.FixtureIdentifier => 32, + IptcTag.Keywords => 64, + IptcTag.LocationCode => 3, + IptcTag.LocationName => 64, + IptcTag.ReleaseDate => 8, + IptcTag.ReleaseTime => 11, + IptcTag.ExpirationDate => 8, + IptcTag.ExpirationTime => 11, + IptcTag.SpecialInstructions => 256, + IptcTag.ActionAdvised => 2, + IptcTag.ReferenceService => 10, + IptcTag.ReferenceDate => 8, + IptcTag.ReferenceNumber => 8, + IptcTag.CreatedDate => 8, + IptcTag.CreatedTime => 11, + IptcTag.DigitalCreationDate => 8, + IptcTag.DigitalCreationTime => 11, + IptcTag.OriginatingProgram => 32, + IptcTag.ProgramVersion => 10, + IptcTag.ObjectCycle => 1, + IptcTag.Byline => 32, + IptcTag.BylineTitle => 32, + IptcTag.City => 32, + IptcTag.SubLocation => 32, + IptcTag.ProvinceState => 32, + IptcTag.CountryCode => 3, + IptcTag.Country => 64, + IptcTag.OriginalTransmissionReference => 32, + IptcTag.Headline => 256, + IptcTag.Credit => 32, + IptcTag.Source => 32, + IptcTag.CopyrightNotice => 128, + IptcTag.Contact => 128, + IptcTag.Caption => 2000, + IptcTag.CaptionWriter => 32, + IptcTag.ImageType => 2, + IptcTag.ImageOrientation => 1, + _ => 256 + }; + } + + /// + /// Determines if the given tag can be repeated according to the specification. + /// + /// The tag to check. + /// True, if the tag can occur multiple times. + public static bool IsRepeatable(this IptcTag tag) + { + switch (tag) + { + case IptcTag.RecordVersion: + case IptcTag.ObjectType: + case IptcTag.Name: + case IptcTag.EditStatus: + case IptcTag.EditorialUpdate: + case IptcTag.Urgency: + case IptcTag.Category: + case IptcTag.FixtureIdentifier: + case IptcTag.ReleaseDate: + case IptcTag.ReleaseTime: + case IptcTag.ExpirationDate: + case IptcTag.ExpirationTime: + case IptcTag.SpecialInstructions: + case IptcTag.ActionAdvised: + case IptcTag.CreatedDate: + case IptcTag.CreatedTime: + case IptcTag.DigitalCreationDate: + case IptcTag.DigitalCreationTime: + case IptcTag.OriginatingProgram: + case IptcTag.ProgramVersion: + case IptcTag.ObjectCycle: + case IptcTag.City: + case IptcTag.SubLocation: + case IptcTag.ProvinceState: + case IptcTag.CountryCode: + case IptcTag.Country: + case IptcTag.OriginalTransmissionReference: + case IptcTag.Headline: + case IptcTag.Credit: + case IptcTag.Source: + case IptcTag.CopyrightNotice: + case IptcTag.Caption: + case IptcTag.ImageType: + case IptcTag.ImageOrientation: + return false; + + default: + return true; + } + } + + /// + /// Determines if the tag is a datetime tag which needs to be formatted as CCYYMMDD. + /// + /// The tag to check. + /// True, if its a datetime tag. + public static bool IsDate(this IptcTag tag) + { + switch (tag) + { + case IptcTag.CreatedDate: + case IptcTag.DigitalCreationDate: + case IptcTag.ExpirationDate: + case IptcTag.ReferenceDate: + case IptcTag.ReleaseDate: + return true; + + default: + return false; + } + } + + /// + /// Determines if the tag is a time tag which need to be formatted as HHMMSS±HHMM. + /// + /// The tag to check. + /// True, if its a time tag. + public static bool IsTime(this IptcTag tag) + { + switch (tag) + { + case IptcTag.CreatedTime: + case IptcTag.DigitalCreationTime: + case IptcTag.ExpirationTime: + case IptcTag.ReleaseTime: + return true; + + default: + return false; + } + } + } +} diff --git a/src/ImageSharp/Metadata/Profiles/IPTC/IptcValue.cs b/src/ImageSharp/Metadata/Profiles/IPTC/IptcValue.cs new file mode 100644 index 000000000..e63781012 --- /dev/null +++ b/src/ImageSharp/Metadata/Profiles/IPTC/IptcValue.cs @@ -0,0 +1,219 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Text; + +namespace SixLabors.ImageSharp.Metadata.Profiles.Iptc +{ + /// + /// Represents a single value of the IPTC profile. + /// + public sealed class IptcValue : IDeepCloneable + { + private byte[] data = Array.Empty(); + private Encoding encoding; + + internal IptcValue(IptcValue other) + { + if (other.data != null) + { + this.data = new byte[other.data.Length]; + other.data.AsSpan().CopyTo(this.data); + } + + if (other.Encoding != null) + { + this.Encoding = (Encoding)other.Encoding.Clone(); + } + + this.Tag = other.Tag; + this.Strict = other.Strict; + } + + internal IptcValue(IptcTag tag, byte[] value, bool strict) + { + Guard.NotNull(value, nameof(value)); + + this.Strict = strict; + this.Tag = tag; + this.data = value; + this.encoding = Encoding.UTF8; + } + + internal IptcValue(IptcTag tag, Encoding encoding, string value, bool strict) + { + this.Strict = strict; + this.Tag = tag; + this.encoding = encoding; + this.Value = value; + } + + internal IptcValue(IptcTag tag, string value, bool strict) + { + this.Strict = strict; + this.Tag = tag; + this.encoding = Encoding.UTF8; + this.Value = value; + } + + /// + /// Gets or sets the encoding to use for the Value. + /// + public Encoding Encoding + { + get => this.encoding; + set + { + if (value != null) + { + this.encoding = value; + } + } + } + + /// + /// Gets the tag of the iptc value. + /// + public IptcTag Tag { get; } + + /// + /// Gets or sets a value indicating whether to be enforce value length restrictions according + /// to the specification. + /// + public bool Strict { get; set; } + + /// + /// Gets or sets the value. + /// + public string Value + { + get => this.encoding.GetString(this.data); + set + { + if (string.IsNullOrEmpty(value)) + { + this.data = Array.Empty(); + } + else + { + int maxLength = this.Tag.MaxLength(); + byte[] valueBytes; + if (this.Strict && value.Length > maxLength) + { + var cappedValue = value.Substring(0, maxLength); + valueBytes = this.encoding.GetBytes(cappedValue); + + // It is still possible that the bytes of the string exceed the limit. + if (valueBytes.Length > maxLength) + { + throw new ArgumentException($"The iptc value exceeds the limit of {maxLength} bytes for the tag {this.Tag}"); + } + } + else + { + valueBytes = this.encoding.GetBytes(value); + } + + this.data = valueBytes; + } + } + } + + /// + /// Gets the length of the value. + /// + public int Length => this.data.Length; + + /// + public IptcValue DeepClone() => new IptcValue(this); + + /// + /// Determines whether the specified object is equal to the current . + /// + /// The object to compare this with. + /// True when the specified object is equal to the current . + public override bool Equals(object obj) + { + if (ReferenceEquals(this, obj)) + { + return true; + } + + return this.Equals(obj as IptcValue); + } + + /// + /// Determines whether the specified iptc value is equal to the current . + /// + /// The iptc value to compare this with. + /// True when the specified iptc value is equal to the current . + public bool Equals(IptcValue other) + { + if (other is null) + { + return false; + } + + if (ReferenceEquals(this, other)) + { + return true; + } + + if (this.Tag != other.Tag) + { + return false; + } + + if (this.data.Length != other.data.Length) + { + return false; + } + + for (int i = 0; i < this.data.Length; i++) + { + if (this.data[i] != other.data[i]) + { + return false; + } + } + + return true; + } + + /// + /// Serves as a hash of this type. + /// + /// A hash code for the current instance. + public override int GetHashCode() => HashCode.Combine(this.data, this.Tag); + + /// + /// Converts this instance to a byte array. + /// + /// A array. + public byte[] ToByteArray() + { + var result = new byte[this.data.Length]; + this.data.CopyTo(result, 0); + return result; + } + + /// + /// Returns a string that represents the current value. + /// + /// A string that represents the current value. + public override string ToString() => this.Value; + + /// + /// Returns a string that represents the current value with the specified encoding. + /// + /// The encoding to use. + /// A string that represents the current value with the specified encoding. + public string ToString(Encoding encoding) + { + Guard.NotNull(encoding, nameof(encoding)); + + return encoding.GetString(this.data); + } + } +} diff --git a/src/ImageSharp/Metadata/Profiles/IPTC/README.md b/src/ImageSharp/Metadata/Profiles/IPTC/README.md new file mode 100644 index 000000000..1217ca0c7 --- /dev/null +++ b/src/ImageSharp/Metadata/Profiles/IPTC/README.md @@ -0,0 +1,11 @@ +IPTC source code is from [Magick.NET](https://github.com/dlemstra/Magick.NET) + +Information about IPTC can be found here in the following sources: + +- [metacpan.org, APP13-segment](https://metacpan.org/pod/Image::MetaData::JPEG::Structures#Structure-of-a-Photoshop-style-APP13-segment) + +- [iptc.org](https://www.iptc.org/std/photometadata/documentation/userguide/) + +- [Adobe File Formats Specification](http://oldschoolprg.x10.mx/downloads/ps6ffspecsv2.pdf) + +- [Tag Overview](https://exiftool.org/TagNames/IPTC.html) \ No newline at end of file diff --git a/tests/ImageSharp.Tests/Metadata/Profiles/IPTC/IptcProfileTests.cs b/tests/ImageSharp.Tests/Metadata/Profiles/IPTC/IptcProfileTests.cs new file mode 100644 index 000000000..d9f44cef9 --- /dev/null +++ b/tests/ImageSharp.Tests/Metadata/Profiles/IPTC/IptcProfileTests.cs @@ -0,0 +1,359 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using SixLabors.ImageSharp.Formats.Jpeg; +using SixLabors.ImageSharp.Metadata.Profiles.Iptc; +using SixLabors.ImageSharp.PixelFormats; +using Xunit; + +namespace SixLabors.ImageSharp.Tests.Metadata.Profiles.IPTC +{ + public class IptcProfileTests + { + private static JpegDecoder JpegDecoder => new JpegDecoder() { IgnoreMetadata = false }; + + public static IEnumerable AllIptcTags() + { + foreach (object tag in Enum.GetValues(typeof(IptcTag))) + { + yield return new object[] { tag }; + } + } + + [Theory] + [MemberData("AllIptcTags")] + public void IptcProfile_SetValue_WithStrictOption_Works(IptcTag tag) + { + // arrange + var profile = new IptcProfile(); + var value = new string('s', tag.MaxLength() + 1); + var expectedLength = tag.MaxLength(); + + // act + profile.SetValue(tag, value); + + // assert + IptcValue actual = profile.GetValues(tag).First(); + Assert.Equal(expectedLength, actual.Value.Length); + } + + [Theory] + [InlineData(IptcTag.DigitalCreationDate)] + [InlineData(IptcTag.ExpirationDate)] + [InlineData(IptcTag.CreatedDate)] + [InlineData(IptcTag.ReferenceDate)] + [InlineData(IptcTag.ReleaseDate)] + public void IptcProfile_SetDateValue_Works(IptcTag tag) + { + // arrange + var profile = new IptcProfile(); + var datetime = new DateTimeOffset(new DateTime(1994, 3, 17)); + + // act + profile.SetDateTimeValue(tag, datetime); + + // assert + IptcValue actual = profile.GetValues(tag).First(); + Assert.Equal("19940317", actual.Value); + } + + [Theory] + [InlineData(IptcTag.CreatedTime)] + [InlineData(IptcTag.DigitalCreationTime)] + [InlineData(IptcTag.ExpirationTime)] + [InlineData(IptcTag.ReleaseTime)] + public void IptcProfile_SetTimeValue_Works(IptcTag tag) + { + // arrange + var profile = new IptcProfile(); + var dateTimeUtc = new DateTime(1994, 3, 17, 14, 15, 16, DateTimeKind.Utc); + DateTimeOffset dateTimeOffset = new DateTimeOffset(dateTimeUtc).ToOffset(TimeSpan.FromHours(2)); + + // act + profile.SetDateTimeValue(tag, dateTimeOffset); + + // assert + IptcValue actual = profile.GetValues(tag).First(); + Assert.Equal("161516+0200", actual.Value); + } + + [Theory] + [WithFile(TestImages.Jpeg.Baseline.Iptc, PixelTypes.Rgba32)] + public void ReadIptcMetadata_Works(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using (Image image = provider.GetImage(JpegDecoder)) + { + Assert.NotNull(image.Metadata.IptcProfile); + var iptcValues = image.Metadata.IptcProfile.Values.ToList(); + ContainsIptcValue(iptcValues, IptcTag.Caption, "description"); + ContainsIptcValue(iptcValues, IptcTag.CaptionWriter, "description writer"); + ContainsIptcValue(iptcValues, IptcTag.Headline, "headline"); + ContainsIptcValue(iptcValues, IptcTag.SpecialInstructions, "special instructions"); + ContainsIptcValue(iptcValues, IptcTag.Byline, "author"); + ContainsIptcValue(iptcValues, IptcTag.BylineTitle, "author title"); + ContainsIptcValue(iptcValues, IptcTag.Credit, "credits"); + ContainsIptcValue(iptcValues, IptcTag.Source, "source"); + ContainsIptcValue(iptcValues, IptcTag.Name, "title"); + ContainsIptcValue(iptcValues, IptcTag.CreatedDate, "20200414"); + ContainsIptcValue(iptcValues, IptcTag.City, "city"); + ContainsIptcValue(iptcValues, IptcTag.SubLocation, "sublocation"); + ContainsIptcValue(iptcValues, IptcTag.ProvinceState, "province-state"); + ContainsIptcValue(iptcValues, IptcTag.Country, "country"); + ContainsIptcValue(iptcValues, IptcTag.Category, "category"); + ContainsIptcValue(iptcValues, IptcTag.Urgency, "1"); + ContainsIptcValue(iptcValues, IptcTag.Keywords, "keywords"); + ContainsIptcValue(iptcValues, IptcTag.CopyrightNotice, "copyright"); + } + } + + [Theory] + [WithFile(TestImages.Jpeg.Baseline.App13WithEmptyIptc, PixelTypes.Rgba32)] + public void ReadApp13_WithEmptyIptc_Works(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image image = provider.GetImage(JpegDecoder); + Assert.Null(image.Metadata.IptcProfile); + } + + [Fact] + public void IptcProfile_ToAndFromByteArray_Works() + { + // arrange + var profile = new IptcProfile(); + var expectedCaptionWriter = "unittest"; + var expectedCaption = "test"; + profile.SetValue(IptcTag.CaptionWriter, expectedCaptionWriter); + profile.SetValue(IptcTag.Caption, expectedCaption); + + // act + profile.UpdateData(); + byte[] profileBytes = profile.Data; + var profileFromBytes = new IptcProfile(profileBytes); + + // assert + var iptcValues = profileFromBytes.Values.ToList(); + ContainsIptcValue(iptcValues, IptcTag.CaptionWriter, expectedCaptionWriter); + ContainsIptcValue(iptcValues, IptcTag.Caption, expectedCaption); + } + + [Fact] + public void IptcProfile_CloneIsDeep() + { + // arrange + var profile = new IptcProfile(); + var captionWriter = "unittest"; + var caption = "test"; + profile.SetValue(IptcTag.CaptionWriter, captionWriter); + profile.SetValue(IptcTag.Caption, caption); + + // act + IptcProfile clone = profile.DeepClone(); + clone.SetValue(IptcTag.Caption, "changed"); + + // assert + Assert.Equal(2, clone.Values.Count()); + var cloneValues = clone.Values.ToList(); + ContainsIptcValue(cloneValues, IptcTag.CaptionWriter, captionWriter); + ContainsIptcValue(cloneValues, IptcTag.Caption, "changed"); + ContainsIptcValue(profile.Values.ToList(), IptcTag.Caption, caption); + } + + [Fact] + public void IptcValue_CloneIsDeep() + { + // arrange + var iptcValue = new IptcValue(IptcTag.Caption, System.Text.Encoding.UTF8, "test", true); + + // act + IptcValue clone = iptcValue.DeepClone(); + clone.Value = "changed"; + + // assert + Assert.NotEqual(iptcValue.Value, clone.Value); + } + + [Fact] + public void WritingImage_PreservesIptcProfile() + { + // arrange + var image = new Image(1, 1); + image.Metadata.IptcProfile = new IptcProfile(); + var expectedCaptionWriter = "unittest"; + var expectedCaption = "test"; + image.Metadata.IptcProfile.SetValue(IptcTag.CaptionWriter, expectedCaptionWriter); + image.Metadata.IptcProfile.SetValue(IptcTag.Caption, expectedCaption); + + // act + Image reloadedImage = WriteAndReadJpeg(image); + + // assert + IptcProfile actual = reloadedImage.Metadata.IptcProfile; + Assert.NotNull(actual); + var iptcValues = actual.Values.ToList(); + ContainsIptcValue(iptcValues, IptcTag.CaptionWriter, expectedCaptionWriter); + ContainsIptcValue(iptcValues, IptcTag.Caption, expectedCaption); + } + + [Fact] + public void IptcProfile_SetNewValue_RespectsMaxLength() + { + // arrange + var profile = new IptcProfile(); + } + + [Theory] + [InlineData(IptcTag.ObjectAttribute)] + [InlineData(IptcTag.SubjectReference)] + [InlineData(IptcTag.SupplementalCategories)] + [InlineData(IptcTag.Keywords)] + [InlineData(IptcTag.LocationCode)] + [InlineData(IptcTag.LocationName)] + [InlineData(IptcTag.ReferenceService)] + [InlineData(IptcTag.ReferenceDate)] + [InlineData(IptcTag.ReferenceNumber)] + [InlineData(IptcTag.Byline)] + [InlineData(IptcTag.BylineTitle)] + [InlineData(IptcTag.Contact)] + [InlineData(IptcTag.LocalCaption)] + [InlineData(IptcTag.CaptionWriter)] + public void IptcProfile_AddRepeatable_Works(IptcTag tag) + { + // arrange + var profile = new IptcProfile(); + var expectedValue1 = "test"; + var expectedValue2 = "another one"; + profile.SetValue(tag, expectedValue1, false); + + // act + profile.SetValue(tag, expectedValue2, false); + + // assert + var values = profile.Values.ToList(); + Assert.Equal(2, values.Count); + ContainsIptcValue(values, tag, expectedValue1); + ContainsIptcValue(values, tag, expectedValue2); + } + + [Theory] + [InlineData(IptcTag.RecordVersion)] + [InlineData(IptcTag.ObjectType)] + [InlineData(IptcTag.Name)] + [InlineData(IptcTag.EditStatus)] + [InlineData(IptcTag.EditorialUpdate)] + [InlineData(IptcTag.Urgency)] + [InlineData(IptcTag.Category)] + [InlineData(IptcTag.FixtureIdentifier)] + [InlineData(IptcTag.ReleaseDate)] + [InlineData(IptcTag.ReleaseTime)] + [InlineData(IptcTag.ExpirationDate)] + [InlineData(IptcTag.ExpirationTime)] + [InlineData(IptcTag.SpecialInstructions)] + [InlineData(IptcTag.ActionAdvised)] + [InlineData(IptcTag.CreatedDate)] + [InlineData(IptcTag.CreatedTime)] + [InlineData(IptcTag.DigitalCreationDate)] + [InlineData(IptcTag.DigitalCreationTime)] + [InlineData(IptcTag.OriginatingProgram)] + [InlineData(IptcTag.ProgramVersion)] + [InlineData(IptcTag.ObjectCycle)] + [InlineData(IptcTag.City)] + [InlineData(IptcTag.SubLocation)] + [InlineData(IptcTag.ProvinceState)] + [InlineData(IptcTag.CountryCode)] + [InlineData(IptcTag.Country)] + [InlineData(IptcTag.OriginalTransmissionReference)] + [InlineData(IptcTag.Headline)] + [InlineData(IptcTag.Credit)] + [InlineData(IptcTag.CopyrightNotice)] + [InlineData(IptcTag.Caption)] + [InlineData(IptcTag.ImageType)] + [InlineData(IptcTag.ImageOrientation)] + public void IptcProfile_AddNoneRepeatable_DoesOverrideOldValue(IptcTag tag) + { + // arrange + var profile = new IptcProfile(); + var expectedValue = "another one"; + profile.SetValue(tag, "test", false); + + // act + profile.SetValue(tag, expectedValue, false); + + // assert + var values = profile.Values.ToList(); + Assert.Equal(1, values.Count); + ContainsIptcValue(values, tag, expectedValue); + } + + [Fact] + public void IptcProfile_RemoveByTag_RemovesAllEntrys() + { + // arange + var profile = new IptcProfile(); + profile.SetValue(IptcTag.Byline, "test"); + profile.SetValue(IptcTag.Byline, "test2"); + + // act + var result = profile.RemoveValue(IptcTag.Byline); + + // assert + Assert.True(result, "removed result should be true"); + Assert.Empty(profile.Values); + } + + [Fact] + public void IptcProfile_RemoveByTagAndValue_Works() + { + // arange + var profile = new IptcProfile(); + profile.SetValue(IptcTag.Byline, "test"); + profile.SetValue(IptcTag.Byline, "test2"); + + // act + var result = profile.RemoveValue(IptcTag.Byline, "test2"); + + // assert + Assert.True(result, "removed result should be true"); + ContainsIptcValue(profile.Values.ToList(), IptcTag.Byline, "test"); + } + + [Fact] + public void IptcProfile_GetValue_RetrievesAllEntrys() + { + // arange + var profile = new IptcProfile(); + profile.SetValue(IptcTag.Byline, "test"); + profile.SetValue(IptcTag.Byline, "test2"); + profile.SetValue(IptcTag.Caption, "test"); + + // act + List result = profile.GetValues(IptcTag.Byline); + + // assert + Assert.NotNull(result); + Assert.Equal(2, result.Count); + } + + private static void ContainsIptcValue(List values, IptcTag tag, string value) + { + Assert.True(values.Any(val => val.Tag == tag), $"Missing iptc tag {tag}"); + Assert.True(values.Contains(new IptcValue(tag, System.Text.Encoding.UTF8.GetBytes(value), false)), $"expected iptc value '{value}' was not found for tag '{tag}'"); + } + + private static Image WriteAndReadJpeg(Image image) + { + using (var memStream = new MemoryStream()) + { + image.SaveAsJpeg(memStream); + image.Dispose(); + + memStream.Position = 0; + return Image.Load(memStream); + } + } + } +} diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs index 892568803..65d975257 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/tests/ImageSharp.Tests/TestImages.cs @@ -162,6 +162,8 @@ namespace SixLabors.ImageSharp.Tests public const string LowContrast = "Jpg/baseline/AsianCarvingLowContrast.jpg"; public const string Testorig12bit = "Jpg/baseline/testorig12.jpg"; public const string YcckSubsample1222 = "Jpg/baseline/ycck-subsample-1222.jpg"; + public const string Iptc = "Jpg/baseline/iptc.jpg"; + public const string App13WithEmptyIptc = "Jpg/baseline/iptc-psAPP13-wIPTCempty.jpg"; public static readonly string[] All = { diff --git a/tests/Images/Input/Jpg/baseline/iptc-psAPP13-wIPTCempty.jpg b/tests/Images/Input/Jpg/baseline/iptc-psAPP13-wIPTCempty.jpg new file mode 100644 index 000000000..d60013dcd Binary files /dev/null and b/tests/Images/Input/Jpg/baseline/iptc-psAPP13-wIPTCempty.jpg differ diff --git a/tests/Images/Input/Jpg/baseline/iptc.jpg b/tests/Images/Input/Jpg/baseline/iptc.jpg new file mode 100644 index 000000000..de30930d6 Binary files /dev/null and b/tests/Images/Input/Jpg/baseline/iptc.jpg differ