diff --git a/src/ImageSharp/Common/Exceptions/InvalidImageContentException.cs b/src/ImageSharp/Common/Exceptions/InvalidImageContentException.cs new file mode 100644 index 0000000000..7069e89823 --- /dev/null +++ b/src/ImageSharp/Common/Exceptions/InvalidImageContentException.cs @@ -0,0 +1,22 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +namespace SixLabors.ImageSharp +{ + /// + /// The exception that is thrown when the library tries to load + /// an image which contains invalid content. + /// + public sealed class InvalidImageContentException : ImageFormatException + { + /// + /// Initializes a new instance of the class with the name of the + /// parameter that causes this exception. + /// + /// The error message that explains the reason for this exception. + public InvalidImageContentException(string errorMessage) + : base(errorMessage) + { + } + } +} diff --git a/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs b/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs index dfdbb22fa5..eeb1ae714a 100644 --- a/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs +++ b/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs @@ -462,7 +462,7 @@ namespace SixLabors.ImageSharp.Formats.Bmp { if (this.stream.Read(cmd, 0, cmd.Length) != 2) { - BmpThrowHelper.ThrowImageFormatException("Failed to read 2 bytes from the stream while uncompressing RLE4 bitmap."); + BmpThrowHelper.ThrowInvalidImageContentException("Failed to read 2 bytes from the stream while uncompressing RLE4 bitmap."); } if (cmd[0] == RleCommand) @@ -569,7 +569,7 @@ namespace SixLabors.ImageSharp.Formats.Bmp { if (this.stream.Read(cmd, 0, cmd.Length) != 2) { - BmpThrowHelper.ThrowImageFormatException("Failed to read 2 bytes from stream while uncompressing RLE8 bitmap."); + BmpThrowHelper.ThrowInvalidImageContentException("Failed to read 2 bytes from stream while uncompressing RLE8 bitmap."); } if (cmd[0] == RleCommand) @@ -648,7 +648,7 @@ namespace SixLabors.ImageSharp.Formats.Bmp { if (this.stream.Read(cmd, 0, cmd.Length) != 2) { - BmpThrowHelper.ThrowImageFormatException("Failed to read 2 bytes from stream while uncompressing RLE24 bitmap."); + BmpThrowHelper.ThrowInvalidImageContentException("Failed to read 2 bytes from stream while uncompressing RLE24 bitmap."); } if (cmd[0] == RleCommand) @@ -1431,7 +1431,7 @@ namespace SixLabors.ImageSharp.Formats.Bmp // Make sure, that we will not read pass the bitmap offset (starting position of image data). if ((this.stream.Position + colorMapSizeBytes) > this.fileHeader.Offset) { - BmpThrowHelper.ThrowImageFormatException( + BmpThrowHelper.ThrowInvalidImageContentException( $"Reading the color map would read beyond the bitmap offset. Either the color map size of '{colorMapSizeBytes}' is invalid or the bitmap offset."); } @@ -1445,7 +1445,7 @@ namespace SixLabors.ImageSharp.Formats.Bmp int skipAmount = this.fileHeader.Offset - (int)this.stream.Position; if ((skipAmount + (int)this.stream.Position) > this.stream.Length) { - BmpThrowHelper.ThrowImageFormatException("Invalid fileheader offset found. Offset is greater than the stream length."); + BmpThrowHelper.ThrowInvalidImageContentException("Invalid fileheader offset found. Offset is greater than the stream length."); } if (skipAmount > 0) diff --git a/src/ImageSharp/Formats/Bmp/BmpInfoHeader.cs b/src/ImageSharp/Formats/Bmp/BmpInfoHeader.cs index 9ede660705..3411de0421 100644 --- a/src/ImageSharp/Formats/Bmp/BmpInfoHeader.cs +++ b/src/ImageSharp/Formats/Bmp/BmpInfoHeader.cs @@ -393,7 +393,7 @@ namespace SixLabors.ImageSharp.Formats.Bmp break; default: // Compression type 3 (1DHuffman) is not supported. - BmpThrowHelper.ThrowImageFormatException("Compression type is not supported. ImageSharp only supports uncompressed, RLE4, RLE8 and RLE24."); + BmpThrowHelper.ThrowInvalidImageContentException("Compression type is not supported. ImageSharp only supports uncompressed, RLE4, RLE8 and RLE24."); break; } diff --git a/src/ImageSharp/Formats/Bmp/BmpThrowHelper.cs b/src/ImageSharp/Formats/Bmp/BmpThrowHelper.cs index 443471404e..c48566f835 100644 --- a/src/ImageSharp/Formats/Bmp/BmpThrowHelper.cs +++ b/src/ImageSharp/Formats/Bmp/BmpThrowHelper.cs @@ -1,4 +1,4 @@ -// Copyright (c) Six Labors and contributors. +// Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. using System; @@ -9,23 +9,19 @@ namespace SixLabors.ImageSharp.Formats.Bmp internal static class BmpThrowHelper { /// - /// Cold path optimization for throwing -s + /// Cold path optimization for throwing 's /// /// The error message for the exception. [MethodImpl(MethodImplOptions.NoInlining)] - public static void ThrowImageFormatException(string errorMessage) - { - throw new ImageFormatException(errorMessage); - } + public static void ThrowInvalidImageContentException(string errorMessage) + => throw new InvalidImageContentException(errorMessage); /// - /// Cold path optimization for throwing -s + /// Cold path optimization for throwing 's /// /// The error message for the exception. [MethodImpl(MethodImplOptions.NoInlining)] public static void ThrowNotSupportedException(string errorMessage) - { - throw new NotSupportedException(errorMessage); - } + => throw new NotSupportedException(errorMessage); } } diff --git a/src/ImageSharp/Formats/Gif/GifDecoderCore.cs b/src/ImageSharp/Formats/Gif/GifDecoderCore.cs index 7919bc1489..de5aa78843 100644 --- a/src/ImageSharp/Formats/Gif/GifDecoderCore.cs +++ b/src/ImageSharp/Formats/Gif/GifDecoderCore.cs @@ -318,7 +318,7 @@ namespace SixLabors.ImageSharp.Formats.Gif { if (length > GifConstants.MaxCommentSubBlockLength) { - throw new ImageFormatException($"Gif comment length '{length}' exceeds max '{GifConstants.MaxCommentSubBlockLength}' of a comment data block"); + GifThrowHelper.ThrowInvalidImageContentException($"Gif comment length '{length}' exceeds max '{GifConstants.MaxCommentSubBlockLength}' of a comment data block"); } if (this.IgnoreMetadata) diff --git a/src/ImageSharp/Formats/Gif/GifThrowHelper.cs b/src/ImageSharp/Formats/Gif/GifThrowHelper.cs new file mode 100644 index 0000000000..1d81008a01 --- /dev/null +++ b/src/ImageSharp/Formats/Gif/GifThrowHelper.cs @@ -0,0 +1,18 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System.Runtime.CompilerServices; + +namespace SixLabors.ImageSharp.Formats.Gif +{ + internal static class GifThrowHelper + { + /// + /// Cold path optimization for throwing 's + /// + /// The error message for the exception. + [MethodImpl(MethodImplOptions.NoInlining)] + public static void ThrowInvalidImageContentException(string errorMessage) + => throw new InvalidImageContentException(errorMessage); + } +} diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/ColorConverters/JpegColorConverter.FromYCbCrSimd.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/ColorConverters/JpegColorConverter.FromYCbCrSimd.cs index 09d6a4d1d8..002f79f84c 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Decoder/ColorConverters/JpegColorConverter.FromYCbCrSimd.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/ColorConverters/JpegColorConverter.FromYCbCrSimd.cs @@ -1,4 +1,4 @@ -// Copyright (c) Six Labors and contributors. +// Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. using System; @@ -104,7 +104,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder.ColorConverters { // TODO: Run fallback scalar code here // However, no issues expected before someone implements this: https://github.com/dotnet/coreclr/issues/12007 - throw new NotImplementedException("Your CPU architecture is too modern!"); + JpegThrowHelper.ThrowNotImplementedException("Your CPU architecture is too modern!"); } // Collect (r0,r1...r8) (g0,g1...g8) (b0,b1...b8) vector values in the expected (r0,g0,g1,1), (r1,g1,g2,1) ... order: diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/JFifMarker.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/JFifMarker.cs index 7497c8a409..44878bd6c7 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Decoder/JFifMarker.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/JFifMarker.cs @@ -29,12 +29,12 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder { if (xDensity <= 0) { - JpegThrowHelper.ThrowImageFormatException($"X-Density {xDensity} must be greater than 0."); + JpegThrowHelper.ThrowInvalidImageContentException($"X-Density {xDensity} must be greater than 0."); } if (yDensity <= 0) { - JpegThrowHelper.ThrowImageFormatException($"Y-Density {yDensity} must be greater than 0."); + JpegThrowHelper.ThrowInvalidImageContentException($"Y-Density {yDensity} must be greater than 0."); } this.MajorVersion = majorVersion; diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/ProfileResolver.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/ProfileResolver.cs index 87b486ea65..325d7780ae 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 951fec1d4c..654610659d 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); @@ -246,7 +259,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg var fileMarker = new JpegFileMarker(this.markerBuffer[1], 0); if (fileMarker.Marker != JpegConstants.Markers.SOI) { - JpegThrowHelper.ThrowImageFormatException("Missing SOI marker."); + JpegThrowHelper.ThrowInvalidImageContentException("Missing SOI marker."); } this.InputStream.Read(this.markerBuffer, 0, 2); @@ -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; @@ -407,7 +423,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg : JpegColorSpace.Cmyk; } - JpegThrowHelper.ThrowImageFormatException($"Unsupported color mode. Supported component counts 1, 3, and 4; found {this.ComponentCount}"); + JpegThrowHelper.ThrowInvalidImageContentException($"Unsupported color mode. Supported component counts 1, 3, and 4; found {this.ComponentCount}"); return default; } @@ -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. @@ -704,7 +821,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg { if (this.Frame != null) { - JpegThrowHelper.ThrowImageFormatException("Multiple SOF markers. Only single frame jpegs supported."); + JpegThrowHelper.ThrowInvalidImageContentException("Multiple SOF markers. Only single frame jpegs supported."); } // Read initial marker definitions. @@ -714,7 +831,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg // We only support 8-bit and 12-bit precision. if (Array.IndexOf(this.supportedPrecisions, this.temp[0]) == -1) { - JpegThrowHelper.ThrowImageFormatException("Only 8-Bit and 12-Bit precision supported."); + JpegThrowHelper.ThrowInvalidImageContentException("Only 8-Bit and 12-Bit precision supported."); } this.Precision = this.temp[0]; @@ -811,13 +928,13 @@ namespace SixLabors.ImageSharp.Formats.Jpeg // Types 0..1 DC..AC if (tableType > 1) { - JpegThrowHelper.ThrowImageFormatException("Bad Huffman Table type."); + JpegThrowHelper.ThrowInvalidImageContentException("Bad Huffman Table type."); } // Max tables of each type if (tableIndex > 3) { - JpegThrowHelper.ThrowImageFormatException("Bad Huffman Table index."); + JpegThrowHelper.ThrowInvalidImageContentException("Bad Huffman Table index."); } this.InputStream.Read(huffmanData.Array, 0, 16); @@ -836,7 +953,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg if (codeLengthSum > 256 || codeLengthSum > length) { - JpegThrowHelper.ThrowImageFormatException("Huffman table has excessive length."); + JpegThrowHelper.ThrowInvalidImageContentException("Huffman table has excessive length."); } using (IManagedByteBuffer huffmanValues = this.configuration.MemoryAllocator.AllocateManagedByteBuffer(256, AllocationOptions.Clean)) @@ -878,7 +995,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg { if (this.Frame is null) { - JpegThrowHelper.ThrowImageFormatException("No readable SOFn (Start Of Frame) marker found."); + JpegThrowHelper.ThrowInvalidImageContentException("No readable SOFn (Start Of Frame) marker found."); } int selectorsCount = this.InputStream.ReadByte(); @@ -899,7 +1016,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg if (componentIndex < 0) { - JpegThrowHelper.ThrowImageFormatException($"Unknown component selector {componentIndex}."); + JpegThrowHelper.ThrowInvalidImageContentException($"Unknown component selector {componentIndex}."); } ref JpegComponent component = ref this.Frame.Components[componentIndex]; diff --git a/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs b/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs index 32f4d22878..eed95c6b07 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/Jpeg/JpegThrowHelper.cs b/src/ImageSharp/Formats/Jpeg/JpegThrowHelper.cs index 7e8384dcff..dd44cb2d19 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegThrowHelper.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegThrowHelper.cs @@ -1,6 +1,7 @@ -// Copyright (c) Six Labors and contributors. +// Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. +using System; using System.Runtime.CompilerServices; namespace SixLabors.ImageSharp.Formats.Jpeg @@ -8,25 +9,33 @@ namespace SixLabors.ImageSharp.Formats.Jpeg internal static class JpegThrowHelper { /// - /// Cold path optimization for throwing 's. + /// Cold path optimization for throwing 's. /// /// The error message for the exception. [MethodImpl(InliningOptions.ColdPath)] - public static void ThrowImageFormatException(string errorMessage) => throw new ImageFormatException(errorMessage); + public static void ThrowInvalidImageContentException(string errorMessage) => throw new InvalidImageContentException(errorMessage); + + /// + /// Cold path optimization for throwing 's + /// + /// The error message for the exception. + [MethodImpl(InliningOptions.ColdPath)] + public static void ThrowNotImplementedException(string errorMessage) + => throw new NotImplementedException(errorMessage); [MethodImpl(InliningOptions.ColdPath)] - public static void ThrowBadMarker(string marker, int length) => throw new ImageFormatException($"Marker {marker} has bad length {length}."); + public static void ThrowBadMarker(string marker, int length) => throw new InvalidImageContentException($"Marker {marker} has bad length {length}."); [MethodImpl(InliningOptions.ColdPath)] - public static void ThrowBadQuantizationTable() => throw new ImageFormatException("Bad Quantization Table index."); + public static void ThrowBadQuantizationTable() => throw new InvalidImageContentException("Bad Quantization Table index."); [MethodImpl(InliningOptions.ColdPath)] - public static void ThrowBadSampling() => throw new ImageFormatException("Bad sampling factor."); + public static void ThrowBadSampling() => throw new InvalidImageContentException("Bad sampling factor."); [MethodImpl(InliningOptions.ColdPath)] - public static void ThrowBadProgressiveScan(int ss, int se, int ah, int al) => throw new ImageFormatException($"Invalid progressive parameters Ss={ss} Se={se} Ah={ah} Al={al}."); + public static void ThrowBadProgressiveScan(int ss, int se, int ah, int al) => throw new InvalidImageContentException($"Invalid progressive parameters Ss={ss} Se={se} Ah={ah} Al={al}."); [MethodImpl(InliningOptions.ColdPath)] - public static void ThrowInvalidImageDimensions(int width, int height) => throw new ImageFormatException($"Invalid image dimensions: {width}x{height}."); + public static void ThrowInvalidImageDimensions(int width, int height) => throw new InvalidImageContentException($"Invalid image dimensions: {width}x{height}."); } -} \ No newline at end of file +} diff --git a/src/ImageSharp/Formats/Png/PngDecoderCore.cs b/src/ImageSharp/Formats/Png/PngDecoderCore.cs index bc7b9d8150..272f93d108 100644 --- a/src/ImageSharp/Formats/Png/PngDecoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngDecoderCore.cs @@ -1141,24 +1141,22 @@ namespace SixLabors.ImageSharp.Formats.Png /// The . private void ValidateChunk(in PngChunk chunk) { - if (!chunk.IsCritical) - { - return; - } - - Span chunkType = stackalloc byte[4]; + uint crc = this.ReadChunkCrc(); - BinaryPrimitives.WriteUInt32BigEndian(chunkType, (uint)chunk.Type); + if (chunk.IsCritical) + { + Span chunkType = stackalloc byte[4]; + BinaryPrimitives.WriteUInt32BigEndian(chunkType, (uint)chunk.Type); - this.crc.Reset(); - this.crc.Update(chunkType); - this.crc.Update(chunk.Data.GetSpan()); + this.crc.Reset(); + this.crc.Update(chunkType); + this.crc.Update(chunk.Data.GetSpan()); - uint crc = this.ReadChunkCrc(); - if (this.crc.Value != crc) - { - string chunkTypeName = Encoding.ASCII.GetString(chunkType); - PngThrowHelper.ThrowInvalidChunkCrc(chunkTypeName); + if (this.crc.Value != crc) + { + string chunkTypeName = Encoding.ASCII.GetString(chunkType); + PngThrowHelper.ThrowInvalidChunkCrc(chunkTypeName); + } } } diff --git a/src/ImageSharp/Formats/Png/PngThrowHelper.cs b/src/ImageSharp/Formats/Png/PngThrowHelper.cs index dd3a054641..dcb643d036 100644 --- a/src/ImageSharp/Formats/Png/PngThrowHelper.cs +++ b/src/ImageSharp/Formats/Png/PngThrowHelper.cs @@ -12,21 +12,21 @@ namespace SixLabors.ImageSharp.Formats.Png internal static class PngThrowHelper { [MethodImpl(InliningOptions.ColdPath)] - public static void ThrowNoHeader() => throw new ImageFormatException("PNG Image does not contain a header chunk"); + public static void ThrowNoHeader() => throw new InvalidImageContentException("PNG Image does not contain a header chunk"); [MethodImpl(InliningOptions.ColdPath)] - public static void ThrowNoData() => throw new ImageFormatException("PNG Image does not contain a data chunk"); + public static void ThrowNoData() => throw new InvalidImageContentException("PNG Image does not contain a data chunk"); [MethodImpl(InliningOptions.ColdPath)] - public static void ThrowInvalidChunkType() => throw new ImageFormatException("Invalid PNG data."); + public static void ThrowInvalidChunkType() => throw new InvalidImageContentException("Invalid PNG data."); [MethodImpl(InliningOptions.ColdPath)] - public static void ThrowInvalidChunkCrc(string chunkTypeName) => throw new ImageFormatException($"CRC Error. PNG {chunkTypeName} chunk is corrupt!"); + public static void ThrowInvalidChunkCrc(string chunkTypeName) => throw new InvalidImageContentException($"CRC Error. PNG {chunkTypeName} chunk is corrupt!"); [MethodImpl(InliningOptions.ColdPath)] public static void ThrowNotSupportedColor() => new NotSupportedException("Unsupported PNG color type"); [MethodImpl(InliningOptions.ColdPath)] - public static void ThrowUnknownFilter() => throw new ImageFormatException("Unknown filter type."); + public static void ThrowUnknownFilter() => throw new InvalidImageContentException("Unknown filter type."); } } diff --git a/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs b/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs index 816a472fb2..057ec1bfc7 100644 --- a/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs +++ b/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs @@ -114,12 +114,12 @@ namespace SixLabors.ImageSharp.Formats.Tga { if (this.fileHeader.CMapLength <= 0) { - TgaThrowHelper.ThrowImageFormatException("Missing tga color map length"); + TgaThrowHelper.ThrowInvalidImageContentException("Missing tga color map length"); } if (this.fileHeader.CMapDepth <= 0) { - TgaThrowHelper.ThrowImageFormatException("Missing tga color map depth"); + TgaThrowHelper.ThrowInvalidImageContentException("Missing tga color map depth"); } int colorMapPixelSizeInBytes = this.fileHeader.CMapDepth / 8; @@ -898,7 +898,7 @@ namespace SixLabors.ImageSharp.Formats.Tga var alphaBits = this.fileHeader.ImageDescriptor & 0xf; if (alphaBits != 0 && alphaBits != 1 && alphaBits != 8) { - TgaThrowHelper.ThrowImageFormatException("Invalid alpha channel bits"); + TgaThrowHelper.ThrowInvalidImageContentException("Invalid alpha channel bits"); } this.tgaMetadata.AlphaChannelBits = (byte)alphaBits; diff --git a/src/ImageSharp/Formats/Tga/TgaThrowHelper.cs b/src/ImageSharp/Formats/Tga/TgaThrowHelper.cs index 845d009227..1714a2025e 100644 --- a/src/ImageSharp/Formats/Tga/TgaThrowHelper.cs +++ b/src/ImageSharp/Formats/Tga/TgaThrowHelper.cs @@ -9,23 +9,19 @@ namespace SixLabors.ImageSharp.Formats.Tga internal static class TgaThrowHelper { /// - /// Cold path optimization for throwing -s + /// Cold path optimization for throwing 's /// /// The error message for the exception. [MethodImpl(MethodImplOptions.NoInlining)] - public static void ThrowImageFormatException(string errorMessage) - { - throw new ImageFormatException(errorMessage); - } + public static void ThrowInvalidImageContentException(string errorMessage) + => throw new InvalidImageContentException(errorMessage); /// - /// Cold path optimization for throwing -s + /// Cold path optimization for throwing 's /// /// The error message for the exception. [MethodImpl(MethodImplOptions.NoInlining)] public static void ThrowNotSupportedException(string errorMessage) - { - throw new NotSupportedException(errorMessage); - } + => throw new NotSupportedException(errorMessage); } } diff --git a/src/ImageSharp/Image.FromBytes.cs b/src/ImageSharp/Image.FromBytes.cs index 06b05fe3c4..a0e8097d8e 100644 --- a/src/ImageSharp/Image.FromBytes.cs +++ b/src/ImageSharp/Image.FromBytes.cs @@ -17,21 +17,23 @@ namespace SixLabors.ImageSharp /// By reading the header on the provided byte array this calculates the images format. /// /// The byte array containing encoded image data to read the header from. + /// The data is null. /// The format or null if none found. public static IImageFormat DetectFormat(byte[] data) - { - return DetectFormat(Configuration.Default, data); - } + => DetectFormat(Configuration.Default, data); /// /// By reading the header on the provided byte array this calculates the images format. /// /// The configuration. /// The byte array containing encoded image data to read the header from. + /// The configuration is null. + /// The data is null. /// The mime type or null if none found. public static IImageFormat DetectFormat(Configuration configuration, byte[] data) { - Guard.NotNull(configuration, nameof(configuration)); + Guard.NotNull(data, nameof(data)); + using (var stream = new MemoryStream(data, 0, data.Length, false, true)) { return DetectFormat(configuration, stream); @@ -42,7 +44,8 @@ namespace SixLabors.ImageSharp /// Reads the raw image information from the specified stream without fully decoding it. /// /// The byte array containing encoded image data to read the header from. - /// Thrown if the stream is not readable. + /// The data is null. + /// The data is not readable. /// /// The or null if suitable info detector not found. /// @@ -53,7 +56,8 @@ namespace SixLabors.ImageSharp /// /// The byte array containing encoded image data to read the header from. /// The format type of the decoded image. - /// Thrown if the stream is not readable. + /// The data is null. + /// The data is not readable. /// /// The or null if suitable info detector not found. /// @@ -65,13 +69,16 @@ namespace SixLabors.ImageSharp /// The configuration. /// The byte array containing encoded image data to read the header from. /// The format type of the decoded image. - /// Thrown if the stream is not readable. + /// The configuration is null. + /// The data is null. + /// The data is not readable. /// /// The or null if suitable info detector is not found. /// public static IImageInfo Identify(Configuration configuration, byte[] data, out IImageFormat format) { - Guard.NotNull(configuration, nameof(configuration)); + Guard.NotNull(data, nameof(data)); + using (var stream = new MemoryStream(data, 0, data.Length, false, true)) { return Identify(configuration, stream, out format); @@ -82,14 +89,20 @@ namespace SixLabors.ImageSharp /// Load a new instance of from the given encoded byte array. /// /// The byte array containing image data. + /// The configuration is null. + /// The data is null. /// A new . - public static Image Load(byte[] data) => Load(Configuration.Default, data); + public static Image Load(byte[] data) + => Load(Configuration.Default, data); /// /// Load a new instance of from the given encoded byte array. /// /// The byte array containing encoded image data. /// The pixel format. + /// The data is null. + /// Image format not recognised. + /// Image contains invalid content. /// A new . public static Image Load(byte[] data) where TPixel : unmanaged, IPixel @@ -101,6 +114,9 @@ namespace SixLabors.ImageSharp /// The byte array containing image data. /// The mime type of the decoded image. /// The pixel format. + /// The data is null. + /// Image format not recognised. + /// Image contains invalid content. /// A new . public static Image Load(byte[] data, out IImageFormat format) where TPixel : unmanaged, IPixel @@ -112,10 +128,16 @@ namespace SixLabors.ImageSharp /// The configuration options. /// The byte array containing encoded image data. /// The pixel format. + /// The configuration is null. + /// The data is null. + /// Image format not recognised. + /// Image contains invalid content. /// A new . public static Image Load(Configuration configuration, byte[] data) where TPixel : unmanaged, IPixel { + Guard.NotNull(data, nameof(data)); + using (var stream = new MemoryStream(data, 0, data.Length, false, true)) { return Load(configuration, stream); @@ -129,10 +151,16 @@ namespace SixLabors.ImageSharp /// The byte array containing encoded image data. /// The of the decoded image. /// The pixel format. + /// The configuration is null. + /// The data is null. + /// Image format not recognised. + /// Image contains invalid content. /// A new . public static Image Load(Configuration configuration, byte[] data, out IImageFormat format) where TPixel : unmanaged, IPixel { + Guard.NotNull(data, nameof(data)); + using (var stream = new MemoryStream(data, 0, data.Length, false, true)) { return Load(configuration, stream, out format); @@ -145,10 +173,15 @@ namespace SixLabors.ImageSharp /// The byte array containing encoded image data. /// The decoder. /// The pixel format. + /// The data is null. + /// Image format not recognised. + /// Image contains invalid content. /// A new . public static Image Load(byte[] data, IImageDecoder decoder) where TPixel : unmanaged, IPixel { + Guard.NotNull(data, nameof(data)); + using (var stream = new MemoryStream(data, 0, data.Length, false, true)) { return Load(stream, decoder); @@ -162,10 +195,16 @@ namespace SixLabors.ImageSharp /// The byte array containing encoded image data. /// The decoder. /// The pixel format. + /// The configuration is null. + /// The data is null. + /// Image format not recognised. + /// Image contains invalid content. /// A new . public static Image Load(Configuration configuration, byte[] data, IImageDecoder decoder) where TPixel : unmanaged, IPixel { + Guard.NotNull(data, nameof(data)); + using (var stream = new MemoryStream(data, 0, data.Length, false, true)) { return Load(configuration, stream, decoder); @@ -173,9 +212,9 @@ namespace SixLabors.ImageSharp } /// - /// By reading the header on the provided byte array this calculates the images format. + /// By reading the header on the provided byte span this calculates the images format. /// - /// The byte array containing encoded image data to read the header from. + /// The byte span containing encoded image data to read the header from. /// The format or null if none found. public static IImageFormat DetectFormat(ReadOnlySpan data) { @@ -183,13 +222,16 @@ namespace SixLabors.ImageSharp } /// - /// By reading the header on the provided byte array this calculates the images format. + /// By reading the header on the provided byte span this calculates the images format. /// /// The configuration. - /// The byte array containing encoded image data to read the header from. + /// The byte span containing encoded image data to read the header from. + /// The configuration is null. /// The mime type or null if none found. public static IImageFormat DetectFormat(Configuration configuration, ReadOnlySpan data) { + Guard.NotNull(configuration, nameof(configuration)); + int maxHeaderSize = configuration.MaxHeaderSize; if (maxHeaderSize <= 0) { @@ -214,28 +256,34 @@ namespace SixLabors.ImageSharp /// /// The byte span containing encoded image data. /// The pixel format. + /// Image format not recognised. + /// Image contains invalid content. /// A new . public static Image Load(ReadOnlySpan data) where TPixel : unmanaged, IPixel => Load(Configuration.Default, data); /// - /// Load a new instance of from the given encoded byte array. + /// Load a new instance of from the given encoded byte span. /// /// The byte span containing image data. /// The mime type of the decoded image. /// The pixel format. + /// Image format not recognised. + /// Image contains invalid content. /// A new . public static Image Load(ReadOnlySpan data, out IImageFormat format) where TPixel : unmanaged, IPixel => Load(Configuration.Default, data, out format); /// - /// Load a new instance of from the given encoded byte array. + /// Load a new instance of from the given encoded byte span. /// /// The byte span containing encoded image data. /// The decoder. /// The pixel format. + /// Image format not recognised. + /// Image contains invalid content. /// A new . public static Image Load(ReadOnlySpan data, IImageDecoder decoder) where TPixel : unmanaged, IPixel @@ -247,6 +295,9 @@ namespace SixLabors.ImageSharp /// The configuration options. /// The byte span containing encoded image data. /// The pixel format. + /// The configuration is null. + /// Image format not recognised. + /// Image contains invalid content. /// A new . public static unsafe Image Load(Configuration configuration, ReadOnlySpan data) where TPixel : unmanaged, IPixel @@ -267,6 +318,9 @@ namespace SixLabors.ImageSharp /// The byte span containing image data. /// The decoder. /// The pixel format. + /// The configuration is null. + /// Image format not recognised. + /// Image contains invalid content. /// A new . public static unsafe Image Load( Configuration configuration, @@ -290,6 +344,9 @@ namespace SixLabors.ImageSharp /// The byte span containing image data. /// The of the decoded image. /// The pixel format. + /// The configuration is null. + /// Image format not recognised. + /// Image contains invalid content. /// A new . public static unsafe Image Load( Configuration configuration, @@ -311,25 +368,38 @@ namespace SixLabors.ImageSharp /// /// The byte array containing image data. /// The detected format. + /// The configuration is null. + /// The data is null. + /// Image format not recognised. + /// Image contains invalid content. /// The . - public static Image Load(byte[] data, out IImageFormat format) => - Load(Configuration.Default, data, out format); + public static Image Load(byte[] data, out IImageFormat format) + => Load(Configuration.Default, data, out format); /// /// Load a new instance of from the given encoded byte array. /// /// The byte array containing encoded image data. /// The decoder. + /// The data is null. + /// Image format not recognised. + /// Image contains invalid content. /// The . - public static Image Load(byte[] data, IImageDecoder decoder) => Load(Configuration.Default, data, decoder); + public static Image Load(byte[] data, IImageDecoder decoder) + => Load(Configuration.Default, data, decoder); /// /// Load a new instance of from the given encoded byte array. /// /// The configuration for the decoder. /// The byte array containing encoded image data. + /// The configuration is null. + /// The data is null. + /// Image format not recognised. + /// Image contains invalid content. /// The . - public static Image Load(Configuration configuration, byte[] data) => Load(configuration, data, out _); + public static Image Load(Configuration configuration, byte[] data) + => Load(configuration, data, out _); /// /// Load a new instance of from the given encoded byte array. @@ -337,6 +407,10 @@ namespace SixLabors.ImageSharp /// The configuration for the decoder. /// The byte array containing image data. /// The decoder. + /// The configuration is null. + /// The data is null. + /// Image format not recognised. + /// Image contains invalid content. /// The . public static Image Load(Configuration configuration, byte[] data, IImageDecoder decoder) { @@ -352,6 +426,10 @@ namespace SixLabors.ImageSharp /// The configuration for the decoder. /// The byte array containing image data. /// The mime type of the decoded image. + /// The configuration is null. + /// The data is null. + /// Image format not recognised. + /// Image contains invalid content. /// The . public static Image Load(Configuration configuration, byte[] data, out IImageFormat format) { @@ -365,26 +443,36 @@ namespace SixLabors.ImageSharp /// Load a new instance of from the given encoded byte span. /// /// The byte span containing image data. + /// Image format not recognised. + /// Image contains invalid content. /// The . - public static Image Load(ReadOnlySpan data) => Load(Configuration.Default, data); + public static Image Load(ReadOnlySpan data) + => Load(Configuration.Default, data); /// /// Load a new instance of from the given encoded byte span. /// /// The byte span containing image data. /// The decoder. + /// The data is null. + /// The decoder is null. + /// Image format not recognised. + /// Image contains invalid content. /// The . - public static Image Load(ReadOnlySpan data, IImageDecoder decoder) => - Load(Configuration.Default, data, decoder); + public static Image Load(ReadOnlySpan data, IImageDecoder decoder) + => Load(Configuration.Default, data, decoder); /// /// Load a new instance of from the given encoded byte array. /// /// The byte span containing image data. /// The detected format. + /// The decoder is null. + /// Image format not recognised. + /// Image contains invalid content. /// The . - public static Image Load(ReadOnlySpan data, out IImageFormat format) => - Load(Configuration.Default, data, out format); + public static Image Load(ReadOnlySpan data, out IImageFormat format) + => Load(Configuration.Default, data, out format); /// /// Decodes a new instance of from the given encoded byte span. @@ -392,7 +480,8 @@ namespace SixLabors.ImageSharp /// The configuration options. /// The byte span containing image data. /// The . - public static Image Load(Configuration configuration, ReadOnlySpan data) => Load(configuration, data, out _); + public static Image Load(Configuration configuration, ReadOnlySpan data) + => Load(configuration, data, out _); /// /// Load a new instance of from the given encoded byte span. @@ -400,6 +489,11 @@ namespace SixLabors.ImageSharp /// The Configuration. /// The byte span containing image data. /// The decoder. + /// The configuration is null. + /// The decoder is null. + /// The stream is not readable. + /// Image format not recognised. + /// Image contains invalid content. /// The . public static unsafe Image Load( Configuration configuration, @@ -421,6 +515,9 @@ namespace SixLabors.ImageSharp /// The configuration options. /// The byte span containing image data. /// The of the decoded image.> + /// The configuration is null. + /// Image format not recognised. + /// Image contains invalid content. /// The . public static unsafe Image Load( Configuration configuration, diff --git a/src/ImageSharp/Image.FromFile.cs b/src/ImageSharp/Image.FromFile.cs index 1a9fa15462..8546dd2700 100644 --- a/src/ImageSharp/Image.FromFile.cs +++ b/src/ImageSharp/Image.FromFile.cs @@ -19,19 +19,19 @@ namespace SixLabors.ImageSharp /// The image file to open and to read the header from. /// The mime type or null if none found. public static IImageFormat DetectFormat(string filePath) - { - return DetectFormat(Configuration.Default, filePath); - } + => DetectFormat(Configuration.Default, filePath); /// /// By reading the header on the provided file this calculates the images mime type. /// /// The configuration. /// The image file to open and to read the header from. + /// The configuration is null. /// The mime type or null if none found. public static IImageFormat DetectFormat(Configuration configuration, string filePath) { Guard.NotNull(configuration, nameof(configuration)); + using (Stream file = configuration.FileSystem.OpenRead(filePath)) { return DetectFormat(configuration, file); @@ -42,22 +42,22 @@ namespace SixLabors.ImageSharp /// Reads the raw image information from the specified stream without fully decoding it. /// /// The image file to open and to read the header from. - /// Thrown if the stream is not readable. /// /// The or null if suitable info detector not found. /// - public static IImageInfo Identify(string filePath) => Identify(filePath, out IImageFormat _); + public static IImageInfo Identify(string filePath) + => Identify(filePath, out IImageFormat _); /// /// Reads the raw image information from the specified stream without fully decoding it. /// /// The image file to open and to read the header from. /// The format type of the decoded image. - /// Thrown if the stream is not readable. /// /// The or null if suitable info detector not found. /// - public static IImageInfo Identify(string filePath, out IImageFormat format) => Identify(Configuration.Default, filePath, out format); + public static IImageInfo Identify(string filePath, out IImageFormat format) + => Identify(Configuration.Default, filePath, out format); /// /// Reads the raw image information from the specified stream without fully decoding it. @@ -65,7 +65,7 @@ namespace SixLabors.ImageSharp /// The configuration. /// The image file to open and to read the header from. /// The format type of the decoded image. - /// Thrown if the stream is not readable. + /// The configuration is null. /// /// The or null if suitable info detector is not found. /// @@ -86,7 +86,8 @@ namespace SixLabors.ImageSharp /// Thrown if the stream is not readable nor seekable. /// /// The . - public static Image Load(string path) => Load(Configuration.Default, path); + public static Image Load(string path) + => Load(Configuration.Default, path); /// /// Create a new instance of the class from the given file. @@ -97,18 +98,21 @@ namespace SixLabors.ImageSharp /// Thrown if the stream is not readable nor seekable. /// /// A new . - public static Image Load(string path, out IImageFormat format) => Load(Configuration.Default, path, out format); + public static Image Load(string path, out IImageFormat format) + => Load(Configuration.Default, path, out format); /// /// Create a new instance of the class from the given file. /// /// The configuration for the decoder. /// The file path to the image. - /// - /// Thrown if the stream is not readable nor seekable. - /// + /// The configuration is null. + /// The path is null. + /// Image format not recognised. + /// Image contains invalid content. /// The . - public static Image Load(Configuration configuration, string path) => Load(configuration, path, out _); + public static Image Load(Configuration configuration, string path) + => Load(configuration, path, out _); /// /// Create a new instance of the class from the given file. @@ -116,13 +120,17 @@ namespace SixLabors.ImageSharp /// The Configuration. /// The file path to the image. /// The decoder. - /// - /// Thrown if the stream is not readable nor seekable. - /// + /// The configuration is null. + /// The path is null. + /// The decoder is null. + /// Image format not recognised. + /// Image contains invalid content. /// The . public static Image Load(Configuration configuration, string path, IImageDecoder decoder) { Guard.NotNull(configuration, nameof(configuration)); + Guard.NotNull(path, nameof(path)); + using (Stream stream = configuration.FileSystem.OpenRead(path)) { return Load(configuration, stream, decoder); @@ -134,57 +142,58 @@ namespace SixLabors.ImageSharp /// /// The file path to the image. /// The decoder. - /// - /// Thrown if the stream is not readable nor seekable. - /// + /// The path is null. + /// The decoder is null. + /// Image format not recognised. + /// Image contains invalid content. /// The . - public static Image Load(string path, IImageDecoder decoder) => Load(Configuration.Default, path, decoder); + public static Image Load(string path, IImageDecoder decoder) + => Load(Configuration.Default, path, decoder); /// /// Create a new instance of the class from the given file. /// /// The file path to the image. - /// - /// Thrown if the stream is not readable nor seekable. - /// + /// The path is null. + /// Image format not recognised. + /// Image contains invalid content. /// The pixel format. /// A new . public static Image Load(string path) where TPixel : unmanaged, IPixel - { - return Load(Configuration.Default, path); - } + => Load(Configuration.Default, path); /// /// Create a new instance of the class from the given file. /// /// The file path to the image. /// The mime type of the decoded image. - /// - /// Thrown if the stream is not readable nor seekable. - /// + /// The path is null. + /// Image format not recognised. + /// Image contains invalid content. /// The pixel format. /// A new . public static Image Load(string path, out IImageFormat format) where TPixel : unmanaged, IPixel - { - return Load(Configuration.Default, path, out format); - } + => Load(Configuration.Default, path, out format); /// /// Create a new instance of the class from the given file. /// /// The configuration options. /// The file path to the image. - /// - /// Thrown if the stream is not readable nor seekable. - /// + /// The configuration is null. + /// The path is null. + /// Image format not recognised. + /// Image contains invalid content. /// The pixel format. /// A new . public static Image Load(Configuration configuration, string path) where TPixel : unmanaged, IPixel { Guard.NotNull(configuration, nameof(configuration)); + Guard.NotNull(path, nameof(path)); + using (Stream stream = configuration.FileSystem.OpenRead(path)) { return Load(configuration, stream); @@ -197,15 +206,18 @@ namespace SixLabors.ImageSharp /// The configuration options. /// The file path to the image. /// The mime type of the decoded image. - /// - /// Thrown if the stream is not readable nor seekable. - /// + /// The configuration is null. + /// The path is null. + /// Image format not recognised. + /// Image contains invalid content. /// The pixel format. /// A new . public static Image Load(Configuration configuration, string path, out IImageFormat format) where TPixel : unmanaged, IPixel { Guard.NotNull(configuration, nameof(configuration)); + Guard.NotNull(path, nameof(path)); + using (Stream stream = configuration.FileSystem.OpenRead(path)) { return Load(configuration, stream, out format); @@ -219,13 +231,16 @@ namespace SixLabors.ImageSharp /// The configuration options. /// The file path to the image. /// The mime type of the decoded image. - /// - /// Thrown if the stream is not readable nor seekable. - /// + /// The configuration is null. + /// The path is null. + /// Image format not recognised. + /// Image contains invalid content. /// A new . public static Image Load(Configuration configuration, string path, out IImageFormat format) { Guard.NotNull(configuration, nameof(configuration)); + Guard.NotNull(path, nameof(path)); + using (Stream stream = configuration.FileSystem.OpenRead(path)) { return Load(configuration, stream, out format); @@ -237,16 +252,14 @@ namespace SixLabors.ImageSharp /// /// The file path to the image. /// The decoder. - /// - /// Thrown if the stream is not readable nor seekable. - /// + /// The path is null. + /// Image format not recognised. + /// Image contains invalid content. /// The pixel format. /// A new . public static Image Load(string path, IImageDecoder decoder) where TPixel : unmanaged, IPixel - { - return Load(Configuration.Default, path, decoder); - } + => Load(Configuration.Default, path, decoder); /// /// Create a new instance of the class from the given file. @@ -254,15 +267,19 @@ namespace SixLabors.ImageSharp /// The Configuration. /// The file path to the image. /// The decoder. - /// - /// Thrown if the stream is not readable nor seekable. - /// + /// The configuration is null. + /// The path is null. + /// The decoder is null. + /// Image format not recognised. + /// Image contains invalid content. /// The pixel format. /// A new . public static Image Load(Configuration configuration, string path, IImageDecoder decoder) where TPixel : unmanaged, IPixel { Guard.NotNull(configuration, nameof(configuration)); + Guard.NotNull(path, nameof(path)); + using (Stream stream = configuration.FileSystem.OpenRead(path)) { return Load(configuration, stream, decoder); diff --git a/src/ImageSharp/Image.FromStream.cs b/src/ImageSharp/Image.FromStream.cs index 52d71409bb..332ca471e9 100644 --- a/src/ImageSharp/Image.FromStream.cs +++ b/src/ImageSharp/Image.FromStream.cs @@ -19,16 +19,20 @@ namespace SixLabors.ImageSharp /// By reading the header on the provided stream this calculates the images format type. /// /// The image stream to read the header from. - /// Thrown if the stream is not readable. + /// The stream is null. + /// The stream is not readable. /// The format type or null if none found. - public static IImageFormat DetectFormat(Stream stream) => DetectFormat(Configuration.Default, stream); + public static IImageFormat DetectFormat(Stream stream) + => DetectFormat(Configuration.Default, stream); /// /// By reading the header on the provided stream this calculates the images format type. /// /// The configuration. /// The image stream to read the header from. - /// Thrown if the stream is not readable. + /// The configuration is null. + /// The stream is null. + /// The stream is not readable. /// The format type or null if none found. public static IImageFormat DetectFormat(Configuration configuration, Stream stream) => WithSeekableStream(configuration, stream, s => InternalDetectFormat(s, configuration)); @@ -37,22 +41,28 @@ namespace SixLabors.ImageSharp /// Reads the raw image information from the specified stream without fully decoding it. /// /// The image stream to read the header from. - /// Thrown if the stream is not readable. + /// The stream is null. + /// The stream is not readable. + /// Image contains invalid content. /// /// The or null if suitable info detector not found. /// - public static IImageInfo Identify(Stream stream) => Identify(stream, out IImageFormat _); + public static IImageInfo Identify(Stream stream) + => Identify(stream, out IImageFormat _); /// /// Reads the raw image information from the specified stream without fully decoding it. /// /// The image stream to read the header from. /// The format type of the decoded image. - /// Thrown if the stream is not readable. + /// The stream is null. + /// The stream is not readable. + /// Image contains invalid content. /// /// The or null if suitable info detector not found. /// - public static IImageInfo Identify(Stream stream, out IImageFormat format) => Identify(Configuration.Default, stream, out format); + public static IImageInfo Identify(Stream stream, out IImageFormat format) + => Identify(Configuration.Default, stream, out format); /// /// Reads the raw image information from the specified stream without fully decoding it. @@ -60,7 +70,10 @@ namespace SixLabors.ImageSharp /// The configuration. /// The image stream to read the information from. /// The format type of the decoded image. - /// Thrown if the stream is not readable. + /// The configuration is null. + /// The stream is null. + /// The stream is not readable. + /// Image contains invalid content. /// /// The or null if suitable info detector is not found. /// @@ -78,18 +91,23 @@ namespace SixLabors.ImageSharp /// /// The stream containing image information. /// The format type of the decoded image. - /// Thrown if the stream is not readable. - /// Image cannot be loaded. + /// The stream is null. + /// The stream is not readable. + /// Image format not recognised. + /// Image contains invalid content. /// The . - public static Image Load(Stream stream, out IImageFormat format) => Load(Configuration.Default, stream, out format); + public static Image Load(Stream stream, out IImageFormat format) + => Load(Configuration.Default, stream, out format); /// /// Decode a new instance of the class from the given stream. /// The pixel format is selected by the decoder. /// /// The stream containing image information. - /// Thrown if the stream is not readable. - /// Image cannot be loaded. + /// The stream is null. + /// The stream is not readable. + /// Image format not recognised. + /// Image contains invalid content. /// The . public static Image Load(Stream stream) => Load(Configuration.Default, stream); @@ -99,10 +117,14 @@ namespace SixLabors.ImageSharp /// /// The stream containing image information. /// The decoder. - /// Thrown if the stream is not readable. - /// Image cannot be loaded. + /// The stream is null. + /// The decoder is null. + /// The stream is not readable. + /// Image format not recognised. + /// Image contains invalid content. /// The . - public static Image Load(Stream stream, IImageDecoder decoder) => Load(Configuration.Default, stream, decoder); + public static Image Load(Stream stream, IImageDecoder decoder) + => Load(Configuration.Default, stream, decoder); /// /// Decode a new instance of the class from the given stream. @@ -111,19 +133,29 @@ namespace SixLabors.ImageSharp /// The configuration for the decoder. /// The stream containing image information. /// The decoder. - /// Thrown if the stream is not readable. - /// Image cannot be loaded. + /// The configuration is null. + /// The stream is null. + /// The decoder is null. + /// The stream is not readable. + /// Image format not recognised. + /// Image contains invalid content. /// A new .> - public static Image Load(Configuration configuration, Stream stream, IImageDecoder decoder) => - WithSeekableStream(configuration, stream, s => decoder.Decode(configuration, s)); + public static Image Load(Configuration configuration, Stream stream, IImageDecoder decoder) + { + Guard.NotNull(decoder, nameof(decoder)); + return WithSeekableStream(configuration, stream, s => decoder.Decode(configuration, s)); + } /// /// Decode a new instance of the class from the given stream. /// /// The configuration for the decoder. /// The stream containing image information. - /// Thrown if the stream is not readable. - /// Image cannot be loaded. + /// The configuration is null. + /// The stream is null. + /// The stream is not readable. + /// Image format not recognised. + /// Image contains invalid content. /// A new .> public static Image Load(Configuration configuration, Stream stream) => Load(configuration, stream, out _); @@ -131,8 +163,10 @@ namespace SixLabors.ImageSharp /// Create a new instance of the class from the given stream. /// /// The stream containing image information. - /// Thrown if the stream is not readable. - /// Image cannot be loaded. + /// The stream is null. + /// The stream is not readable. + /// Image format not recognised. + /// Image contains invalid content. /// The pixel format. /// A new .> public static Image Load(Stream stream) @@ -144,8 +178,10 @@ namespace SixLabors.ImageSharp /// /// The stream containing image information. /// The format type of the decoded image. - /// Thrown if the stream is not readable. - /// Image cannot be loaded. + /// The stream is null. + /// The stream is not readable. + /// Image format not recognised. + /// Image contains invalid content. /// The pixel format. /// A new .> public static Image Load(Stream stream, out IImageFormat format) @@ -157,8 +193,10 @@ namespace SixLabors.ImageSharp /// /// The stream containing image information. /// The decoder. - /// Thrown if the stream is not readable. - /// Image cannot be loaded. + /// The stream is null. + /// The stream is not readable. + /// Image format not recognised. + /// Image contains invalid content. /// The pixel format. /// A new .> public static Image Load(Stream stream, IImageDecoder decoder) @@ -171,8 +209,11 @@ namespace SixLabors.ImageSharp /// The Configuration. /// The stream containing image information. /// The decoder. - /// Thrown if the stream is not readable. - /// Image cannot be loaded. + /// The configuration is null. + /// The stream is null. + /// The stream is not readable. + /// Image format not recognised. + /// Image contains invalid content. /// The pixel format. /// A new .> public static Image Load(Configuration configuration, Stream stream, IImageDecoder decoder) @@ -184,8 +225,11 @@ namespace SixLabors.ImageSharp /// /// The configuration options. /// The stream containing image information. - /// Thrown if the stream is not readable. - /// Image cannot be loaded. + /// The configuration is null. + /// The stream is null. + /// The stream is not readable. + /// Image format not recognised. + /// Image contains invalid content. /// The pixel format. /// A new .> public static Image Load(Configuration configuration, Stream stream) @@ -198,14 +242,16 @@ namespace SixLabors.ImageSharp /// The configuration options. /// The stream containing image information. /// The format type of the decoded image. - /// Thrown if the stream is not readable. - /// Image cannot be loaded. + /// The configuration is null. + /// The stream is null. + /// The stream is not readable. + /// Image format not recognised. + /// Image contains invalid content. /// The pixel format. /// A new . public static Image Load(Configuration configuration, Stream stream, out IImageFormat format) where TPixel : unmanaged, IPixel { - Guard.NotNull(configuration, nameof(configuration)); (Image img, IImageFormat format) data = WithSeekableStream(configuration, stream, s => Decode(s, configuration)); format = data.format; @@ -220,7 +266,7 @@ namespace SixLabors.ImageSharp foreach (KeyValuePair val in configuration.ImageFormatsManager.ImageDecoders) { - sb.AppendLine($" - {val.Key.Name} : {val.Value.GetType().Name}"); + sb.AppendFormat(" - {0} : {1}{2}", val.Key.Name, val.Value.GetType().Name, Environment.NewLine); } throw new UnknownImageFormatException(sb.ToString()); @@ -233,12 +279,14 @@ namespace SixLabors.ImageSharp /// The configuration options. /// The stream containing image information. /// The format type of the decoded image. - /// Thrown if the stream is not readable. - /// Image cannot be loaded. + /// The configuration is null. + /// The stream is null. + /// The stream is not readable. + /// Image format not recognised. + /// Image contains invalid content. /// A new . public static Image Load(Configuration configuration, Stream stream, out IImageFormat format) { - Guard.NotNull(configuration, nameof(configuration)); (Image img, IImageFormat format) data = WithSeekableStream(configuration, stream, s => Decode(s, configuration)); format = data.format; @@ -253,7 +301,7 @@ namespace SixLabors.ImageSharp foreach (KeyValuePair val in configuration.ImageFormatsManager.ImageDecoders) { - sb.AppendLine($" - {val.Key.Name} : {val.Value.GetType().Name}"); + sb.AppendFormat(" - {0} : {1}{2}", val.Key.Name, val.Value.GetType().Name, Environment.NewLine); } throw new UnknownImageFormatException(sb.ToString()); @@ -261,6 +309,9 @@ namespace SixLabors.ImageSharp private static T WithSeekableStream(Configuration configuration, Stream stream, Func action) { + Guard.NotNull(configuration, nameof(configuration)); + Guard.NotNull(stream, nameof(stream)); + if (!stream.CanRead) { throw new NotSupportedException("Cannot read from the stream."); diff --git a/src/ImageSharp/Image.LoadPixelData.cs b/src/ImageSharp/Image.LoadPixelData.cs index f8f2e84850..f36243cc3e 100644 --- a/src/ImageSharp/Image.LoadPixelData.cs +++ b/src/ImageSharp/Image.LoadPixelData.cs @@ -1,9 +1,8 @@ -// Copyright (c) Six Labors and contributors. +// Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. using System; using System.Runtime.InteropServices; -using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; @@ -21,6 +20,7 @@ namespace SixLabors.ImageSharp /// The width of the final image. /// The height of the final image. /// The pixel format. + /// The data length is incorrect. /// A new . public static Image LoadPixelData(TPixel[] data, int width, int height) where TPixel : unmanaged, IPixel @@ -33,6 +33,7 @@ namespace SixLabors.ImageSharp /// The width of the final image. /// The height of the final image. /// The pixel format. + /// The data length is incorrect. /// A new . public static Image LoadPixelData(ReadOnlySpan data, int width, int height) where TPixel : unmanaged, IPixel @@ -45,6 +46,7 @@ namespace SixLabors.ImageSharp /// The width of the final image. /// The height of the final image. /// The pixel format. + /// The data length is incorrect. /// A new . public static Image LoadPixelData(byte[] data, int width, int height) where TPixel : unmanaged, IPixel @@ -57,6 +59,7 @@ namespace SixLabors.ImageSharp /// The width of the final image. /// The height of the final image. /// The pixel format. + /// The data length is incorrect. /// A new . public static Image LoadPixelData(ReadOnlySpan data, int width, int height) where TPixel : unmanaged, IPixel @@ -65,60 +68,68 @@ namespace SixLabors.ImageSharp /// /// Create a new instance of the class from the given byte array in format. /// - /// The config for the decoder. + /// The configuration for the decoder. /// The byte array containing image data. /// The width of the final image. /// The height of the final image. /// The pixel format. + /// The configuration is null. + /// The data length is incorrect. /// A new . - public static Image LoadPixelData(Configuration config, byte[] data, int width, int height) + public static Image LoadPixelData(Configuration configuration, byte[] data, int width, int height) where TPixel : unmanaged, IPixel - => LoadPixelData(config, MemoryMarshal.Cast(new ReadOnlySpan(data)), width, height); + => LoadPixelData(configuration, MemoryMarshal.Cast(new ReadOnlySpan(data)), width, height); /// /// Create a new instance of the class from the given byte array in format. /// - /// The config for the decoder. + /// The configuration for the decoder. /// The byte array containing image data. /// The width of the final image. /// The height of the final image. /// The pixel format. + /// The configuration is null. + /// The data length is incorrect. /// A new . - public static Image LoadPixelData(Configuration config, ReadOnlySpan data, int width, int height) + public static Image LoadPixelData(Configuration configuration, ReadOnlySpan data, int width, int height) where TPixel : unmanaged, IPixel - => LoadPixelData(config, MemoryMarshal.Cast(data), width, height); + => LoadPixelData(configuration, MemoryMarshal.Cast(data), width, height); /// /// Create a new instance of the class from the raw data. /// - /// The config for the decoder. + /// The configuration for the decoder. /// The Span containing the image Pixel data. /// The width of the final image. /// The height of the final image. /// The pixel format. + /// The configuration is null. + /// The data length is incorrect. /// A new . - public static Image LoadPixelData(Configuration config, TPixel[] data, int width, int height) + public static Image LoadPixelData(Configuration configuration, TPixel[] data, int width, int height) where TPixel : unmanaged, IPixel - { - return LoadPixelData(config, new ReadOnlySpan(data), width, height); - } + => LoadPixelData(configuration, new ReadOnlySpan(data), width, height); /// /// Create a new instance of the class from the raw data. /// - /// The config for the decoder. + /// The configuration for the decoder. /// The Span containing the image Pixel data. /// The width of the final image. /// The height of the final image. + /// The configuration is null. + /// The data length is incorrect. /// The pixel format. /// A new . - public static Image LoadPixelData(Configuration config, ReadOnlySpan data, int width, int height) + public static Image LoadPixelData(Configuration configuration, ReadOnlySpan data, int width, int height) where TPixel : unmanaged, IPixel { + Guard.NotNull(configuration, nameof(configuration)); + int count = width * height; Guard.MustBeGreaterThanOrEqualTo(data.Length, count, nameof(data)); - var image = new Image(config, width, height); + var image = new Image(configuration, width, height); data = data.Slice(0, count); data.CopyTo(image.Frames.RootFrame.PixelBuffer.FastMemoryGroup); diff --git a/src/ImageSharp/Image.WrapMemory.cs b/src/ImageSharp/Image.WrapMemory.cs index e5181a0db3..0dd8c814d0 100644 --- a/src/ImageSharp/Image.WrapMemory.cs +++ b/src/ImageSharp/Image.WrapMemory.cs @@ -1,4 +1,4 @@ -// Copyright (c) Six Labors and contributors. +// Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. using System; @@ -20,22 +20,27 @@ namespace SixLabors.ImageSharp /// allowing to view/manipulate it as an ImageSharp instance. /// /// The pixel type - /// The + /// The /// The pixel memory. /// The width of the memory image. /// The height of the memory image. /// The . + /// The configuration is null. + /// The metadata is null. /// An instance public static Image WrapMemory( - Configuration config, + Configuration configuration, Memory pixelMemory, int width, int height, ImageMetadata metadata) where TPixel : unmanaged, IPixel { + Guard.NotNull(configuration, nameof(configuration)); + Guard.NotNull(metadata, nameof(metadata)); + var memorySource = MemoryGroup.Wrap(pixelMemory); - return new Image(config, memorySource, width, height, metadata); + return new Image(configuration, memorySource, width, height, metadata); } /// @@ -43,20 +48,19 @@ namespace SixLabors.ImageSharp /// allowing to view/manipulate it as an ImageSharp instance. /// /// The pixel type - /// The + /// The /// The pixel memory. /// The width of the memory image. /// The height of the memory image. + /// The configuration is null. /// An instance. public static Image WrapMemory( - Configuration config, + Configuration configuration, Memory pixelMemory, int width, int height) where TPixel : unmanaged, IPixel - { - return WrapMemory(config, pixelMemory, width, height, new ImageMetadata()); - } + => WrapMemory(configuration, pixelMemory, width, height, new ImageMetadata()); /// /// Wraps an existing contiguous memory area of 'width' x 'height' pixels, @@ -73,9 +77,7 @@ namespace SixLabors.ImageSharp int width, int height) where TPixel : unmanaged, IPixel - { - return WrapMemory(Configuration.Default, pixelMemory, width, height); - } + => WrapMemory(Configuration.Default, pixelMemory, width, height); /// /// Wraps an existing contiguous memory area of 'width' x 'height' pixels, @@ -85,22 +87,27 @@ namespace SixLabors.ImageSharp /// It will be disposed together with the result image. /// /// The pixel type - /// The + /// The /// The that is being transferred to the image /// The width of the memory image. /// The height of the memory image. /// The + /// The configuration is null. + /// The metadata is null. /// An instance public static Image WrapMemory( - Configuration config, + Configuration configuration, IMemoryOwner pixelMemoryOwner, int width, int height, ImageMetadata metadata) where TPixel : unmanaged, IPixel { + Guard.NotNull(configuration, nameof(configuration)); + Guard.NotNull(metadata, nameof(metadata)); + var memorySource = MemoryGroup.Wrap(pixelMemoryOwner); - return new Image(config, memorySource, width, height, metadata); + return new Image(configuration, memorySource, width, height, metadata); } /// @@ -111,20 +118,19 @@ namespace SixLabors.ImageSharp /// It will be disposed together with the result image. /// /// The pixel type. - /// The + /// The /// The that is being transferred to the image. /// The width of the memory image. /// The height of the memory image. + /// The configuration is null. /// An instance public static Image WrapMemory( - Configuration config, + Configuration configuration, IMemoryOwner pixelMemoryOwner, int width, int height) where TPixel : unmanaged, IPixel - { - return WrapMemory(config, pixelMemoryOwner, width, height, new ImageMetadata()); - } + => WrapMemory(configuration, pixelMemoryOwner, width, height, new ImageMetadata()); /// /// Wraps an existing contiguous memory area of 'width' x 'height' pixels, @@ -143,8 +149,6 @@ namespace SixLabors.ImageSharp int width, int height) where TPixel : unmanaged, IPixel - { - return WrapMemory(Configuration.Default, pixelMemoryOwner, width, height); - } + => WrapMemory(Configuration.Default, pixelMemoryOwner, width, height); } } diff --git a/src/ImageSharp/ImageExtensions.cs b/src/ImageSharp/ImageExtensions.cs index 62ac449b72..e5b2a32a99 100644 --- a/src/ImageSharp/ImageExtensions.cs +++ b/src/ImageSharp/ImageExtensions.cs @@ -7,12 +7,11 @@ using System.IO; using System.Text; using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Formats; -using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp { /// - /// Extension methods over Image{TPixel}. + /// Extension methods for the type. /// public static partial class ImageExtensions { @@ -20,13 +19,13 @@ namespace SixLabors.ImageSharp /// Writes the image to the given stream using the currently loaded image format. /// /// The source image. - /// The file path to save the image to. - /// Thrown if the stream is null. - public static void Save(this Image source, string filePath) + /// The file path to save the image to. + /// The path is null. + public static void Save(this Image source, string path) { - Guard.NotNullOrWhiteSpace(filePath, nameof(filePath)); + Guard.NotNull(path, nameof(path)); - string ext = Path.GetExtension(filePath); + string ext = Path.GetExtension(path); IImageFormat format = source.GetConfiguration().ImageFormatsManager.FindFormatByFileExtension(ext); if (format is null) { @@ -34,7 +33,7 @@ namespace SixLabors.ImageSharp sb.AppendLine($"No encoder was found for extension '{ext}'. Registered encoders include:"); foreach (IImageFormat fmt in source.GetConfiguration().ImageFormats) { - sb.AppendLine($" - {fmt.Name} : {string.Join(", ", fmt.FileExtensions)}"); + sb.AppendFormat(" - {0} : {1}{2}", fmt.Name, string.Join(", ", fmt.FileExtensions), Environment.NewLine); } throw new NotSupportedException(sb.ToString()); @@ -48,26 +47,28 @@ namespace SixLabors.ImageSharp sb.AppendLine($"No encoder was found for extension '{ext}' using image format '{format.Name}'. Registered encoders include:"); foreach (KeyValuePair enc in source.GetConfiguration().ImageFormatsManager.ImageEncoders) { - sb.AppendLine($" - {enc.Key} : {enc.Value.GetType().Name}"); + sb.AppendFormat(" - {0} : {1}{2}", enc.Key, enc.Value.GetType().Name, Environment.NewLine); } throw new NotSupportedException(sb.ToString()); } - source.Save(filePath, encoder); + source.Save(path, encoder); } /// /// Writes the image to the given stream using the currently loaded image format. /// /// The source image. - /// The file path to save the image to. + /// The file path to save the image to. /// The encoder to save the image with. - /// Thrown if the encoder is null. - public static void Save(this Image source, string filePath, IImageEncoder encoder) + /// The path is null. + /// The encoder is null. + public static void Save(this Image source, string path, IImageEncoder encoder) { + Guard.NotNull(path, nameof(path)); Guard.NotNull(encoder, nameof(encoder)); - using (Stream fs = source.GetConfiguration().FileSystem.Create(filePath)) + using (Stream fs = source.GetConfiguration().FileSystem.Create(path)) { source.Save(fs, encoder); } @@ -79,10 +80,20 @@ namespace SixLabors.ImageSharp /// The source image. /// The stream to save the image to. /// The format to save the image in. - /// Thrown if the stream is null. + /// The stream is null. + /// The format is null. + /// The stream is not writable. + /// No encoder available for provided format. public static void Save(this Image source, Stream stream, IImageFormat format) { + Guard.NotNull(stream, nameof(stream)); Guard.NotNull(format, nameof(format)); + + if (!stream.CanWrite) + { + throw new NotSupportedException("Cannot write to the stream."); + } + IImageEncoder encoder = source.GetConfiguration().ImageFormatsManager.FindEncoder(format); if (encoder is null) @@ -92,7 +103,7 @@ namespace SixLabors.ImageSharp foreach (KeyValuePair val in source.GetConfiguration().ImageFormatsManager.ImageEncoders) { - sb.AppendLine($" - {val.Key.Name} : {val.Value.GetType().Name}"); + sb.AppendFormat(" - {0} : {1}{2}", val.Key.Name, val.Value.GetType().Name, Environment.NewLine); } throw new NotSupportedException(sb.ToString()); @@ -113,9 +124,12 @@ namespace SixLabors.ImageSharp /// /// The source image /// The format. + /// The format is null. /// The public static string ToBase64String(this Image source, IImageFormat format) { + Guard.NotNull(format, nameof(format)); + using var stream = new MemoryStream(); source.Save(stream, format); diff --git a/src/ImageSharp/Memory/Buffer2D{T}.cs b/src/ImageSharp/Memory/Buffer2D{T}.cs index bf8630931a..ada1d29b6d 100644 --- a/src/ImageSharp/Memory/Buffer2D{T}.cs +++ b/src/ImageSharp/Memory/Buffer2D{T}.cs @@ -156,7 +156,7 @@ namespace SixLabors.ImageSharp.Memory { DebugGuard.MustBeGreaterThanOrEqualTo(y, 0, nameof(y)); DebugGuard.MustBeLessThan(y, this.Height, nameof(y)); - return this.FastMemoryGroup.View.GetBoundedSlice(y * this.Width, this.Width); + return this.FastMemoryGroup.View.GetBoundedSlice(y * (long)this.Width, this.Width); } /// @@ -200,7 +200,7 @@ namespace SixLabors.ImageSharp.Memory } [MethodImpl(InliningOptions.ColdPath)] - private Memory GetRowMemorySlow(int y) => this.FastMemoryGroup.GetBoundedSlice(y * this.Width, this.Width); + private Memory GetRowMemorySlow(int y) => this.FastMemoryGroup.GetBoundedSlice(y * (long)this.Width, this.Width); [MethodImpl(InliningOptions.ColdPath)] private Memory GetSingleMemorySlow() => this.FastMemoryGroup.Single(); diff --git a/src/ImageSharp/Memory/DiscontiguousBuffers/IMemoryGroup{T}.cs b/src/ImageSharp/Memory/DiscontiguousBuffers/IMemoryGroup{T}.cs index 89aca914d0..3bb6b8d336 100644 --- a/src/ImageSharp/Memory/DiscontiguousBuffers/IMemoryGroup{T}.cs +++ b/src/ImageSharp/Memory/DiscontiguousBuffers/IMemoryGroup{T}.cs @@ -33,5 +33,15 @@ namespace SixLabors.ImageSharp.Memory /// the image buffers internally. /// bool IsValid { get; } + + /// + /// Returns a value-type implementing an allocation-free enumerator of the memory groups in the current + /// instance. The return type shouldn't be used directly: just use a block on + /// the instance in use and the C# compiler will automatically invoke this + /// method behind the scenes. This method takes precedence over the + /// implementation, which is still available when casting to one of the underlying interfaces. + /// + /// A new instance mapping the current values in use. + new MemoryGroupEnumerator GetEnumerator(); } } diff --git a/src/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroupEnumerator{T}.cs b/src/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroupEnumerator{T}.cs new file mode 100644 index 0000000000..1bc44e33e1 --- /dev/null +++ b/src/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroupEnumerator{T}.cs @@ -0,0 +1,69 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.ComponentModel; +using System.Runtime.CompilerServices; + +namespace SixLabors.ImageSharp.Memory +{ + /// + /// A value-type enumerator for instances. + /// + /// The element type. + [EditorBrowsable(EditorBrowsableState.Never)] + public ref struct MemoryGroupEnumerator + where T : struct + { + private readonly IMemoryGroup memoryGroup; + private readonly int count; + private int index; + + [MethodImpl(InliningOptions.ShortMethod)] + internal MemoryGroupEnumerator(MemoryGroup.Owned memoryGroup) + { + this.memoryGroup = memoryGroup; + this.count = memoryGroup.Count; + this.index = -1; + } + + [MethodImpl(InliningOptions.ShortMethod)] + internal MemoryGroupEnumerator(MemoryGroup.Consumed memoryGroup) + { + this.memoryGroup = memoryGroup; + this.count = memoryGroup.Count; + this.index = -1; + } + + [MethodImpl(InliningOptions.ShortMethod)] + internal MemoryGroupEnumerator(MemoryGroupView memoryGroup) + { + this.memoryGroup = memoryGroup; + this.count = memoryGroup.Count; + this.index = -1; + } + + /// + public Memory Current + { + [MethodImpl(InliningOptions.ShortMethod)] + get => this.memoryGroup[this.index]; + } + + /// + [MethodImpl(InliningOptions.ShortMethod)] + public bool MoveNext() + { + int index = this.index + 1; + + if (index < this.count) + { + this.index = index; + + return true; + } + + return false; + } + } +} diff --git a/src/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroupExtensions.cs b/src/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroupExtensions.cs index 28da49263e..295f9190a9 100644 --- a/src/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroupExtensions.cs +++ b/src/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroupExtensions.cs @@ -1,4 +1,4 @@ -// Copyright (c) Six Labors and contributors. +// Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. using System; @@ -38,6 +38,12 @@ namespace SixLabors.ImageSharp.Memory Guard.MustBeLessThan(start, group.TotalLength, nameof(start)); int bufferIdx = (int)(start / group.BufferLength); + + if (bufferIdx < 0) + { + throw new ArgumentOutOfRangeException(nameof(start)); + } + if (bufferIdx >= group.Count) { throw new ArgumentOutOfRangeException(nameof(start)); diff --git a/src/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroupView{T}.cs b/src/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroupView{T}.cs index 3f39ba12f5..1698f08d17 100644 --- a/src/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroupView{T}.cs +++ b/src/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroupView{T}.cs @@ -5,6 +5,7 @@ using System; using System.Buffers; using System.Collections; using System.Collections.Generic; +using System.Runtime.CompilerServices; namespace SixLabors.ImageSharp.Memory { @@ -37,6 +38,7 @@ namespace SixLabors.ImageSharp.Memory public int Count { + [MethodImpl(InliningOptions.ShortMethod)] get { this.EnsureIsValid(); @@ -73,7 +75,15 @@ namespace SixLabors.ImageSharp.Memory } } - public IEnumerator> GetEnumerator() + /// + [MethodImpl(InliningOptions.ShortMethod)] + public MemoryGroupEnumerator GetEnumerator() + { + return new MemoryGroupEnumerator(this); + } + + /// + IEnumerator> IEnumerable>.GetEnumerator() { this.EnsureIsValid(); for (int i = 0; i < this.Count; i++) @@ -82,7 +92,8 @@ namespace SixLabors.ImageSharp.Memory } } - IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator(); + /// + IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable>)this).GetEnumerator(); internal void Invalidate() { diff --git a/src/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroup{T}.Consumed.cs b/src/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroup{T}.Consumed.cs index f1fe4ed9c5..1dfbaea932 100644 --- a/src/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroup{T}.Consumed.cs +++ b/src/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroup{T}.Consumed.cs @@ -2,16 +2,17 @@ // Licensed under the Apache License, Version 2.0. using System; -using System.Buffers; using System.Collections.Generic; -using System.Linq; +using System.Runtime.CompilerServices; namespace SixLabors.ImageSharp.Memory { internal abstract partial class MemoryGroup { - // Analogous to the "consumed" variant of MemorySource - private sealed class Consumed : MemoryGroup + /// + /// A implementation that consumes the underlying memory buffers. + /// + public sealed class Consumed : MemoryGroup, IEnumerable> { private readonly Memory[] source; @@ -22,16 +23,31 @@ namespace SixLabors.ImageSharp.Memory this.View = new MemoryGroupView(this); } - public override int Count => this.source.Length; + public override int Count + { + [MethodImpl(InliningOptions.ShortMethod)] + get => this.source.Length; + } public override Memory this[int index] => this.source[index]; - public override IEnumerator> GetEnumerator() + /// + [MethodImpl(InliningOptions.ShortMethod)] + public override MemoryGroupEnumerator GetEnumerator() + { + return new MemoryGroupEnumerator(this); + } + + /// + IEnumerator> IEnumerable>.GetEnumerator() { - for (int i = 0; i < this.source.Length; i++) - { - yield return this.source[i]; - } + /* The runtime sees the Array class as if it implemented the + * type-generic collection interfaces explicitly, so here we + * can just cast the source array to IList> (or to + * an equivalent type), and invoke the generic GetEnumerator + * method directly from that interface reference. This saves + * having to create our own iterator block here. */ + return ((IList>)this.source).GetEnumerator(); } public override void Dispose() diff --git a/src/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroup{T}.Owned.cs b/src/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroup{T}.Owned.cs index b42b90d286..5a86ac4268 100644 --- a/src/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroup{T}.Owned.cs +++ b/src/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroup{T}.Owned.cs @@ -5,13 +5,16 @@ using System; using System.Buffers; using System.Collections.Generic; using System.Linq; +using System.Runtime.CompilerServices; namespace SixLabors.ImageSharp.Memory { - // Analogous to the "owned" variant of MemorySource internal abstract partial class MemoryGroup { - private sealed class Owned : MemoryGroup + /// + /// A implementation that owns the underlying memory buffers. + /// + public sealed class Owned : MemoryGroup, IEnumerable> { private IMemoryOwner[] memoryOwners; @@ -29,6 +32,7 @@ namespace SixLabors.ImageSharp.Memory public override int Count { + [MethodImpl(InliningOptions.ShortMethod)] get { this.EnsureNotDisposed(); @@ -45,7 +49,15 @@ namespace SixLabors.ImageSharp.Memory } } - public override IEnumerator> GetEnumerator() + /// + [MethodImpl(InliningOptions.ShortMethod)] + public override MemoryGroupEnumerator GetEnumerator() + { + return new MemoryGroupEnumerator(this); + } + + /// + IEnumerator> IEnumerable>.GetEnumerator() { this.EnsureNotDisposed(); return this.memoryOwners.Select(mo => mo.Memory).GetEnumerator(); @@ -69,14 +81,21 @@ namespace SixLabors.ImageSharp.Memory this.IsValid = false; } + [MethodImpl(InliningOptions.ShortMethod)] private void EnsureNotDisposed() { - if (this.memoryOwners == null) + if (this.memoryOwners is null) { - throw new ObjectDisposedException(nameof(MemoryGroup)); + ThrowObjectDisposedException(); } } + [MethodImpl(MethodImplOptions.NoInlining)] + private static void ThrowObjectDisposedException() + { + throw new ObjectDisposedException(nameof(MemoryGroup)); + } + internal static void SwapContents(Owned a, Owned b) { a.EnsureNotDisposed(); diff --git a/src/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroup{T}.cs b/src/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroup{T}.cs index 38de57b4ac..6fd93f12ea 100644 --- a/src/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroup{T}.cs +++ b/src/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroup{T}.cs @@ -6,7 +6,6 @@ using System.Buffers; using System.Collections; using System.Collections.Generic; using System.Runtime.CompilerServices; -using SixLabors.ImageSharp.Memory.Internals; namespace SixLabors.ImageSharp.Memory { @@ -48,10 +47,21 @@ namespace SixLabors.ImageSharp.Memory public abstract void Dispose(); /// - public abstract IEnumerator> GetEnumerator(); + public abstract MemoryGroupEnumerator GetEnumerator(); /// - IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator(); + IEnumerator> IEnumerable>.GetEnumerator() + { + /* This method is implemented in each derived class. + * Implementing the method here as non-abstract and throwing, + * then reimplementing it explicitly in each derived class, is + * a workaround for the lack of support for abstract explicit + * interface method implementations in C#. */ + throw new NotImplementedException($"The type {this.GetType()} needs to override IEnumerable>.GetEnumerator()"); + } + + /// + IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable>)this).GetEnumerator(); /// /// Creates a new memory group, allocating it's buffers with the provided allocator. diff --git a/src/ImageSharp/Metadata/ImageMetadata.cs b/src/ImageSharp/Metadata/ImageMetadata.cs index b3751bfbdc..716e89e68d 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 11d0bd01b0..29c21d6113 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 0000000000..b00355181c 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 0000000000..9206e43771 --- /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 0000000000..7258a02917 --- /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 0000000000..6b39769a7f --- /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 0000000000..e63781012a --- /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 0000000000..1217ca0c70 --- /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/src/ImageSharp/Processing/Extensions/ProcessingExtensions.cs b/src/ImageSharp/Processing/Extensions/ProcessingExtensions.cs index 59be5bf02c..45cff93982 100644 --- a/src/ImageSharp/Processing/Extensions/ProcessingExtensions.cs +++ b/src/ImageSharp/Processing/Extensions/ProcessingExtensions.cs @@ -19,6 +19,10 @@ namespace SixLabors.ImageSharp.Processing /// /// The image to mutate. /// The operation to perform on the source. + /// The source is null. + /// The operation is null. + /// The source has been disposed. + /// The processing operation failed. public static void Mutate(this Image source, Action operation) => Mutate(source, source.GetConfiguration(), operation); @@ -28,6 +32,11 @@ namespace SixLabors.ImageSharp.Processing /// The image to mutate. /// The configuration which allows altering default behaviour or extending the library. /// The operation to perform on the source. + /// The configuration is null. + /// The source is null. + /// The operation is null. + /// The source has been disposed. + /// The processing operation failed. public static void Mutate(this Image source, Configuration configuration, Action operation) { Guard.NotNull(configuration, nameof(configuration)); @@ -44,6 +53,10 @@ namespace SixLabors.ImageSharp.Processing /// The pixel format. /// The image to mutate. /// The operation to perform on the source. + /// The source is null. + /// The operation is null. + /// The source has been disposed. + /// The processing operation failed. public static void Mutate(this Image source, Action operation) where TPixel : unmanaged, IPixel => Mutate(source, source.GetConfiguration(), operation); @@ -55,6 +68,11 @@ namespace SixLabors.ImageSharp.Processing /// The image to mutate. /// The configuration which allows altering default behaviour or extending the library. /// The operation to perform on the source. + /// The configuration is null. + /// The source is null. + /// The operation is null. + /// The source has been disposed. + /// The processing operation failed. public static void Mutate(this Image source, Configuration configuration, Action operation) where TPixel : unmanaged, IPixel { @@ -75,6 +93,10 @@ namespace SixLabors.ImageSharp.Processing /// The pixel format. /// The image to mutate. /// The operations to perform on the source. + /// The source is null. + /// The operations are null. + /// The source has been disposed. + /// The processing operation failed. public static void Mutate(this Image source, params IImageProcessor[] operations) where TPixel : unmanaged, IPixel => Mutate(source, source.GetConfiguration(), operations); @@ -86,6 +108,11 @@ namespace SixLabors.ImageSharp.Processing /// The image to mutate. /// The configuration which allows altering default behaviour or extending the library. /// The operations to perform on the source. + /// The configuration is null. + /// The source is null. + /// The operations are null. + /// The source has been disposed. + /// The processing operation failed. public static void Mutate(this Image source, Configuration configuration, params IImageProcessor[] operations) where TPixel : unmanaged, IPixel { @@ -104,7 +131,11 @@ namespace SixLabors.ImageSharp.Processing /// /// The image to clone. /// The operation to perform on the clone. - /// The new . + /// The new . + /// The source is null. + /// The operation is null. + /// The source has been disposed. + /// The processing operation failed. public static Image Clone(this Image source, Action operation) => Clone(source, source.GetConfiguration(), operation); @@ -114,7 +145,12 @@ namespace SixLabors.ImageSharp.Processing /// The image to clone. /// The configuration which allows altering default behaviour or extending the library. /// The operation to perform on the clone. - /// The new . + /// The configuration is null. + /// The source is null. + /// The operation is null. + /// The source has been disposed. + /// The processing operation failed. + /// The new . public static Image Clone(this Image source, Configuration configuration, Action operation) { Guard.NotNull(configuration, nameof(configuration)); @@ -133,7 +169,11 @@ namespace SixLabors.ImageSharp.Processing /// The pixel format. /// The image to clone. /// The operation to perform on the clone. - /// The new + /// The source is null. + /// The operation is null. + /// The source has been disposed. + /// The processing operation failed. + /// The new . public static Image Clone(this Image source, Action operation) where TPixel : unmanaged, IPixel => Clone(source, source.GetConfiguration(), operation); @@ -145,7 +185,12 @@ namespace SixLabors.ImageSharp.Processing /// The image to clone. /// The configuration which allows altering default behaviour or extending the library. /// The operation to perform on the clone. - /// The new + /// The configuration is null. + /// The source is null. + /// The operation is null. + /// The source has been disposed. + /// The processing operation failed. + /// The new public static Image Clone(this Image source, Configuration configuration, Action operation) where TPixel : unmanaged, IPixel { @@ -167,7 +212,11 @@ namespace SixLabors.ImageSharp.Processing /// The pixel format. /// The image to clone. /// The operations to perform on the clone. - /// The new + /// The source is null. + /// The operations are null. + /// The source has been disposed. + /// The processing operation failed. + /// The new public static Image Clone(this Image source, params IImageProcessor[] operations) where TPixel : unmanaged, IPixel => Clone(source, source.GetConfiguration(), operations); @@ -179,7 +228,12 @@ namespace SixLabors.ImageSharp.Processing /// The image to clone. /// The configuration which allows altering default behaviour or extending the library. /// The operations to perform on the clone. - /// The new + /// The configuration is null. + /// The source is null. + /// The operations are null. + /// The source has been disposed. + /// The processing operation failed. + /// The new public static Image Clone(this Image source, Configuration configuration, params IImageProcessor[] operations) where TPixel : unmanaged, IPixel { @@ -200,6 +254,7 @@ namespace SixLabors.ImageSharp.Processing /// /// The image processing context. /// The operations to perform on the source. + /// The processing operation failed. /// The to allow chaining of operations. public static IImageProcessingContext ApplyProcessors( this IImageProcessingContext source, diff --git a/tests/ImageSharp.Tests/Formats/Bmp/BmpDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Bmp/BmpDecoderTests.cs index a8376f51b9..85cdf6d11a 100644 --- a/tests/ImageSharp.Tests/Formats/Bmp/BmpDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Bmp/BmpDecoderTests.cs @@ -394,10 +394,10 @@ namespace SixLabors.ImageSharp.Tests.Formats.Bmp [Theory] [WithFile(InvalidPaletteSize, PixelTypes.Rgba32)] - public void BmpDecoder_ThrowsImageFormatException_OnInvalidPaletteSize(TestImageProvider provider) + public void BmpDecoder_ThrowsInvalidImageContentException_OnInvalidPaletteSize(TestImageProvider provider) where TPixel : unmanaged, IPixel { - Assert.Throws(() => + Assert.Throws(() => { using (provider.GetImage(BmpDecoder)) { diff --git a/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Baseline.cs b/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Baseline.cs index cf2e5c81b3..57051a9d7b 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Baseline.cs +++ b/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Baseline.cs @@ -52,7 +52,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg [Theory] [WithFileCollection(nameof(UnrecoverableTestJpegs), PixelTypes.Rgba32)] - public void UnrecoverableImage_Throws_ImageFormatException(TestImageProvider provider) - where TPixel : unmanaged, IPixel => Assert.Throws(provider.GetImage); + public void UnrecoverableImage_Throws_InvalidImageContentException(TestImageProvider provider) + where TPixel : unmanaged, IPixel => Assert.Throws(provider.GetImage); } } diff --git a/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.Chunks.cs b/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.Chunks.cs index ee4001c203..297512fa6a 100644 --- a/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.Chunks.cs +++ b/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.Chunks.cs @@ -72,7 +72,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Png var decoder = new PngDecoder(); ImageFormatException exception = - Assert.Throws(() => decoder.Decode(null, memStream)); + Assert.Throws(() => decoder.Decode(null, memStream)); Assert.Equal($"CRC Error. PNG {chunkName} chunk is corrupt!", exception.Message); } diff --git a/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs index 46112bdd81..b08025f532 100644 --- a/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs @@ -89,6 +89,12 @@ namespace SixLabors.ImageSharp.Tests.Formats.Png TestImages.Png.Issue1014_5, TestImages.Png.Issue1014_6 }; + public static readonly string[] TestImagesIssue1177 = + { + TestImages.Png.Issue1177_1, + TestImages.Png.Issue1177_2 + }; + [Theory] [WithFileCollection(nameof(CommonTestImages), PixelTypes.Rgba32)] public void Decode(TestImageProvider provider) @@ -243,6 +249,23 @@ namespace SixLabors.ImageSharp.Tests.Formats.Png Assert.Null(ex); } + [Theory] + [WithFileCollection(nameof(TestImagesIssue1177), PixelTypes.Rgba32)] + public void Issue1177(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + System.Exception ex = Record.Exception( + () => + { + using (Image image = provider.GetImage(PngDecoder)) + { + image.DebugSave(provider); + image.CompareToOriginal(provider, ImageComparer.Exact); + } + }); + Assert.Null(ex); + } + [Theory] [WithFile(TestImages.Png.Issue1127, PixelTypes.Rgba32)] public void Issue1127(TestImageProvider provider) diff --git a/tests/ImageSharp.Tests/Memory/DiscontiguousBuffers/MemoryGroupTests.cs b/tests/ImageSharp.Tests/Memory/DiscontiguousBuffers/MemoryGroupTests.cs index 694c4d32f6..15b07265f6 100644 --- a/tests/ImageSharp.Tests/Memory/DiscontiguousBuffers/MemoryGroupTests.cs +++ b/tests/ImageSharp.Tests/Memory/DiscontiguousBuffers/MemoryGroupTests.cs @@ -1,8 +1,10 @@ -// Copyright (c) Six Labors and contributors. +// Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. using System; using System.Buffers; +using System.Collections; +using System.Collections.Generic; using System.Linq; using System.Runtime.InteropServices; using SixLabors.ImageSharp.Memory; @@ -150,6 +152,7 @@ namespace SixLabors.ImageSharp.Tests.Memory.DiscontiguousBuffers public static TheoryData GetBoundedSlice_ErrorData = new TheoryData() { + { 300, 100, -1, 91 }, { 300, 100, 110, 91 }, { 42, 7, 0, 8 }, { 42, 7, 1, 7 }, @@ -165,7 +168,7 @@ namespace SixLabors.ImageSharp.Tests.Memory.DiscontiguousBuffers } [Fact] - public void Fill() + public void FillWithFastEnumerator() { using MemoryGroup group = this.CreateTestGroup(100, 10, true); group.Fill(42); @@ -177,6 +180,34 @@ namespace SixLabors.ImageSharp.Tests.Memory.DiscontiguousBuffers } } + [Fact] + public void FillWithSlowGenericEnumerator() + { + using MemoryGroup group = this.CreateTestGroup(100, 10, true); + group.Fill(42); + + int[] expectedRow = Enumerable.Repeat(42, 10).ToArray(); + IReadOnlyList> groupAsList = group; + foreach (Memory memory in groupAsList) + { + Assert.True(memory.Span.SequenceEqual(expectedRow)); + } + } + + [Fact] + public void FillWithSlowEnumerator() + { + using MemoryGroup group = this.CreateTestGroup(100, 10, true); + group.Fill(42); + + int[] expectedRow = Enumerable.Repeat(42, 10).ToArray(); + IEnumerable groupAsList = group; + foreach (Memory memory in groupAsList) + { + Assert.True(memory.Span.SequenceEqual(expectedRow)); + } + } + [Fact] public void Clear() { 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 0000000000..d9f44cef9c --- /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 892568803e..18fd84331f 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/tests/ImageSharp.Tests/TestImages.cs @@ -95,6 +95,10 @@ namespace SixLabors.ImageSharp.Tests public const string Issue1014_6 = "Png/issues/Issue_1014_6.png"; public const string Issue1127 = "Png/issues/Issue_1127.png"; + // Issue 1177: https://github.com/SixLabors/ImageSharp/issues/1177 + public const string Issue1177_1 = "Png/issues/Issue_1177_1.png"; + public const string Issue1177_2 = "Png/issues/Issue_1177_2.png"; + public static class Bad { // Odd chunk lengths @@ -162,6 +166,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 0000000000..9300dced9a --- /dev/null +++ b/tests/Images/Input/Jpg/baseline/iptc-psAPP13-wIPTCempty.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c7508a28e39026ed8ebc9751138d014450b2f636a343838d8e08dbc7e19ad6df +size 18329 diff --git a/tests/Images/Input/Jpg/baseline/iptc.jpg b/tests/Images/Input/Jpg/baseline/iptc.jpg new file mode 100644 index 0000000000..adb12621fb --- /dev/null +++ b/tests/Images/Input/Jpg/baseline/iptc.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6c8a0747d9282bfd7e8e7f4a0119c53c702bf600384b786ef9b5263457f38ada +size 18611 diff --git a/tests/Images/Input/Png/issues/Issue_1177_1.png b/tests/Images/Input/Png/issues/Issue_1177_1.png new file mode 100644 index 0000000000..2d851e31bf --- /dev/null +++ b/tests/Images/Input/Png/issues/Issue_1177_1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cef2be6012f4604f9f30b51273661058df0201be4de508235f372eb2304b2132 +size 7023 diff --git a/tests/Images/Input/Png/issues/Issue_1177_2.png b/tests/Images/Input/Png/issues/Issue_1177_2.png new file mode 100644 index 0000000000..efd043b38c --- /dev/null +++ b/tests/Images/Input/Png/issues/Issue_1177_2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7067af724977e1ecd8fc761f50226eaaa9e9d4142be963b4edbbf0918b8eba1d +size 57125