From f74dbac80be8e0eed97a0ac5268bddeb63dfb1bd Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Fri, 6 May 2022 16:13:41 +0200 Subject: [PATCH 01/15] Add parsing bitmap V5 header --- src/ImageSharp/Formats/Bmp/BmpColorSpace.cs | 37 +++++++ src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs | 12 ++- src/ImageSharp/Formats/Bmp/BmpFileHeader.cs | 5 +- src/ImageSharp/Formats/Bmp/BmpInfoHeader.cs | 96 ++++++++++++++----- .../Formats/Bmp/BmpRenderingIntent.cs | 37 +++++++ 5 files changed, 158 insertions(+), 29 deletions(-) create mode 100644 src/ImageSharp/Formats/Bmp/BmpColorSpace.cs create mode 100644 src/ImageSharp/Formats/Bmp/BmpRenderingIntent.cs diff --git a/src/ImageSharp/Formats/Bmp/BmpColorSpace.cs b/src/ImageSharp/Formats/Bmp/BmpColorSpace.cs new file mode 100644 index 0000000000..8640871219 --- /dev/null +++ b/src/ImageSharp/Formats/Bmp/BmpColorSpace.cs @@ -0,0 +1,37 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +// ReSharper disable InconsistentNaming +namespace SixLabors.ImageSharp.Formats.Bmp +{ + /// + /// Enum for the different color spaces. + /// + internal enum BmpColorSpace + { + /// + /// This value implies that endpoints and gamma values are given in the appropriate fields. + /// + LCS_CALIBRATED_RGB = 0, + + /// + /// The Windows default color space ('Win '). + /// + LCS_WINDOWS_COLOR_SPACE = 1466527264, + + /// + /// Specifies that the bitmap is in sRGB color space ('sRGB'). + /// + LCS_sRGB = 1934772034, + + /// + /// This value indicates that bV5ProfileData points to the file name of the profile to use (gamma and endpoints values are ignored). + /// + PROFILE_LINKED = 1279872587, + + /// + /// This value indicates that bV5ProfileData points to a memory buffer that contains the profile to be used (gamma and endpoints values are ignored). + /// + PROFILE_EMBEDDED = 1296188740 + } +} diff --git a/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs b/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs index a22a04980c..ab0f1bd031 100644 --- a/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs +++ b/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs @@ -1271,12 +1271,18 @@ namespace SixLabors.ImageSharp.Formats.Bmp infoHeaderType = BmpInfoHeaderType.Os2Version2; this.infoHeader = BmpInfoHeader.ParseOs2Version2(buffer); } - else if (headerSize >= BmpInfoHeader.SizeV4) + else if (headerSize == BmpInfoHeader.SizeV4) { - // >= 108 bytes - infoHeaderType = headerSize == BmpInfoHeader.SizeV4 ? BmpInfoHeaderType.WinVersion4 : BmpInfoHeaderType.WinVersion5; + // == 108 bytes + infoHeaderType = BmpInfoHeaderType.WinVersion4; this.infoHeader = BmpInfoHeader.ParseV4(buffer); } + else if (headerSize > BmpInfoHeader.SizeV4) + { + // > 108 bytes + infoHeaderType = BmpInfoHeaderType.WinVersion5; + this.infoHeader = BmpInfoHeader.ParseV5(buffer); + } else { BmpThrowHelper.ThrowNotSupportedException($"ImageSharp does not support this BMP file. HeaderSize '{headerSize}'."); diff --git a/src/ImageSharp/Formats/Bmp/BmpFileHeader.cs b/src/ImageSharp/Formats/Bmp/BmpFileHeader.cs index acbcdaef3a..ab56bd246b 100644 --- a/src/ImageSharp/Formats/Bmp/BmpFileHeader.cs +++ b/src/ImageSharp/Formats/Bmp/BmpFileHeader.cs @@ -57,10 +57,7 @@ namespace SixLabors.ImageSharp.Formats.Bmp /// public int Offset { get; } - public static BmpFileHeader Parse(Span data) - { - return MemoryMarshal.Cast(data)[0]; - } + public static BmpFileHeader Parse(Span data) => MemoryMarshal.Cast(data)[0]; public void WriteTo(Span buffer) { diff --git a/src/ImageSharp/Formats/Bmp/BmpInfoHeader.cs b/src/ImageSharp/Formats/Bmp/BmpInfoHeader.cs index 0d0c05c9f4..70462b73ae 100644 --- a/src/ImageSharp/Formats/Bmp/BmpInfoHeader.cs +++ b/src/ImageSharp/Formats/Bmp/BmpInfoHeader.cs @@ -82,7 +82,7 @@ namespace SixLabors.ImageSharp.Formats.Bmp int greenMask = 0, int blueMask = 0, int alphaMask = 0, - int csType = 0, + BmpColorSpace csType = 0, int redX = 0, int redY = 0, int redZ = 0, @@ -94,7 +94,11 @@ namespace SixLabors.ImageSharp.Formats.Bmp int blueZ = 0, int gammeRed = 0, int gammeGreen = 0, - int gammeBlue = 0) + int gammeBlue = 0, + BmpRenderingIntent intent = BmpRenderingIntent.Invalid, + int profileData = 0, + int profileSize = 0, + int reserved = 0) { this.HeaderSize = headerSize; this.Width = width; @@ -124,6 +128,10 @@ namespace SixLabors.ImageSharp.Formats.Bmp this.GammaRed = gammeRed; this.GammaGreen = gammeGreen; this.GammaBlue = gammeBlue; + this.Intent = intent; + this.ProfileData = profileData; + this.ProfileSize = profileSize; + this.Reserved = reserved; } /// @@ -211,7 +219,7 @@ namespace SixLabors.ImageSharp.Formats.Bmp /// /// Gets or sets the Color space type. Not used yet. /// - public int CsType { get; set; } + public BmpColorSpace CsType { get; set; } /// /// Gets or sets the X coordinate of red endpoint. Not used yet. @@ -273,21 +281,38 @@ namespace SixLabors.ImageSharp.Formats.Bmp /// public int GammaBlue { get; set; } + /// + /// Gets or sets the rendering intent for bitmap. + /// + public BmpRenderingIntent Intent { get; set; } + + /// + /// Gets or sets the offset, in bytes, from the beginning of the BITMAPV5HEADER structure to the start of the profile data. + /// + public int ProfileData { get; set; } + + /// + /// Gets or sets the size, in bytes, of embedded profile data. + /// + public int ProfileSize { get; set; } + + /// + /// Gets or sets the reserved value. + /// + public int Reserved { get; set; } + /// /// Parses the BITMAPCOREHEADER (BMP Version 2) consisting of the headerSize, width, height, planes, and bitsPerPixel fields (12 bytes). /// /// The data to parse. /// The parsed header. /// - public static BmpInfoHeader ParseCore(ReadOnlySpan data) - { - return new BmpInfoHeader( + public static BmpInfoHeader ParseCore(ReadOnlySpan data) => new( headerSize: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(0, 4)), width: BinaryPrimitives.ReadUInt16LittleEndian(data.Slice(4, 2)), height: BinaryPrimitives.ReadUInt16LittleEndian(data.Slice(6, 2)), planes: BinaryPrimitives.ReadInt16LittleEndian(data.Slice(8, 2)), bitsPerPixel: BinaryPrimitives.ReadInt16LittleEndian(data.Slice(10, 2))); - } /// /// Parses a short variant of the OS22XBITMAPHEADER. It is identical to the BITMAPCOREHEADER, except that the width and height @@ -296,15 +321,12 @@ namespace SixLabors.ImageSharp.Formats.Bmp /// The data to parse. /// The parsed header. /// - public static BmpInfoHeader ParseOs22Short(ReadOnlySpan data) - { - return new BmpInfoHeader( + public static BmpInfoHeader ParseOs22Short(ReadOnlySpan data) => new( headerSize: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(0, 4)), width: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(4, 4)), height: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(8, 4)), planes: BinaryPrimitives.ReadInt16LittleEndian(data.Slice(12, 2)), bitsPerPixel: BinaryPrimitives.ReadInt16LittleEndian(data.Slice(14, 2))); - } /// /// Parses the full BMP Version 3 BITMAPINFOHEADER header (40 bytes). @@ -312,9 +334,7 @@ namespace SixLabors.ImageSharp.Formats.Bmp /// The data to parse. /// The parsed header. /// - public static BmpInfoHeader ParseV3(ReadOnlySpan data) - { - return new BmpInfoHeader( + public static BmpInfoHeader ParseV3(ReadOnlySpan data) => new( headerSize: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(0, 4)), width: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(4, 4)), height: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(8, 4)), @@ -326,7 +346,6 @@ namespace SixLabors.ImageSharp.Formats.Bmp yPelsPerMeter: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(28, 4)), clrUsed: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(32, 4)), clrImportant: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(36, 4))); - } /// /// Special case of the BITMAPINFOHEADER V3 used by adobe where the color bitmasks are part of the info header instead of following it. @@ -336,9 +355,7 @@ namespace SixLabors.ImageSharp.Formats.Bmp /// Indicates, if the alpha bitmask is present. /// The parsed header. /// - public static BmpInfoHeader ParseAdobeV3(ReadOnlySpan data, bool withAlpha = true) - { - return new BmpInfoHeader( + public static BmpInfoHeader ParseAdobeV3(ReadOnlySpan data, bool withAlpha = true) => new( headerSize: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(0, 4)), width: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(4, 4)), height: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(8, 4)), @@ -354,7 +371,6 @@ namespace SixLabors.ImageSharp.Formats.Bmp greenMask: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(44, 4)), blueMask: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(48, 4)), alphaMask: withAlpha ? BinaryPrimitives.ReadInt32LittleEndian(data.Slice(52, 4)) : 0); - } /// /// Parses a OS/2 version 2 bitmap header (64 bytes). Only the first 40 bytes are parsed which are @@ -413,11 +429,47 @@ namespace SixLabors.ImageSharp.Formats.Bmp /// The data to parse. /// The parsed header. /// - public static BmpInfoHeader ParseV4(ReadOnlySpan data) + public static BmpInfoHeader ParseV4(ReadOnlySpan data) => new( + headerSize: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(0, 4)), + width: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(4, 4)), + height: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(8, 4)), + planes: BinaryPrimitives.ReadInt16LittleEndian(data.Slice(12, 2)), + bitsPerPixel: BinaryPrimitives.ReadInt16LittleEndian(data.Slice(14, 2)), + compression: (BmpCompression)BinaryPrimitives.ReadInt32LittleEndian(data.Slice(16, 4)), + imageSize: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(20, 4)), + xPelsPerMeter: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(24, 4)), + yPelsPerMeter: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(28, 4)), + clrUsed: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(32, 4)), + clrImportant: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(36, 4)), + redMask: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(40, 4)), + greenMask: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(44, 4)), + blueMask: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(48, 4)), + alphaMask: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(52, 4)), + csType: (BmpColorSpace)BinaryPrimitives.ReadInt32LittleEndian(data.Slice(56, 4)), + redX: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(60, 4)), + redY: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(64, 4)), + redZ: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(68, 4)), + greenX: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(72, 4)), + greenY: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(76, 4)), + greenZ: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(80, 4)), + blueX: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(84, 4)), + blueY: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(88, 4)), + blueZ: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(92, 4)), + gammeRed: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(96, 4)), + gammeGreen: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(100, 4)), + gammeBlue: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(104, 4))); + + /// + /// Parses the full BMP Version 5 BITMAPINFOHEADER header (124 bytes). + /// + /// The data to parse. + /// The parsed header. + /// + public static BmpInfoHeader ParseV5(ReadOnlySpan data) { - if (data.Length < SizeV4) + if (data.Length < SizeV5) { - throw new ArgumentException(nameof(data), $"Must be {SizeV4} bytes. Was {data.Length} bytes."); + throw new ArgumentException(nameof(data), $"Must be {SizeV5} bytes. Was {data.Length} bytes."); } return MemoryMarshal.Cast(data)[0]; diff --git a/src/ImageSharp/Formats/Bmp/BmpRenderingIntent.cs b/src/ImageSharp/Formats/Bmp/BmpRenderingIntent.cs new file mode 100644 index 0000000000..8e03625959 --- /dev/null +++ b/src/ImageSharp/Formats/Bmp/BmpRenderingIntent.cs @@ -0,0 +1,37 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +// ReSharper disable InconsistentNaming +namespace SixLabors.ImageSharp.Formats.Bmp +{ + /// + /// Enum for the different rendering intent's. + /// + internal enum BmpRenderingIntent + { + /// + /// Invalid default value. + /// + Invalid = 0, + + /// + /// TMaintains saturation. Used for business charts and other situations in which undithered colors are required. + /// + LCS_GM_BUSINESS = 1, + + /// + /// Maintains colorimetric match. Used for graphic designs and named colors. + /// + LCS_GM_GRAPHICS = 2, + + /// + /// Maintains contrast. Used for photographs and natural images. + /// + LCS_GM_IMAGES = 4, + + /// + /// Maintains the white point. Matches the colors to their nearest color in the destination gamut. + /// + LCS_GM_ABS_COLORIMETRIC = 8, + } +} From fa86a04464effdb47a14d4780aeacb8d3fdaee8d Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Fri, 6 May 2022 17:32:22 +0200 Subject: [PATCH 02/15] Fix writing bitmap v4 header --- src/ImageSharp/Formats/Bmp/BmpInfoHeader.cs | 32 +++++++++++++++++++-- 1 file changed, 29 insertions(+), 3 deletions(-) diff --git a/src/ImageSharp/Formats/Bmp/BmpInfoHeader.cs b/src/ImageSharp/Formats/Bmp/BmpInfoHeader.cs index 70462b73ae..9b6e4e6524 100644 --- a/src/ImageSharp/Formats/Bmp/BmpInfoHeader.cs +++ b/src/ImageSharp/Formats/Bmp/BmpInfoHeader.cs @@ -501,9 +501,35 @@ namespace SixLabors.ImageSharp.Formats.Bmp /// The buffer to write to. public void WriteV4Header(Span buffer) { - ref BmpInfoHeader dest = ref Unsafe.As(ref MemoryMarshal.GetReference(buffer)); - - dest = this; + buffer.Clear(); + BinaryPrimitives.WriteInt32LittleEndian(buffer.Slice(0, 4), SizeV4); + BinaryPrimitives.WriteInt32LittleEndian(buffer.Slice(4, 4), this.Width); + BinaryPrimitives.WriteInt32LittleEndian(buffer.Slice(8, 4), this.Height); + BinaryPrimitives.WriteInt16LittleEndian(buffer.Slice(12, 2), this.Planes); + BinaryPrimitives.WriteInt16LittleEndian(buffer.Slice(14, 2), this.BitsPerPixel); + BinaryPrimitives.WriteInt32LittleEndian(buffer.Slice(16, 4), (int)this.Compression); + BinaryPrimitives.WriteInt32LittleEndian(buffer.Slice(20, 4), this.ImageSize); + BinaryPrimitives.WriteInt32LittleEndian(buffer.Slice(24, 4), this.XPelsPerMeter); + BinaryPrimitives.WriteInt32LittleEndian(buffer.Slice(28, 4), this.YPelsPerMeter); + BinaryPrimitives.WriteInt32LittleEndian(buffer.Slice(32, 4), this.ClrUsed); + BinaryPrimitives.WriteInt32LittleEndian(buffer.Slice(36, 4), this.ClrImportant); + BinaryPrimitives.WriteInt32LittleEndian(buffer.Slice(40, 4), this.RedMask); + BinaryPrimitives.WriteInt32LittleEndian(buffer.Slice(44, 4), this.GreenMask); + BinaryPrimitives.WriteInt32LittleEndian(buffer.Slice(48, 4), this.BlueMask); + BinaryPrimitives.WriteInt32LittleEndian(buffer.Slice(52, 4), this.AlphaMask); + BinaryPrimitives.WriteInt32LittleEndian(buffer.Slice(56, 4), (int)this.CsType); + BinaryPrimitives.WriteInt32LittleEndian(buffer.Slice(60, 4), this.RedX); + BinaryPrimitives.WriteInt32LittleEndian(buffer.Slice(64, 4), this.RedY); + BinaryPrimitives.WriteInt32LittleEndian(buffer.Slice(68, 4), this.RedZ); + BinaryPrimitives.WriteInt32LittleEndian(buffer.Slice(72, 4), this.GreenX); + BinaryPrimitives.WriteInt32LittleEndian(buffer.Slice(76, 4), this.GreenY); + BinaryPrimitives.WriteInt32LittleEndian(buffer.Slice(80, 4), this.GreenZ); + BinaryPrimitives.WriteInt32LittleEndian(buffer.Slice(84, 4), this.BlueX); + BinaryPrimitives.WriteInt32LittleEndian(buffer.Slice(88, 4), this.BlueY); + BinaryPrimitives.WriteInt32LittleEndian(buffer.Slice(92, 4), this.BlueZ); + BinaryPrimitives.WriteInt32LittleEndian(buffer.Slice(96, 4), this.GammaRed); + BinaryPrimitives.WriteInt32LittleEndian(buffer.Slice(100, 4), this.GammaGreen); + BinaryPrimitives.WriteInt32LittleEndian(buffer.Slice(104, 4), this.GammaBlue); } internal void VerifyDimensions() From 6991fd13bd64becce280b63ab99bcf9a01de61ee Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Fri, 6 May 2022 17:50:37 +0200 Subject: [PATCH 03/15] Read ICC profile from v5 header --- src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs | 41 ++++++++++++-------- 1 file changed, 25 insertions(+), 16 deletions(-) diff --git a/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs b/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs index ab0f1bd031..f88e5762d2 100644 --- a/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs +++ b/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs @@ -11,6 +11,7 @@ using SixLabors.ImageSharp.Common.Helpers; using SixLabors.ImageSharp.IO; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Metadata; +using SixLabors.ImageSharp.Metadata.Profiles.Icc; using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Formats.Bmp @@ -185,7 +186,7 @@ namespace SixLabors.ImageSharp.Formats.Bmp break; default: - BmpThrowHelper.ThrowNotSupportedException("Does not support this kind of bitmap files."); + BmpThrowHelper.ThrowNotSupportedException("ImageSharp does not support this kind of bitmap files."); break; } @@ -1199,6 +1200,13 @@ namespace SixLabors.ImageSharp.Formats.Bmp private void ReadInfoHeader() { Span buffer = stackalloc byte[BmpInfoHeader.MaxHeaderSize]; + var infoHeaderStart = this.stream.Position; + + // Resolution is stored in PPM. + this.metadata = new ImageMetadata + { + ResolutionUnits = PixelResolutionUnit.PixelsPerMeter + }; // Read the header size. this.stream.Read(buffer, 0, BmpInfoHeader.HeaderSizeSize); @@ -1282,31 +1290,34 @@ namespace SixLabors.ImageSharp.Formats.Bmp // > 108 bytes infoHeaderType = BmpInfoHeaderType.WinVersion5; this.infoHeader = BmpInfoHeader.ParseV5(buffer); + if (this.infoHeader.ProfileData != 0 && this.infoHeader.ProfileSize != 0) + { + // Read color profile. + long streamPosition = this.stream.Position; + byte[] iccProfileData = new byte[this.infoHeader.ProfileSize]; + this.stream.Position = infoHeaderStart + this.infoHeader.ProfileData; + this.stream.Read(iccProfileData); + this.metadata.IccProfile = new IccProfile(iccProfileData); + this.stream.Position = streamPosition; + } } else { BmpThrowHelper.ThrowNotSupportedException($"ImageSharp does not support this BMP file. HeaderSize '{headerSize}'."); } - // Resolution is stored in PPM. - var meta = new ImageMetadata - { - ResolutionUnits = PixelResolutionUnit.PixelsPerMeter - }; if (this.infoHeader.XPelsPerMeter > 0 && this.infoHeader.YPelsPerMeter > 0) { - meta.HorizontalResolution = this.infoHeader.XPelsPerMeter; - meta.VerticalResolution = this.infoHeader.YPelsPerMeter; + this.metadata.HorizontalResolution = this.infoHeader.XPelsPerMeter; + this.metadata.VerticalResolution = this.infoHeader.YPelsPerMeter; } else { // Convert default metadata values to PPM. - meta.HorizontalResolution = Math.Round(UnitConverter.InchToMeter(ImageMetadata.DefaultHorizontalResolution)); - meta.VerticalResolution = Math.Round(UnitConverter.InchToMeter(ImageMetadata.DefaultVerticalResolution)); + this.metadata.HorizontalResolution = Math.Round(UnitConverter.InchToMeter(ImageMetadata.DefaultHorizontalResolution)); + this.metadata.VerticalResolution = Math.Round(UnitConverter.InchToMeter(ImageMetadata.DefaultVerticalResolution)); } - this.metadata = meta; - short bitsPerPixel = this.infoHeader.BitsPerPixel; this.bmpMetadata = this.metadata.GetBmpMetadata(); this.bmpMetadata.InfoHeaderType = infoHeaderType; @@ -1376,9 +1387,7 @@ namespace SixLabors.ImageSharp.Formats.Bmp int colorMapSizeBytes = -1; if (this.infoHeader.ClrUsed == 0) { - if (this.infoHeader.BitsPerPixel == 1 - || this.infoHeader.BitsPerPixel == 4 - || this.infoHeader.BitsPerPixel == 8) + if (this.infoHeader.BitsPerPixel is 1 or 4 or 8) { switch (this.fileMarkerType) { @@ -1430,7 +1439,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.ThrowInvalidImageContentException("Invalid fileheader offset found. Offset is greater than the stream length."); + BmpThrowHelper.ThrowInvalidImageContentException("Invalid file header offset found. Offset is greater than the stream length."); } if (skipAmount > 0) From 106f6c0a9b1520619746b7877d9ce5f1049a660e Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Sat, 7 May 2022 11:31:28 +0200 Subject: [PATCH 04/15] Write color profile, if present --- src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs | 2 +- src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs | 149 ++++++++++++++---- src/ImageSharp/Formats/Bmp/BmpInfoHeader.cs | 11 ++ .../Formats/Bmp/BmpRenderingIntent.cs | 2 +- 4 files changed, 132 insertions(+), 32 deletions(-) diff --git a/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs b/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs index f88e5762d2..26687ff16f 100644 --- a/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs +++ b/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs @@ -1200,7 +1200,7 @@ namespace SixLabors.ImageSharp.Formats.Bmp private void ReadInfoHeader() { Span buffer = stackalloc byte[BmpInfoHeader.MaxHeaderSize]; - var infoHeaderStart = this.stream.Position; + long infoHeaderStart = this.stream.Position; // Resolution is stored in PPM. this.metadata = new ImageMetadata diff --git a/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs b/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs index 6384074df3..32025f69fc 100644 --- a/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs +++ b/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs @@ -3,6 +3,7 @@ using System; using System.Buffers; +using System.Buffers.Binary; using System.IO; using System.Runtime.InteropServices; using System.Threading; @@ -79,9 +80,10 @@ namespace SixLabors.ImageSharp.Formats.Bmp /// /// A bitmap v4 header will only be written, if the user explicitly wants support for transparency. /// In this case the compression type BITFIELDS will be used. + /// If the image contains a color profile, a bitmap v5 header is written, which is needed to write this info. /// Otherwise a bitmap v3 header will be written, which is supported by almost all decoders. /// - private readonly bool writeV4Header; + private BmpInfoHeaderType infoHeaderType; /// /// The quantizer for reducing the color count for 8-Bit, 4-Bit and 1-Bit images. @@ -97,8 +99,8 @@ namespace SixLabors.ImageSharp.Formats.Bmp { this.memoryAllocator = memoryAllocator; this.bitsPerPixel = options.BitsPerPixel; - this.writeV4Header = options.SupportTransparency; this.quantizer = options.Quantizer ?? KnownQuantizers.Octree; + this.infoHeaderType = options.SupportTransparency ? BmpInfoHeaderType.WinVersion4 : BmpInfoHeaderType.WinVersion3; } /// @@ -123,7 +125,62 @@ namespace SixLabors.ImageSharp.Formats.Bmp int bytesPerLine = 4 * (((image.Width * bpp) + 31) / 32); this.padding = bytesPerLine - (int)(image.Width * (bpp / 8F)); - // Set Resolution. + int colorPaletteSize = 0; + if (this.bitsPerPixel == BmpBitsPerPixel.Pixel8) + { + colorPaletteSize = ColorPaletteSize8Bit; + } + else if (this.bitsPerPixel == BmpBitsPerPixel.Pixel4) + { + colorPaletteSize = ColorPaletteSize4Bit; + } + else if (this.bitsPerPixel == BmpBitsPerPixel.Pixel1) + { + colorPaletteSize = ColorPaletteSize1Bit; + } + + byte[] iccProfileData = null; + int iccProfileSize = 0; + if (metadata.IccProfile != null) + { + this.infoHeaderType = BmpInfoHeaderType.WinVersion5; + iccProfileData = metadata.IccProfile.ToByteArray(); + iccProfileSize = iccProfileData.Length; + } + + int infoHeaderSize = this.infoHeaderType switch + { + BmpInfoHeaderType.WinVersion3 => BmpInfoHeader.SizeV3, + BmpInfoHeaderType.WinVersion4 => BmpInfoHeader.SizeV4, + BmpInfoHeaderType.WinVersion5 => BmpInfoHeader.SizeV5, + _ => BmpInfoHeader.SizeV3 + }; + + BmpInfoHeader infoHeader = this.CreateBmpInfoHeader(image.Width, image.Height, infoHeaderSize, bpp, bytesPerLine, metadata, iccProfileData); + + Span buffer = stackalloc byte[infoHeaderSize]; + + this.WriteBitmapFileHeader(stream, infoHeaderSize, colorPaletteSize, iccProfileSize, infoHeader, buffer); + this.WriteBitmapInfoHeader(stream, infoHeader, buffer, infoHeaderSize); + this.WriteImage(stream, image.Frames.RootFrame); + this.WriteColorProfile(stream, metadata, buffer); + + stream.Flush(); + } + + /// + /// Creates the bitmap information header. + /// + /// The width of the image. + /// The height of the image. + /// Size of the information header. + /// The bits per pixel. + /// The bytes per line. + /// The metadata. + /// The icc profile data. + /// The bitmap information header. + private BmpInfoHeader CreateBmpInfoHeader(int width, int height, int infoHeaderSize, short bpp, int bytesPerLine, ImageMetadata metadata, byte[] iccProfileData) + { int hResolution = 0; int vResolution = 0; @@ -154,20 +211,19 @@ namespace SixLabors.ImageSharp.Formats.Bmp } } - int infoHeaderSize = this.writeV4Header ? BmpInfoHeader.SizeV4 : BmpInfoHeader.SizeV3; var infoHeader = new BmpInfoHeader( headerSize: infoHeaderSize, - height: image.Height, - width: image.Width, + height: height, + width: width, bitsPerPixel: bpp, planes: 1, - imageSize: image.Height * bytesPerLine, + imageSize: height * bytesPerLine, clrUsed: 0, clrImportant: 0, xPelsPerMeter: hResolution, yPelsPerMeter: vResolution); - if (this.writeV4Header && this.bitsPerPixel == BmpBitsPerPixel.Pixel32) + if ((this.infoHeaderType is BmpInfoHeaderType.WinVersion4 or BmpInfoHeaderType.WinVersion5) && this.bitsPerPixel == BmpBitsPerPixel.Pixel32) { infoHeader.AlphaMask = Rgba32AlphaMask; infoHeader.RedMask = Rgba32RedMask; @@ -176,45 +232,78 @@ namespace SixLabors.ImageSharp.Formats.Bmp infoHeader.Compression = BmpCompression.BitFields; } - int colorPaletteSize = 0; - if (this.bitsPerPixel == BmpBitsPerPixel.Pixel8) + if (this.infoHeaderType is BmpInfoHeaderType.WinVersion5 && metadata.IccProfile != null) { - colorPaletteSize = ColorPaletteSize8Bit; + infoHeader.ProfileSize = iccProfileData.Length; + infoHeader.CsType = BmpColorSpace.PROFILE_EMBEDDED; + infoHeader.Intent = BmpRenderingIntent.LCS_GM_IMAGES; } - else if (this.bitsPerPixel == BmpBitsPerPixel.Pixel4) - { - colorPaletteSize = ColorPaletteSize4Bit; - } - else if (this.bitsPerPixel == BmpBitsPerPixel.Pixel1) + + return infoHeader; + } + + /// + /// Writes the color profile to the stream. + /// + /// The stream to write to. + /// The metadata. + /// The buffer. + private void WriteColorProfile(Stream stream, ImageMetadata metadata, Span buffer) + { + if (metadata.IccProfile != null) { - colorPaletteSize = ColorPaletteSize1Bit; + int streamPositionAfterImageData = (int)stream.Position; + stream.Write(metadata.IccProfile.ToByteArray()); + BinaryPrimitives.WriteInt32LittleEndian(buffer, streamPositionAfterImageData); + stream.Position = BmpFileHeader.Size + 112; + stream.Write(buffer.Slice(0, 4)); } + } + /// + /// Writes the bitmap file header. + /// + /// The stream to write the header to. + /// Size of the bitmap information header. + /// Size of the color palette. + /// The size in bytes of the color profile. + /// The information header to write. + /// The buffer to write to. + private void WriteBitmapFileHeader(Stream stream, int infoHeaderSize, int colorPaletteSize, int iccProfileSize, BmpInfoHeader infoHeader, Span buffer) + { var fileHeader = new BmpFileHeader( type: BmpConstants.TypeMarkers.Bitmap, - fileSize: BmpFileHeader.Size + infoHeaderSize + colorPaletteSize + infoHeader.ImageSize, + fileSize: BmpFileHeader.Size + infoHeaderSize + colorPaletteSize + iccProfileSize + infoHeader.ImageSize, reserved: 0, offset: BmpFileHeader.Size + infoHeaderSize + colorPaletteSize); - Span buffer = stackalloc byte[infoHeaderSize]; fileHeader.WriteTo(buffer); - stream.Write(buffer, 0, BmpFileHeader.Size); + } - if (this.writeV4Header) - { - infoHeader.WriteV4Header(buffer); - } - else + /// + /// Writes the bitmap information header. + /// + /// The stream to write info header into. + /// The information header. + /// The buffer. + /// Size of the information header. + private void WriteBitmapInfoHeader(Stream stream, BmpInfoHeader infoHeader, Span buffer, int infoHeaderSize) + { + switch (this.infoHeaderType) { - infoHeader.WriteV3Header(buffer); + case BmpInfoHeaderType.WinVersion3: + infoHeader.WriteV3Header(buffer); + break; + case BmpInfoHeaderType.WinVersion4: + infoHeader.WriteV4Header(buffer); + break; + case BmpInfoHeaderType.WinVersion5: + infoHeader.WriteV5Header(buffer); + break; } stream.Write(buffer, 0, infoHeaderSize); - - this.WriteImage(stream, image.Frames.RootFrame); - - stream.Flush(); } /// diff --git a/src/ImageSharp/Formats/Bmp/BmpInfoHeader.cs b/src/ImageSharp/Formats/Bmp/BmpInfoHeader.cs index 9b6e4e6524..31394821f8 100644 --- a/src/ImageSharp/Formats/Bmp/BmpInfoHeader.cs +++ b/src/ImageSharp/Formats/Bmp/BmpInfoHeader.cs @@ -532,6 +532,17 @@ namespace SixLabors.ImageSharp.Formats.Bmp BinaryPrimitives.WriteInt32LittleEndian(buffer.Slice(104, 4), this.GammaBlue); } + /// + /// Writes a complete Bitmap V5 header to a buffer. + /// + /// The buffer to write to. + public void WriteV5Header(Span buffer) + { + ref BmpInfoHeader dest = ref Unsafe.As(ref MemoryMarshal.GetReference(buffer)); + + dest = this; + } + internal void VerifyDimensions() { const int MaximumBmpDimension = 65535; diff --git a/src/ImageSharp/Formats/Bmp/BmpRenderingIntent.cs b/src/ImageSharp/Formats/Bmp/BmpRenderingIntent.cs index 8e03625959..e437a0cbf8 100644 --- a/src/ImageSharp/Formats/Bmp/BmpRenderingIntent.cs +++ b/src/ImageSharp/Formats/Bmp/BmpRenderingIntent.cs @@ -15,7 +15,7 @@ namespace SixLabors.ImageSharp.Formats.Bmp Invalid = 0, /// - /// TMaintains saturation. Used for business charts and other situations in which undithered colors are required. + /// Maintains saturation. Used for business charts and other situations in which undithered colors are required. /// LCS_GM_BUSINESS = 1, From 5388489d380f3a060bfe1c3e406ab26e185dfb2a Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Sat, 7 May 2022 12:24:40 +0200 Subject: [PATCH 05/15] Add color profile tests --- src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs | 13 ++++----- .../Formats/Bmp/BmpDecoderTests.cs | 4 +-- .../Formats/Bmp/BmpEncoderTests.cs | 27 +++++++++++++++++++ .../Formats/Bmp/BmpMetadataTests.cs | 16 ++++++++++- tests/ImageSharp.Tests/TestImages.cs | 1 + tests/Images/Input/Bmp/BMP_v5_with_ICC_2.bmp | 3 +++ 6 files changed, 55 insertions(+), 9 deletions(-) create mode 100644 tests/Images/Input/Bmp/BMP_v5_with_ICC_2.bmp diff --git a/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs b/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs index 32025f69fc..247ed78117 100644 --- a/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs +++ b/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs @@ -163,7 +163,7 @@ namespace SixLabors.ImageSharp.Formats.Bmp this.WriteBitmapFileHeader(stream, infoHeaderSize, colorPaletteSize, iccProfileSize, infoHeader, buffer); this.WriteBitmapInfoHeader(stream, infoHeader, buffer, infoHeaderSize); this.WriteImage(stream, image.Frames.RootFrame); - this.WriteColorProfile(stream, metadata, buffer); + this.WriteColorProfile(stream, iccProfileData, buffer); stream.Flush(); } @@ -246,14 +246,15 @@ namespace SixLabors.ImageSharp.Formats.Bmp /// Writes the color profile to the stream. /// /// The stream to write to. - /// The metadata. + /// The color profile data. /// The buffer. - private void WriteColorProfile(Stream stream, ImageMetadata metadata, Span buffer) + private void WriteColorProfile(Stream stream, byte[] iccProfileData, Span buffer) { - if (metadata.IccProfile != null) + if (iccProfileData != null) { - int streamPositionAfterImageData = (int)stream.Position; - stream.Write(metadata.IccProfile.ToByteArray()); + // The offset, in bytes, from the beginning of the BITMAPV5HEADER structure to the start of the profile data. + int streamPositionAfterImageData = (int)stream.Position - BmpFileHeader.Size; + stream.Write(iccProfileData); BinaryPrimitives.WriteInt32LittleEndian(buffer, streamPositionAfterImageData); stream.Position = BmpFileHeader.Size + 112; stream.Write(buffer.Slice(0, 4)); diff --git a/tests/ImageSharp.Tests/Formats/Bmp/BmpDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Bmp/BmpDecoderTests.cs index 43ec45a34f..b42569a232 100644 --- a/tests/ImageSharp.Tests/Formats/Bmp/BmpDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Bmp/BmpDecoderTests.cs @@ -29,10 +29,10 @@ namespace SixLabors.ImageSharp.Tests.Formats.Bmp public static readonly string[] BitfieldsBmpFiles = BitFields; - private static BmpDecoder BmpDecoder => new BmpDecoder(); + private static BmpDecoder BmpDecoder => new(); public static readonly TheoryData RatioFiles = - new TheoryData + new() { { Car, 3780, 3780, PixelResolutionUnit.PixelsPerMeter }, { V5Header, 3780, 3780, PixelResolutionUnit.PixelsPerMeter }, diff --git a/tests/ImageSharp.Tests/Formats/Bmp/BmpEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Bmp/BmpEncoderTests.cs index 073cf5fcf2..2215d5f0a5 100644 --- a/tests/ImageSharp.Tests/Formats/Bmp/BmpEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Bmp/BmpEncoderTests.cs @@ -301,6 +301,33 @@ namespace SixLabors.ImageSharp.Tests.Formats.Bmp public void Encode_PreservesAlpha(TestImageProvider provider, BmpBitsPerPixel bitsPerPixel) where TPixel : unmanaged, IPixel => TestBmpEncoderCore(provider, bitsPerPixel, supportTransparency: true); + [Theory] + [WithFile(IccProfile, PixelTypes.Rgba32)] + public void Encode_PreservesColorProfile(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using (Image input = provider.GetImage(new BmpDecoder())) + { + ImageSharp.Metadata.Profiles.Icc.IccProfile expectedProfile = input.Metadata.IccProfile; + byte[] expectedProfileBytes = expectedProfile.ToByteArray(); + + using (var memStream = new MemoryStream()) + { + input.Save(memStream, new BmpEncoder()); + + memStream.Position = 0; + using (var output = Image.Load(memStream)) + { + ImageSharp.Metadata.Profiles.Icc.IccProfile actualProfile = output.Metadata.IccProfile; + byte[] actualProfileBytes = actualProfile.ToByteArray(); + + Assert.NotNull(actualProfile); + Assert.Equal(expectedProfileBytes, actualProfileBytes); + } + } + } + } + [Theory] [WithFile(Car, PixelTypes.Rgba32, BmpBitsPerPixel.Pixel32)] [WithFile(V5Header, PixelTypes.Rgba32, BmpBitsPerPixel.Pixel32)] diff --git a/tests/ImageSharp.Tests/Formats/Bmp/BmpMetadataTests.cs b/tests/ImageSharp.Tests/Formats/Bmp/BmpMetadataTests.cs index 8931c242ef..6a582e0199 100644 --- a/tests/ImageSharp.Tests/Formats/Bmp/BmpMetadataTests.cs +++ b/tests/ImageSharp.Tests/Formats/Bmp/BmpMetadataTests.cs @@ -3,7 +3,7 @@ using System.IO; using SixLabors.ImageSharp.Formats.Bmp; - +using SixLabors.ImageSharp.PixelFormats; using Xunit; using static SixLabors.ImageSharp.Tests.TestImages.Bmp; @@ -47,5 +47,19 @@ namespace SixLabors.ImageSharp.Tests.Formats.Bmp Assert.Equal(expectedInfoHeaderType, bitmapMetadata.InfoHeaderType); } } + + [Theory] + [WithFile(IccProfile, PixelTypes.Rgba32)] + public void Decoder_CanReadColorProfile(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using (Image image = provider.GetImage(new BmpDecoder())) + { + ImageSharp.Metadata.ImageMetadata metaData = image.Metadata; + Assert.NotNull(metaData); + Assert.NotNull(metaData.IccProfile); + Assert.Equal(16, metaData.IccProfile.Entries.Length); + } + } } } diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs index fa51fb2254..d36cec630d 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/tests/ImageSharp.Tests/TestImages.cs @@ -379,6 +379,7 @@ namespace SixLabors.ImageSharp.Tests public const string Rgb24jpeg = "Bmp/rgb24jpeg.bmp"; public const string Rgb24png = "Bmp/rgb24png.bmp"; public const string Rgba32v4 = "Bmp/rgba32v4.bmp"; + public const string IccProfile = "Bmp/BMP_v5_with_ICC_2.bmp"; // Bitmap images with compression type BITFIELDS. public const string Rgb32bfdef = "Bmp/rgb32bfdef.bmp"; diff --git a/tests/Images/Input/Bmp/BMP_v5_with_ICC_2.bmp b/tests/Images/Input/Bmp/BMP_v5_with_ICC_2.bmp new file mode 100644 index 0000000000..d6328d429f --- /dev/null +++ b/tests/Images/Input/Bmp/BMP_v5_with_ICC_2.bmp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b5b483e9a9d3f3ebdeada2eff70800002c27c046bf971206af0ecc73fa1416e6 +size 27782 From 43c83dabc77d2a72453899783d520b93a378db7b Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Sat, 7 May 2022 14:36:30 +0200 Subject: [PATCH 06/15] Preserve color profile when encoding webp --- .../Formats/Webp/BitWriter/BitWriterBase.cs | 34 ++++++++++++++++++- .../Formats/Webp/BitWriter/Vp8BitWriter.cs | 26 +++++++++++--- .../Formats/Webp/BitWriter/Vp8LBitWriter.cs | 20 +++++++++-- .../Formats/Webp/Lossless/Vp8LEncoder.cs | 2 +- .../Formats/Webp/Lossy/Vp8Encoder.cs | 1 + 5 files changed, 75 insertions(+), 8 deletions(-) diff --git a/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs b/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs index fc1accfdee..3c503f5e0e 100644 --- a/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs +++ b/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs @@ -5,6 +5,7 @@ using System; using System.Buffers.Binary; using System.IO; using SixLabors.ImageSharp.Metadata.Profiles.Exif; +using SixLabors.ImageSharp.Metadata.Profiles.Icc; using SixLabors.ImageSharp.Metadata.Profiles.Xmp; namespace SixLabors.ImageSharp.Formats.Webp.BitWriter @@ -178,16 +179,41 @@ namespace SixLabors.ImageSharp.Formats.Webp.BitWriter } } + /// + /// Writes the color profile to the stream. + /// + /// The stream to write to. + /// The color profile bytes. + protected void WriteColorProfile(Stream stream, byte[] iccProfileBytes) + { + uint size = (uint)iccProfileBytes.Length; + + Span buf = this.scratchBuffer.AsSpan(0, 4); + BinaryPrimitives.WriteUInt32BigEndian(buf, (uint)WebpChunkType.Iccp); + stream.Write(buf); + BinaryPrimitives.WriteUInt32LittleEndian(buf, size); + stream.Write(buf); + + stream.Write(iccProfileBytes); + + // Add padding byte if needed. + if ((size & 1) == 1) + { + stream.WriteByte(0); + } + } + /// /// Writes a VP8X header to the stream. /// /// The stream to write to. /// A exif profile or null, if it does not exist. /// A XMP profile or null, if it does not exist. + /// The color profile. /// The width of the image. /// The height of the image. /// Flag indicating, if a alpha channel is present. - protected void WriteVp8XHeader(Stream stream, ExifProfile exifProfile, XmpProfile xmpProfile, uint width, uint height, bool hasAlpha) + protected void WriteVp8XHeader(Stream stream, ExifProfile exifProfile, XmpProfile xmpProfile, IccProfile iccProfile, uint width, uint height, bool hasAlpha) { if (width > MaxDimension || height > MaxDimension) { @@ -219,6 +245,12 @@ namespace SixLabors.ImageSharp.Formats.Webp.BitWriter flags |= 16; } + if (iccProfile != null) + { + // Set iccp flag. + flags |= 32; + } + Span buf = this.scratchBuffer.AsSpan(0, 4); stream.Write(WebpConstants.Vp8XMagicBytes); BinaryPrimitives.WriteUInt32LittleEndian(buf, WebpConstants.Vp8XChunkSize); diff --git a/src/ImageSharp/Formats/Webp/BitWriter/Vp8BitWriter.cs b/src/ImageSharp/Formats/Webp/BitWriter/Vp8BitWriter.cs index fa6e09d875..cfc024624e 100644 --- a/src/ImageSharp/Formats/Webp/BitWriter/Vp8BitWriter.cs +++ b/src/ImageSharp/Formats/Webp/BitWriter/Vp8BitWriter.cs @@ -6,6 +6,7 @@ using System.Buffers.Binary; using System.IO; using SixLabors.ImageSharp.Formats.Webp.Lossy; using SixLabors.ImageSharp.Metadata.Profiles.Exif; +using SixLabors.ImageSharp.Metadata.Profiles.Icc; using SixLabors.ImageSharp.Metadata.Profiles.Xmp; namespace SixLabors.ImageSharp.Formats.Webp.BitWriter @@ -406,6 +407,7 @@ namespace SixLabors.ImageSharp.Formats.Webp.BitWriter /// The stream to write to. /// The exif profile. /// The XMP profile. + /// The color profile. /// The width of the image. /// The height of the image. /// Flag indicating, if a alpha channel is present. @@ -415,6 +417,7 @@ namespace SixLabors.ImageSharp.Formats.Webp.BitWriter Stream stream, ExifProfile exifProfile, XmpProfile xmpProfile, + IccProfile iccProfile, uint width, uint height, bool hasAlpha, @@ -424,6 +427,7 @@ namespace SixLabors.ImageSharp.Formats.Webp.BitWriter bool isVp8X = false; byte[] exifBytes = null; byte[] xmpBytes = null; + byte[] iccProfileBytes = null; uint riffSize = 0; if (exifProfile != null) { @@ -439,6 +443,13 @@ namespace SixLabors.ImageSharp.Formats.Webp.BitWriter riffSize += this.MetadataChunkSize(xmpBytes); } + if (iccProfile != null) + { + isVp8X = true; + iccProfileBytes = iccProfile.ToByteArray(); + riffSize += this.MetadataChunkSize(iccProfileBytes); + } + if (hasAlpha) { isVp8X = true; @@ -457,7 +468,7 @@ namespace SixLabors.ImageSharp.Formats.Webp.BitWriter var bitWriterPartZero = new Vp8BitWriter(expectedSize); - // Partition #0 with header and partition sizes + // Partition #0 with header and partition sizes. uint size0 = this.GeneratePartition0(bitWriterPartZero); uint vp8Size = WebpConstants.Vp8FrameHeaderSize + size0; @@ -465,12 +476,12 @@ namespace SixLabors.ImageSharp.Formats.Webp.BitWriter uint pad = vp8Size & 1; vp8Size += pad; - // Compute RIFF size + // Compute RIFF size. // At the minimum it is: "WEBPVP8 nnnn" + VP8 data size. riffSize += WebpConstants.TagSize + WebpConstants.ChunkHeaderSize + vp8Size; // Emit headers and partition #0 - this.WriteWebpHeaders(stream, size0, vp8Size, riffSize, isVp8X, width, height, exifProfile, xmpProfile, hasAlpha, alphaData, alphaDataIsCompressed); + this.WriteWebpHeaders(stream, size0, vp8Size, riffSize, isVp8X, width, height, exifProfile, xmpProfile, iccProfile, hasAlpha, alphaData, alphaDataIsCompressed); bitWriterPartZero.WriteToStream(stream); // Write the encoded image to the stream. @@ -668,6 +679,7 @@ namespace SixLabors.ImageSharp.Formats.Webp.BitWriter uint height, ExifProfile exifProfile, XmpProfile xmpProfile, + IccProfile iccProfile, bool hasAlpha, Span alphaData, bool alphaDataIsCompressed) @@ -677,7 +689,13 @@ namespace SixLabors.ImageSharp.Formats.Webp.BitWriter // Write VP8X, header if necessary. if (isVp8X) { - this.WriteVp8XHeader(stream, exifProfile, xmpProfile, width, height, hasAlpha); + this.WriteVp8XHeader(stream, exifProfile, xmpProfile, iccProfile, width, height, hasAlpha); + + if (iccProfile != null) + { + this.WriteColorProfile(stream, iccProfile.ToByteArray()); + } + if (hasAlpha) { this.WriteAlphaChunk(stream, alphaData, alphaDataIsCompressed); diff --git a/src/ImageSharp/Formats/Webp/BitWriter/Vp8LBitWriter.cs b/src/ImageSharp/Formats/Webp/BitWriter/Vp8LBitWriter.cs index d41224f908..60f6b97c89 100644 --- a/src/ImageSharp/Formats/Webp/BitWriter/Vp8LBitWriter.cs +++ b/src/ImageSharp/Formats/Webp/BitWriter/Vp8LBitWriter.cs @@ -6,6 +6,7 @@ using System.Buffers.Binary; using System.IO; using SixLabors.ImageSharp.Formats.Webp.Lossless; using SixLabors.ImageSharp.Metadata.Profiles.Exif; +using SixLabors.ImageSharp.Metadata.Profiles.Icc; using SixLabors.ImageSharp.Metadata.Profiles.Xmp; namespace SixLabors.ImageSharp.Formats.Webp.BitWriter @@ -134,14 +135,16 @@ namespace SixLabors.ImageSharp.Formats.Webp.BitWriter /// The stream to write to. /// The exif profile. /// The XMP profile. + /// The color profile. /// The width of the image. /// The height of the image. /// Flag indicating, if a alpha channel is present. - public void WriteEncodedImageToStream(Stream stream, ExifProfile exifProfile, XmpProfile xmpProfile, uint width, uint height, bool hasAlpha) + public void WriteEncodedImageToStream(Stream stream, ExifProfile exifProfile, XmpProfile xmpProfile, IccProfile iccProfile, uint width, uint height, bool hasAlpha) { bool isVp8X = false; byte[] exifBytes = null; byte[] xmpBytes = null; + byte[] iccBytes = null; uint riffSize = 0; if (exifProfile != null) { @@ -159,6 +162,14 @@ namespace SixLabors.ImageSharp.Formats.Webp.BitWriter riffSize += this.MetadataChunkSize(xmpBytes); } + if (iccProfile != null) + { + isVp8X = true; + riffSize += ExtendedFileChunkSize; + iccBytes = iccProfile.ToByteArray(); + riffSize += this.MetadataChunkSize(iccBytes); + } + this.Finish(); uint size = (uint)this.NumBytes(); size++; // One byte extra for the VP8L signature. @@ -171,7 +182,12 @@ namespace SixLabors.ImageSharp.Formats.Webp.BitWriter // Write VP8X, header if necessary. if (isVp8X) { - this.WriteVp8XHeader(stream, exifProfile, xmpProfile, width, height, hasAlpha); + this.WriteVp8XHeader(stream, exifProfile, xmpProfile, iccProfile, width, height, hasAlpha); + + if (iccBytes != null) + { + this.WriteColorProfile(stream, iccBytes); + } } // Write magic bytes indicating its a lossless webp. diff --git a/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs b/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs index 30d65562ae..8de1ccc420 100644 --- a/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs +++ b/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs @@ -255,7 +255,7 @@ namespace SixLabors.ImageSharp.Formats.Webp.Lossless this.EncodeStream(image); // Write bytes from the bitwriter buffer to the stream. - this.bitWriter.WriteEncodedImageToStream(stream, metadata.ExifProfile, metadata.XmpProfile, (uint)width, (uint)height, hasAlpha); + this.bitWriter.WriteEncodedImageToStream(stream, metadata.ExifProfile, metadata.XmpProfile, metadata.IccProfile, (uint)width, (uint)height, hasAlpha); } /// diff --git a/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs b/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs index 695359e5ea..f24fdb45d8 100644 --- a/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs +++ b/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs @@ -378,6 +378,7 @@ namespace SixLabors.ImageSharp.Formats.Webp.Lossy stream, metadata.ExifProfile, metadata.XmpProfile, + metadata.IccProfile, (uint)width, (uint)height, hasAlpha, From 5bac8b36bfb7909fc229f95d98cdb64c30309db5 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Sat, 7 May 2022 16:15:32 +0200 Subject: [PATCH 07/15] Add test for ICCP --- .../Formats/Webp/BitWriter/BitWriterBase.cs | 8 ++--- .../Formats/Webp/BitWriter/Vp8BitWriter.cs | 10 +++--- .../Formats/Webp/BitWriter/Vp8LBitWriter.cs | 10 +++--- .../Formats/WebP/WebpMetaDataTests.cs | 31 +++++++++++++++++++ 4 files changed, 46 insertions(+), 13 deletions(-) diff --git a/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs b/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs index 3c503f5e0e..0e1c9f4d95 100644 --- a/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs +++ b/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs @@ -98,7 +98,7 @@ namespace SixLabors.ImageSharp.Formats.Webp.BitWriter } /// - /// Calculates the chunk size of EXIF or XMP metadata. + /// Calculates the chunk size of EXIF, XMP or ICCP metadata. /// /// The metadata profile bytes. /// The metadata chunk size in bytes. @@ -209,11 +209,11 @@ namespace SixLabors.ImageSharp.Formats.Webp.BitWriter /// The stream to write to. /// A exif profile or null, if it does not exist. /// A XMP profile or null, if it does not exist. - /// The color profile. + /// The color profile bytes. /// The width of the image. /// The height of the image. /// Flag indicating, if a alpha channel is present. - protected void WriteVp8XHeader(Stream stream, ExifProfile exifProfile, XmpProfile xmpProfile, IccProfile iccProfile, uint width, uint height, bool hasAlpha) + protected void WriteVp8XHeader(Stream stream, ExifProfile exifProfile, XmpProfile xmpProfile, byte[] iccProfileBytes, uint width, uint height, bool hasAlpha) { if (width > MaxDimension || height > MaxDimension) { @@ -245,7 +245,7 @@ namespace SixLabors.ImageSharp.Formats.Webp.BitWriter flags |= 16; } - if (iccProfile != null) + if (iccProfileBytes != null) { // Set iccp flag. flags |= 32; diff --git a/src/ImageSharp/Formats/Webp/BitWriter/Vp8BitWriter.cs b/src/ImageSharp/Formats/Webp/BitWriter/Vp8BitWriter.cs index cfc024624e..7cc915e18f 100644 --- a/src/ImageSharp/Formats/Webp/BitWriter/Vp8BitWriter.cs +++ b/src/ImageSharp/Formats/Webp/BitWriter/Vp8BitWriter.cs @@ -481,7 +481,7 @@ namespace SixLabors.ImageSharp.Formats.Webp.BitWriter riffSize += WebpConstants.TagSize + WebpConstants.ChunkHeaderSize + vp8Size; // Emit headers and partition #0 - this.WriteWebpHeaders(stream, size0, vp8Size, riffSize, isVp8X, width, height, exifProfile, xmpProfile, iccProfile, hasAlpha, alphaData, alphaDataIsCompressed); + this.WriteWebpHeaders(stream, size0, vp8Size, riffSize, isVp8X, width, height, exifProfile, xmpProfile, iccProfileBytes, hasAlpha, alphaData, alphaDataIsCompressed); bitWriterPartZero.WriteToStream(stream); // Write the encoded image to the stream. @@ -679,7 +679,7 @@ namespace SixLabors.ImageSharp.Formats.Webp.BitWriter uint height, ExifProfile exifProfile, XmpProfile xmpProfile, - IccProfile iccProfile, + byte[] iccProfileBytes, bool hasAlpha, Span alphaData, bool alphaDataIsCompressed) @@ -689,11 +689,11 @@ namespace SixLabors.ImageSharp.Formats.Webp.BitWriter // Write VP8X, header if necessary. if (isVp8X) { - this.WriteVp8XHeader(stream, exifProfile, xmpProfile, iccProfile, width, height, hasAlpha); + this.WriteVp8XHeader(stream, exifProfile, xmpProfile, iccProfileBytes, width, height, hasAlpha); - if (iccProfile != null) + if (iccProfileBytes != null) { - this.WriteColorProfile(stream, iccProfile.ToByteArray()); + this.WriteColorProfile(stream, iccProfileBytes); } if (hasAlpha) diff --git a/src/ImageSharp/Formats/Webp/BitWriter/Vp8LBitWriter.cs b/src/ImageSharp/Formats/Webp/BitWriter/Vp8LBitWriter.cs index 60f6b97c89..7bd6febeb6 100644 --- a/src/ImageSharp/Formats/Webp/BitWriter/Vp8LBitWriter.cs +++ b/src/ImageSharp/Formats/Webp/BitWriter/Vp8LBitWriter.cs @@ -149,7 +149,6 @@ namespace SixLabors.ImageSharp.Formats.Webp.BitWriter if (exifProfile != null) { isVp8X = true; - riffSize += ExtendedFileChunkSize; exifBytes = exifProfile.ToByteArray(); riffSize += this.MetadataChunkSize(exifBytes); } @@ -157,7 +156,6 @@ namespace SixLabors.ImageSharp.Formats.Webp.BitWriter if (xmpProfile != null) { isVp8X = true; - riffSize += ExtendedFileChunkSize; xmpBytes = xmpProfile.Data; riffSize += this.MetadataChunkSize(xmpBytes); } @@ -165,11 +163,15 @@ namespace SixLabors.ImageSharp.Formats.Webp.BitWriter if (iccProfile != null) { isVp8X = true; - riffSize += ExtendedFileChunkSize; iccBytes = iccProfile.ToByteArray(); riffSize += this.MetadataChunkSize(iccBytes); } + if (isVp8X) + { + riffSize += ExtendedFileChunkSize; + } + this.Finish(); uint size = (uint)this.NumBytes(); size++; // One byte extra for the VP8L signature. @@ -182,7 +184,7 @@ namespace SixLabors.ImageSharp.Formats.Webp.BitWriter // Write VP8X, header if necessary. if (isVp8X) { - this.WriteVp8XHeader(stream, exifProfile, xmpProfile, iccProfile, width, height, hasAlpha); + this.WriteVp8XHeader(stream, exifProfile, xmpProfile, iccBytes, width, height, hasAlpha); if (iccBytes != null) { diff --git a/tests/ImageSharp.Tests/Formats/WebP/WebpMetaDataTests.cs b/tests/ImageSharp.Tests/Formats/WebP/WebpMetaDataTests.cs index 456b9a3f52..d1f148be02 100644 --- a/tests/ImageSharp.Tests/Formats/WebP/WebpMetaDataTests.cs +++ b/tests/ImageSharp.Tests/Formats/WebP/WebpMetaDataTests.cs @@ -152,6 +152,37 @@ namespace SixLabors.ImageSharp.Tests.Formats.Webp Assert.Equal(expectedExif.Values.Count, actualExif.Values.Count); } + [Theory] + [WithFile(TestImages.Webp.Lossy.WithIccp, PixelTypes.Rgba32, WebpFileFormatType.Lossless)] + [WithFile(TestImages.Webp.Lossy.WithIccp, PixelTypes.Rgba32, WebpFileFormatType.Lossy)] + public void Encode_PreservesColorProfile(TestImageProvider provider, WebpFileFormatType fileFormat) + where TPixel : unmanaged, IPixel + { + using (Image input = provider.GetImage(new WebpDecoder())) + { + ImageSharp.Metadata.Profiles.Icc.IccProfile expectedProfile = input.Metadata.IccProfile; + byte[] expectedProfileBytes = expectedProfile.ToByteArray(); + + using (var memStream = new MemoryStream()) + { + input.Save(memStream, new WebpEncoder() + { + FileFormat = fileFormat + }); + + memStream.Position = 0; + using (var output = Image.Load(memStream)) + { + ImageSharp.Metadata.Profiles.Icc.IccProfile actualProfile = output.Metadata.IccProfile; + byte[] actualProfileBytes = actualProfile.ToByteArray(); + + Assert.NotNull(actualProfile); + Assert.Equal(expectedProfileBytes, actualProfileBytes); + } + } + } + } + [Theory] [WithFile(TestImages.Webp.Lossy.WithExifNotEnoughData, PixelTypes.Rgba32)] public void WebpDecoder_IgnoresInvalidExifChunk(TestImageProvider provider) From 1c474c7758c3a192aa53aef557ba0ff5f9b4f79d Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Sat, 7 May 2022 17:19:31 +0200 Subject: [PATCH 08/15] Add decoding color profile chunk --- src/ImageSharp/Formats/Png/PngDecoderCore.cs | 56 ++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/src/ImageSharp/Formats/Png/PngDecoderCore.cs b/src/ImageSharp/Formats/Png/PngDecoderCore.cs index 12770bc521..3dc6d0bf42 100644 --- a/src/ImageSharp/Formats/Png/PngDecoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngDecoderCore.cs @@ -19,6 +19,7 @@ using SixLabors.ImageSharp.IO; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.Metadata.Profiles.Exif; +using SixLabors.ImageSharp.Metadata.Profiles.Icc; using SixLabors.ImageSharp.Metadata.Profiles.Xmp; using SixLabors.ImageSharp.PixelFormats; @@ -205,6 +206,9 @@ namespace SixLabors.ImageSharp.Formats.Png this.MergeOrSetExifProfile(metadata, new ExifProfile(exifData), replaceExistingKeys: true); } + break; + case PngChunkType.EmbeddedColorProfile: + this.ReadColorProfileChunk(metadata, chunk.Data.GetSpan()); break; case PngChunkType.End: goto EOF; @@ -1174,6 +1178,58 @@ namespace SixLabors.ImageSharp.Formats.Png return true; } + /// + /// Reads the color profile chunk. The data is stored similar to the zTXt chunk. + /// + /// The metadata. + /// The bytes containing the profile. + private void ReadColorProfileChunk(ImageMetadata metadata, ReadOnlySpan data) + { + int zeroIndex = data.IndexOf((byte)0); + if (zeroIndex < PngConstants.MinTextKeywordLength || zeroIndex > PngConstants.MaxTextKeywordLength) + { + return; + } + + byte compressionMethod = data[zeroIndex + 1]; + if (compressionMethod != 0) + { + // Only compression method 0 is supported (zlib datastream with deflate compression). + return; + } + + ReadOnlySpan keywordBytes = data.Slice(0, zeroIndex); + if (!this.TryReadTextKeyword(keywordBytes, out string name)) + { + return; + } + + ReadOnlySpan compressedData = data.Slice(zeroIndex + 2); + + using (var memoryStream = new MemoryStream(compressedData.ToArray())) + using (var bufferedStream = new BufferedReadStream(this.Configuration, memoryStream)) + using (var inflateStream = new ZlibInflateStream(bufferedStream)) + { + if (!inflateStream.AllocateNewBytes(compressedData.Length, false)) + { + return; + } + + var uncompressedBytes = new List(); + + // Note: this uses a buffer which is only 4 bytes long to read the stream, maybe allocating a larger buffer makes sense here. + int bytesRead = inflateStream.CompressedStream.Read(this.buffer, 0, this.buffer.Length); + while (bytesRead != 0) + { + uncompressedBytes.AddRange(this.buffer.AsSpan(0, bytesRead).ToArray()); + bytesRead = inflateStream.CompressedStream.Read(this.buffer, 0, this.buffer.Length); + } + + byte[] iccpProfileBytes = uncompressedBytes.ToArray(); + metadata.IccProfile = new IccProfile(iccpProfileBytes); + } + } + /// /// Compares two ReadOnlySpan<char>s in a case-insensitive method. /// This is only needed because older frameworks are missing the extension method. From 9b5d56f585adb4e032fc41ef37f27097b2526134 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Sat, 7 May 2022 18:53:57 +0200 Subject: [PATCH 09/15] Preserve color profile when encoding png's --- src/ImageSharp/Formats/Png/PngEncoderCore.cs | 70 ++++++++++++++------ 1 file changed, 51 insertions(+), 19 deletions(-) diff --git a/src/ImageSharp/Formats/Png/PngEncoderCore.cs b/src/ImageSharp/Formats/Png/PngEncoderCore.cs index c443c0fcf1..8663915279 100644 --- a/src/ImageSharp/Formats/Png/PngEncoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngEncoderCore.cs @@ -87,6 +87,11 @@ namespace SixLabors.ImageSharp.Formats.Png /// private IMemoryOwner currentScanline; + /// + /// The color profile name. + /// + private const string ProfileName = "ICC Profile"; + /// /// Initializes a new instance of the class. /// @@ -134,6 +139,7 @@ namespace SixLabors.ImageSharp.Formats.Png this.WriteHeaderChunk(stream); this.WriteGammaChunk(stream); + this.WriteColorProfileChunk(stream, metadata); this.WritePaletteChunk(stream, quantized); this.WriteTransparencyChunk(stream, pngMetadata); this.WritePhysicalChunk(stream, metadata); @@ -656,7 +662,7 @@ namespace SixLabors.ImageSharp.Formats.Png } /// - /// Writes an iTXT chunk, containing the XMP metdata to the stream, if such profile is present in the metadata. + /// Writes an iTXT chunk, containing the XMP metadata to the stream, if such profile is present in the metadata. /// /// The containing image data. /// The image metadata. @@ -673,7 +679,7 @@ namespace SixLabors.ImageSharp.Formats.Png return; } - var xmpData = meta.XmpProfile.Data; + byte[] xmpData = meta.XmpProfile.Data; if (xmpData.Length == 0) { @@ -687,19 +693,48 @@ namespace SixLabors.ImageSharp.Formats.Png PngConstants.XmpKeyword.CopyTo(payload); int bytesWritten = PngConstants.XmpKeyword.Length; - // Write the iTxt header (all zeros in this case) + // Write the iTxt header (all zeros in this case). payload[bytesWritten++] = 0; payload[bytesWritten++] = 0; payload[bytesWritten++] = 0; payload[bytesWritten++] = 0; payload[bytesWritten++] = 0; - // And the XMP data itself + // And the XMP data itself. xmpData.CopyTo(payload.Slice(bytesWritten)); this.WriteChunk(stream, PngChunkType.InternationalText, payload); } } + /// + /// Writes the color profile chunk. + /// + /// The stream to write to. + /// The image meta data. + private void WriteColorProfileChunk(Stream stream, ImageMetadata metaData) + { + if (metaData.IccProfile is null) + { + return; + } + + string profileName = "ICC Profile"; + byte[] iccProfileBytes = metaData.IccProfile.ToByteArray(); + + byte[] compressedData = this.GetZlibCompressedBytes(iccProfileBytes); + int payloadLength = profileName.Length + compressedData.Length + 2; + using (IMemoryOwner owner = this.memoryAllocator.Allocate(payloadLength)) + { + Span outputBytes = owner.GetSpan(); + PngConstants.Encoding.GetBytes(profileName).CopyTo(outputBytes); + int bytesWritten = profileName.Length; + outputBytes[bytesWritten++] = 0; // Null separator. + outputBytes[bytesWritten++] = 0; // Compression. + compressedData.CopyTo(outputBytes.Slice(bytesWritten)); + this.WriteChunk(stream, PngChunkType.EmbeddedColorProfile, outputBytes); + } + } + /// /// Writes a text chunk to the stream. Can be either a tTXt, iTXt or zTXt chunk, /// depending whether the text contains any latin characters or should be compressed. @@ -727,13 +762,12 @@ namespace SixLabors.ImageSharp.Formats.Png } } - if (hasUnicodeCharacters || (!string.IsNullOrWhiteSpace(textData.LanguageTag) || - !string.IsNullOrWhiteSpace(textData.TranslatedKeyword))) + if (hasUnicodeCharacters || (!string.IsNullOrWhiteSpace(textData.LanguageTag) || !string.IsNullOrWhiteSpace(textData.TranslatedKeyword))) { // Write iTXt chunk. byte[] keywordBytes = PngConstants.Encoding.GetBytes(textData.Keyword); byte[] textBytes = textData.Value.Length > this.options.TextCompressionThreshold - ? this.GetCompressedTextBytes(PngConstants.TranslatedEncoding.GetBytes(textData.Value)) + ? this.GetZlibCompressedBytes(PngConstants.TranslatedEncoding.GetBytes(textData.Value)) : PngConstants.TranslatedEncoding.GetBytes(textData.Value); byte[] translatedKeyword = PngConstants.TranslatedEncoding.GetBytes(textData.TranslatedKeyword); @@ -772,18 +806,17 @@ namespace SixLabors.ImageSharp.Formats.Png if (textData.Value.Length > this.options.TextCompressionThreshold) { // Write zTXt chunk. - byte[] compressedData = - this.GetCompressedTextBytes(PngConstants.Encoding.GetBytes(textData.Value)); + byte[] compressedData = this.GetZlibCompressedBytes(PngConstants.Encoding.GetBytes(textData.Value)); int payloadLength = textData.Keyword.Length + compressedData.Length + 2; using (IMemoryOwner owner = this.memoryAllocator.Allocate(payloadLength)) { Span outputBytes = owner.GetSpan(); PngConstants.Encoding.GetBytes(textData.Keyword).CopyTo(outputBytes); int bytesWritten = textData.Keyword.Length; - outputBytes[bytesWritten++] = 0; - outputBytes[bytesWritten++] = 0; + outputBytes[bytesWritten++] = 0; // Null separator. + outputBytes[bytesWritten++] = 0; // Compression. compressedData.CopyTo(outputBytes.Slice(bytesWritten)); - this.WriteChunk(stream, PngChunkType.CompressedText, outputBytes.ToArray()); + this.WriteChunk(stream, PngChunkType.CompressedText, outputBytes); } } else @@ -796,9 +829,8 @@ namespace SixLabors.ImageSharp.Formats.Png PngConstants.Encoding.GetBytes(textData.Keyword).CopyTo(outputBytes); int bytesWritten = textData.Keyword.Length; outputBytes[bytesWritten++] = 0; - PngConstants.Encoding.GetBytes(textData.Value) - .CopyTo(outputBytes.Slice(bytesWritten)); - this.WriteChunk(stream, PngChunkType.Text, outputBytes.ToArray()); + PngConstants.Encoding.GetBytes(textData.Value).CopyTo(outputBytes.Slice(bytesWritten)); + this.WriteChunk(stream, PngChunkType.Text, outputBytes); } } } @@ -808,15 +840,15 @@ namespace SixLabors.ImageSharp.Formats.Png /// /// Compresses a given text using Zlib compression. /// - /// The text bytes to compress. - /// The compressed text byte array. - private byte[] GetCompressedTextBytes(byte[] textBytes) + /// The bytes to compress. + /// The compressed byte array. + private byte[] GetZlibCompressedBytes(byte[] dataBytes) { using (var memoryStream = new MemoryStream()) { using (var deflateStream = new ZlibDeflateStream(this.memoryAllocator, memoryStream, this.options.CompressionLevel)) { - deflateStream.Write(textBytes); + deflateStream.Write(dataBytes); } return memoryStream.ToArray(); From d645ba4bf3ff7ba9e26b334897852b92d13311d0 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Sat, 7 May 2022 19:08:31 +0200 Subject: [PATCH 10/15] Add test for ICC profile --- .../Formats/Png/PngEncoderTests.Chunks.cs | 1 + .../Formats/Png/PngMetadataTests.cs | 29 ++++++++++++++++++- 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.Chunks.cs b/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.Chunks.cs index 30a6847029..ede4ec1ccc 100644 --- a/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.Chunks.cs +++ b/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.Chunks.cs @@ -302,6 +302,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Png { PngChunkType.Header, PngChunkType.Gamma, + PngChunkType.EmbeddedColorProfile, PngChunkType.Palette, PngChunkType.InternationalText, PngChunkType.Text, diff --git a/tests/ImageSharp.Tests/Formats/Png/PngMetadataTests.cs b/tests/ImageSharp.Tests/Formats/Png/PngMetadataTests.cs index fd39a828f9..d0ae61fd4c 100644 --- a/tests/ImageSharp.Tests/Formats/Png/PngMetadataTests.cs +++ b/tests/ImageSharp.Tests/Formats/Png/PngMetadataTests.cs @@ -17,7 +17,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Png public class PngMetadataTests { public static readonly TheoryData RatioFiles = - new TheoryData + new() { { TestImages.Png.Splash, 11810, 11810, PixelResolutionUnit.PixelsPerMeter }, { TestImages.Png.Ratio1x4, 1, 4, PixelResolutionUnit.AspectRatio }, @@ -222,6 +222,33 @@ namespace SixLabors.ImageSharp.Tests.Formats.Png } } + [Theory] + [WithFile(TestImages.Png.PngWithMetadata, PixelTypes.Rgba32)] + public void Encode_PreservesColorProfile(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using (Image input = provider.GetImage(new PngDecoder())) + { + ImageSharp.Metadata.Profiles.Icc.IccProfile expectedProfile = input.Metadata.IccProfile; + byte[] expectedProfileBytes = expectedProfile.ToByteArray(); + + using (var memStream = new MemoryStream()) + { + input.Save(memStream, new PngEncoder()); + + memStream.Position = 0; + using (var output = Image.Load(memStream)) + { + ImageSharp.Metadata.Profiles.Icc.IccProfile actualProfile = output.Metadata.IccProfile; + byte[] actualProfileBytes = actualProfile.ToByteArray(); + + Assert.NotNull(actualProfile); + Assert.Equal(expectedProfileBytes, actualProfileBytes); + } + } + } + } + [Theory] [MemberData(nameof(RatioFiles))] public void Identify_VerifyRatio(string imagePath, int xResolution, int yResolution, PixelResolutionUnit resolutionUnit) From 35d1473d7adc7fa16cad589e73da55e294edc35e Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Sat, 7 May 2022 19:11:23 +0200 Subject: [PATCH 11/15] Use const color profile name --- src/ImageSharp/Formats/Png/PngEncoderCore.cs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/ImageSharp/Formats/Png/PngEncoderCore.cs b/src/ImageSharp/Formats/Png/PngEncoderCore.cs index 8663915279..e5b49c41d3 100644 --- a/src/ImageSharp/Formats/Png/PngEncoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngEncoderCore.cs @@ -90,7 +90,7 @@ namespace SixLabors.ImageSharp.Formats.Png /// /// The color profile name. /// - private const string ProfileName = "ICC Profile"; + private const string ColorProfileName = "ICC Profile"; /// /// Initializes a new instance of the class. @@ -718,16 +718,15 @@ namespace SixLabors.ImageSharp.Formats.Png return; } - string profileName = "ICC Profile"; byte[] iccProfileBytes = metaData.IccProfile.ToByteArray(); byte[] compressedData = this.GetZlibCompressedBytes(iccProfileBytes); - int payloadLength = profileName.Length + compressedData.Length + 2; + int payloadLength = ColorProfileName.Length + compressedData.Length + 2; using (IMemoryOwner owner = this.memoryAllocator.Allocate(payloadLength)) { Span outputBytes = owner.GetSpan(); - PngConstants.Encoding.GetBytes(profileName).CopyTo(outputBytes); - int bytesWritten = profileName.Length; + PngConstants.Encoding.GetBytes(ColorProfileName).CopyTo(outputBytes); + int bytesWritten = ColorProfileName.Length; outputBytes[bytesWritten++] = 0; // Null separator. outputBytes[bytesWritten++] = 0; // Compression. compressedData.CopyTo(outputBytes.Slice(bytesWritten)); From b025d29de4e6ba350275f122038f0fdabfcf2b09 Mon Sep 17 00:00:00 2001 From: Brian Popow <38701097+brianpopow@users.noreply.github.com> Date: Tue, 10 May 2022 09:30:34 +0200 Subject: [PATCH 12/15] Apply suggestions from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Günther Foidl --- src/ImageSharp/Formats/Png/PngDecoderCore.cs | 2 +- src/ImageSharp/Formats/Png/PngEncoderCore.cs | 12 +++++++----- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/ImageSharp/Formats/Png/PngDecoderCore.cs b/src/ImageSharp/Formats/Png/PngDecoderCore.cs index 3dc6d0bf42..0745a5f5a5 100644 --- a/src/ImageSharp/Formats/Png/PngDecoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngDecoderCore.cs @@ -1186,7 +1186,7 @@ namespace SixLabors.ImageSharp.Formats.Png private void ReadColorProfileChunk(ImageMetadata metadata, ReadOnlySpan data) { int zeroIndex = data.IndexOf((byte)0); - if (zeroIndex < PngConstants.MinTextKeywordLength || zeroIndex > PngConstants.MaxTextKeywordLength) + if (zeroIndex is < PngConstants.MinTextKeywordLength or > PngConstants.MaxTextKeywordLength) { return; } diff --git a/src/ImageSharp/Formats/Png/PngEncoderCore.cs b/src/ImageSharp/Formats/Png/PngEncoderCore.cs index e5b49c41d3..ad16c80374 100644 --- a/src/ImageSharp/Formats/Png/PngEncoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngEncoderCore.cs @@ -694,11 +694,13 @@ namespace SixLabors.ImageSharp.Formats.Png int bytesWritten = PngConstants.XmpKeyword.Length; // Write the iTxt header (all zeros in this case). - payload[bytesWritten++] = 0; - payload[bytesWritten++] = 0; - payload[bytesWritten++] = 0; - payload[bytesWritten++] = 0; - payload[bytesWritten++] = 0; + Span iTxtHeader = payload.Slice(bytesWritten); + iTxtHeader[4] = 0; + iTxtHeader[3] = 0; + iTxtHeader[2] = 0; + iTxtHeader[1] = 0; + iTxtHeader[0] = 0; + bytesWritten += 5; // And the XMP data itself. xmpData.CopyTo(payload.Slice(bytesWritten)); From 2dd35982e7efd4a52dba1f3f6cd7b1b2b4de96a9 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Tue, 10 May 2022 09:47:09 +0200 Subject: [PATCH 13/15] Avoid allocation, remove code duplication for decompressing zlib data --- src/ImageSharp/Formats/Png/PngDecoderCore.cs | 77 ++++++++++---------- 1 file changed, 40 insertions(+), 37 deletions(-) diff --git a/src/ImageSharp/Formats/Png/PngDecoderCore.cs b/src/ImageSharp/Formats/Png/PngDecoderCore.cs index 0745a5f5a5..470a245b24 100644 --- a/src/ImageSharp/Formats/Png/PngDecoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngDecoderCore.cs @@ -1206,27 +1206,45 @@ namespace SixLabors.ImageSharp.Formats.Png ReadOnlySpan compressedData = data.Slice(zeroIndex + 2); - using (var memoryStream = new MemoryStream(compressedData.ToArray())) - using (var bufferedStream = new BufferedReadStream(this.Configuration, memoryStream)) - using (var inflateStream = new ZlibInflateStream(bufferedStream)) + if (this.TryUncompressZlibData(compressedData, out byte[] iccpProfileBytes)) { - if (!inflateStream.AllocateNewBytes(compressedData.Length, false)) + metadata.IccProfile = new IccProfile(iccpProfileBytes); + } + } + + /// + /// Tries to un-compress zlib compressed data. + /// + /// The compressed data. + /// The uncompressed bytes array. + /// True, if de-compressing was successful. + private unsafe bool TryUncompressZlibData(ReadOnlySpan compressedData, out byte[] uncompressedBytesArray) + { + fixed (byte* compressedDataBase = compressedData) + { + using (var memoryStream = new UnmanagedMemoryStream(compressedDataBase, compressedData.Length)) + using (var bufferedStream = new BufferedReadStream(this.Configuration, memoryStream)) + using (var inflateStream = new ZlibInflateStream(bufferedStream)) { - return; - } + if (!inflateStream.AllocateNewBytes(compressedData.Length, false)) + { + uncompressedBytesArray = Array.Empty(); + return false; + } - var uncompressedBytes = new List(); + var uncompressedBytes = new List(compressedData.Length); - // Note: this uses a buffer which is only 4 bytes long to read the stream, maybe allocating a larger buffer makes sense here. - int bytesRead = inflateStream.CompressedStream.Read(this.buffer, 0, this.buffer.Length); - while (bytesRead != 0) - { - uncompressedBytes.AddRange(this.buffer.AsSpan(0, bytesRead).ToArray()); - bytesRead = inflateStream.CompressedStream.Read(this.buffer, 0, this.buffer.Length); - } + // Note: this uses a buffer which is only 4 bytes long to read the stream, maybe allocating a larger buffer makes sense here. + int bytesRead = inflateStream.CompressedStream.Read(this.buffer, 0, this.buffer.Length); + while (bytesRead != 0) + { + uncompressedBytes.AddRange(this.buffer.AsSpan(0, bytesRead).ToArray()); + bytesRead = inflateStream.CompressedStream.Read(this.buffer, 0, this.buffer.Length); + } - byte[] iccpProfileBytes = uncompressedBytes.ToArray(); - metadata.IccProfile = new IccProfile(iccpProfileBytes); + uncompressedBytesArray = uncompressedBytes.ToArray(); + return true; + } } } @@ -1362,7 +1380,7 @@ namespace SixLabors.ImageSharp.Formats.Png } else if (this.IsXmpTextData(keywordBytes)) { - XmpProfile xmpProfile = new XmpProfile(data.Slice(dataStartIdx).ToArray()); + var xmpProfile = new XmpProfile(data.Slice(dataStartIdx).ToArray()); metadata.XmpProfile = xmpProfile; } else @@ -1381,29 +1399,14 @@ namespace SixLabors.ImageSharp.Formats.Png /// The . private bool TryUncompressTextData(ReadOnlySpan compressedData, Encoding encoding, out string value) { - using (var memoryStream = new MemoryStream(compressedData.ToArray())) - using (var bufferedStream = new BufferedReadStream(this.Configuration, memoryStream)) - using (var inflateStream = new ZlibInflateStream(bufferedStream)) + if (this.TryUncompressZlibData(compressedData, out byte[] uncompressedData)) { - if (!inflateStream.AllocateNewBytes(compressedData.Length, false)) - { - value = null; - return false; - } - - var uncompressedBytes = new List(); - - // Note: this uses a buffer which is only 4 bytes long to read the stream, maybe allocating a larger buffer makes sense here. - int bytesRead = inflateStream.CompressedStream.Read(this.buffer, 0, this.buffer.Length); - while (bytesRead != 0) - { - uncompressedBytes.AddRange(this.buffer.AsSpan(0, bytesRead).ToArray()); - bytesRead = inflateStream.CompressedStream.Read(this.buffer, 0, this.buffer.Length); - } - - value = encoding.GetString(uncompressedBytes.ToArray()); + value = encoding.GetString(uncompressedData); return true; } + + value = null; + return false; } /// From d43ec499e3cdfe1e0d100e12c781360471e9bc69 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Wed, 11 May 2022 14:32:00 +0200 Subject: [PATCH 14/15] Use memory allocator for destination buffer for the uncomressed bytes --- src/ImageSharp/Formats/Png/PngDecoderCore.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/ImageSharp/Formats/Png/PngDecoderCore.cs b/src/ImageSharp/Formats/Png/PngDecoderCore.cs index 470a245b24..16971b5b86 100644 --- a/src/ImageSharp/Formats/Png/PngDecoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngDecoderCore.cs @@ -1222,10 +1222,12 @@ namespace SixLabors.ImageSharp.Formats.Png { fixed (byte* compressedDataBase = compressedData) { + using (IMemoryOwner destBuffer = this.memoryAllocator.Allocate(this.Configuration.StreamProcessingBufferSize)) using (var memoryStream = new UnmanagedMemoryStream(compressedDataBase, compressedData.Length)) using (var bufferedStream = new BufferedReadStream(this.Configuration, memoryStream)) using (var inflateStream = new ZlibInflateStream(bufferedStream)) { + Span dest = destBuffer.GetSpan(); if (!inflateStream.AllocateNewBytes(compressedData.Length, false)) { uncompressedBytesArray = Array.Empty(); @@ -1233,13 +1235,11 @@ namespace SixLabors.ImageSharp.Formats.Png } var uncompressedBytes = new List(compressedData.Length); - - // Note: this uses a buffer which is only 4 bytes long to read the stream, maybe allocating a larger buffer makes sense here. - int bytesRead = inflateStream.CompressedStream.Read(this.buffer, 0, this.buffer.Length); + int bytesRead = inflateStream.CompressedStream.Read(dest, 0, dest.Length); while (bytesRead != 0) { - uncompressedBytes.AddRange(this.buffer.AsSpan(0, bytesRead).ToArray()); - bytesRead = inflateStream.CompressedStream.Read(this.buffer, 0, this.buffer.Length); + uncompressedBytes.AddRange(dest.Slice(0, bytesRead).ToArray()); + bytesRead = inflateStream.CompressedStream.Read(dest, 0, dest.Length); } uncompressedBytesArray = uncompressedBytes.ToArray(); From a0e38c87b01af9c47101947580870f09513ed151 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Wed, 11 May 2022 14:59:50 +0200 Subject: [PATCH 15/15] Use memory stream for uncompressed data instead of a list --- src/ImageSharp/Formats/Png/PngDecoderCore.cs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/ImageSharp/Formats/Png/PngDecoderCore.cs b/src/ImageSharp/Formats/Png/PngDecoderCore.cs index 16971b5b86..f46b5058a1 100644 --- a/src/ImageSharp/Formats/Png/PngDecoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngDecoderCore.cs @@ -1223,26 +1223,26 @@ namespace SixLabors.ImageSharp.Formats.Png fixed (byte* compressedDataBase = compressedData) { using (IMemoryOwner destBuffer = this.memoryAllocator.Allocate(this.Configuration.StreamProcessingBufferSize)) - using (var memoryStream = new UnmanagedMemoryStream(compressedDataBase, compressedData.Length)) - using (var bufferedStream = new BufferedReadStream(this.Configuration, memoryStream)) + using (var memoryStreamOutput = new MemoryStream(compressedData.Length)) + using (var memoryStreamInput = new UnmanagedMemoryStream(compressedDataBase, compressedData.Length)) + using (var bufferedStream = new BufferedReadStream(this.Configuration, memoryStreamInput)) using (var inflateStream = new ZlibInflateStream(bufferedStream)) { - Span dest = destBuffer.GetSpan(); + Span destUncompressedData = destBuffer.GetSpan(); if (!inflateStream.AllocateNewBytes(compressedData.Length, false)) { uncompressedBytesArray = Array.Empty(); return false; } - var uncompressedBytes = new List(compressedData.Length); - int bytesRead = inflateStream.CompressedStream.Read(dest, 0, dest.Length); + int bytesRead = inflateStream.CompressedStream.Read(destUncompressedData, 0, destUncompressedData.Length); while (bytesRead != 0) { - uncompressedBytes.AddRange(dest.Slice(0, bytesRead).ToArray()); - bytesRead = inflateStream.CompressedStream.Read(dest, 0, dest.Length); + memoryStreamOutput.Write(destUncompressedData.Slice(0, bytesRead)); + bytesRead = inflateStream.CompressedStream.Read(destUncompressedData, 0, destUncompressedData.Length); } - uncompressedBytesArray = uncompressedBytes.ToArray(); + uncompressedBytesArray = memoryStreamOutput.ToArray(); return true; } }