From 16aef1757b379fdf840e450a5397b80cc26a30ab Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Sun, 6 Oct 2019 12:23:39 +0200 Subject: [PATCH 01/75] Add decoding of 24 bit targa files --- src/ImageSharp/Configuration.cs | 7 +- .../Formats/Tga/ITgaDecoderOptions.cs | 12 ++ .../Formats/Tga/TgaConfigurationModule.cs | 18 +++ src/ImageSharp/Formats/Tga/TgaConstants.cs | 20 +++ src/ImageSharp/Formats/Tga/TgaDecoder.cs | 34 +++++ src/ImageSharp/Formats/Tga/TgaDecoderCore.cs | 125 ++++++++++++++++ src/ImageSharp/Formats/Tga/TgaFileHeader.cs | 139 ++++++++++++++++++ src/ImageSharp/Formats/Tga/TgaFormat.cs | 33 +++++ .../Formats/Tga/TgaImageFormatDetector.cs | 27 ++++ src/ImageSharp/Formats/Tga/TgaImageType.cs | 48 ++++++ src/ImageSharp/Formats/Tga/TgaMetadata.cs | 21 +++ 11 files changed, 482 insertions(+), 2 deletions(-) create mode 100644 src/ImageSharp/Formats/Tga/ITgaDecoderOptions.cs create mode 100644 src/ImageSharp/Formats/Tga/TgaConfigurationModule.cs create mode 100644 src/ImageSharp/Formats/Tga/TgaConstants.cs create mode 100644 src/ImageSharp/Formats/Tga/TgaDecoder.cs create mode 100644 src/ImageSharp/Formats/Tga/TgaDecoderCore.cs create mode 100644 src/ImageSharp/Formats/Tga/TgaFileHeader.cs create mode 100644 src/ImageSharp/Formats/Tga/TgaFormat.cs create mode 100644 src/ImageSharp/Formats/Tga/TgaImageFormatDetector.cs create mode 100644 src/ImageSharp/Formats/Tga/TgaImageType.cs create mode 100644 src/ImageSharp/Formats/Tga/TgaMetadata.cs diff --git a/src/ImageSharp/Configuration.cs b/src/ImageSharp/Configuration.cs index 0d44db8d87..e385fba5e0 100644 --- a/src/ImageSharp/Configuration.cs +++ b/src/ImageSharp/Configuration.cs @@ -8,6 +8,7 @@ using SixLabors.ImageSharp.Formats.Bmp; using SixLabors.ImageSharp.Formats.Gif; using SixLabors.ImageSharp.Formats.Jpeg; using SixLabors.ImageSharp.Formats.Png; +using SixLabors.ImageSharp.Formats.Tga; using SixLabors.ImageSharp.IO; using SixLabors.ImageSharp.Processing; using SixLabors.Memory; @@ -150,6 +151,7 @@ namespace SixLabors.ImageSharp /// /// /// . + /// . /// /// The default configuration of . internal static Configuration CreateDefaultInstance() @@ -158,7 +160,8 @@ namespace SixLabors.ImageSharp new PngConfigurationModule(), new JpegConfigurationModule(), new GifConfigurationModule(), - new BmpConfigurationModule()); + new BmpConfigurationModule(), + new TgaConfigurationModule()); } } -} \ No newline at end of file +} diff --git a/src/ImageSharp/Formats/Tga/ITgaDecoderOptions.cs b/src/ImageSharp/Formats/Tga/ITgaDecoderOptions.cs new file mode 100644 index 0000000000..e99e8b0c8d --- /dev/null +++ b/src/ImageSharp/Formats/Tga/ITgaDecoderOptions.cs @@ -0,0 +1,12 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +namespace SixLabors.ImageSharp.Formats.Tga +{ + /// + /// The options for decoding tga images. Currently empty, but this may change in the future. + /// + internal interface ITgaDecoderOptions + { + } +} diff --git a/src/ImageSharp/Formats/Tga/TgaConfigurationModule.cs b/src/ImageSharp/Formats/Tga/TgaConfigurationModule.cs new file mode 100644 index 0000000000..c7bf6cc93d --- /dev/null +++ b/src/ImageSharp/Formats/Tga/TgaConfigurationModule.cs @@ -0,0 +1,18 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +namespace SixLabors.ImageSharp.Formats.Tga +{ + /// + /// Registers the image encoders, decoders and mime type detectors for the tga format. + /// + public sealed class TgaConfigurationModule : IConfigurationModule + { + /// + public void Configure(Configuration configuration) + { + configuration.ImageFormatsManager.SetDecoder(TgaFormat.Instance, new TgaDecoder()); + configuration.ImageFormatsManager.AddImageFormatDetector(new TgaImageFormatDetector()); + } + } +} diff --git a/src/ImageSharp/Formats/Tga/TgaConstants.cs b/src/ImageSharp/Formats/Tga/TgaConstants.cs new file mode 100644 index 0000000000..88c98b06a9 --- /dev/null +++ b/src/ImageSharp/Formats/Tga/TgaConstants.cs @@ -0,0 +1,20 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System.Collections.Generic; + +namespace SixLabors.ImageSharp.Formats.Tga +{ + internal static class TgaConstants + { + /// + /// The list of mimetypes that equate to a targa file. + /// + public static readonly IEnumerable MimeTypes = new[] { "image/x-tga", "image/x-targa" }; + + /// + /// The list of file extensions that equate to a targa file. + /// + public static readonly IEnumerable FileExtensions = new[] { "tga", "vda", "icb", "vst" }; + } +} diff --git a/src/ImageSharp/Formats/Tga/TgaDecoder.cs b/src/ImageSharp/Formats/Tga/TgaDecoder.cs new file mode 100644 index 0000000000..b97388773a --- /dev/null +++ b/src/ImageSharp/Formats/Tga/TgaDecoder.cs @@ -0,0 +1,34 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System.IO; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Tga +{ + /// + /// Image decoder for Truevision TGA images. + /// + public sealed class TgaDecoder : IImageDecoder, ITgaDecoderOptions, IImageInfoDetector + { + /// + public Image Decode(Configuration configuration, Stream stream) + where TPixel : struct, IPixel + { + Guard.NotNull(stream, nameof(stream)); + + return new TgaDecoderCore(configuration, this).Decode(stream); + } + + /// + public Image Decode(Configuration configuration, Stream stream) => this.Decode(configuration, stream); + + /// + public IImageInfo Identify(Configuration configuration, Stream stream) + { + Guard.NotNull(stream, nameof(stream)); + + return new TgaDecoderCore(configuration, this).Identify(stream); + } + } +} diff --git a/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs b/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs new file mode 100644 index 0000000000..b8068c0825 --- /dev/null +++ b/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs @@ -0,0 +1,125 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.IO; + +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.Metadata; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.Memory; + +namespace SixLabors.ImageSharp.Formats.Tga +{ + internal sealed class TgaDecoderCore + { + /// + /// The metadata. + /// + private ImageMetadata metadata; + + /// + /// The file header containing general information about the image. + /// + private TgaFileHeader fileHeader; + + /// + /// The global configuration. + /// + private readonly Configuration configuration; + + /// + /// Used for allocating memory during processing operations. + /// + private readonly MemoryAllocator memoryAllocator; + + /// + /// The stream to decode from. + /// + private Stream currentStream; + + /// + /// The bitmap decoder options. + /// + private readonly ITgaDecoderOptions options; + + public TgaDecoderCore(Configuration configuration, ITgaDecoderOptions options) + { + this.configuration = configuration; + this.memoryAllocator = configuration.MemoryAllocator; + this.options = options; + } + + /// + /// Decodes the image from the specified stream. + /// + /// The pixel format. + /// The stream, where the image should be decoded from. Cannot be null. + /// + /// is null. + /// + /// The decoded image. + public Image Decode(Stream stream) + where TPixel : struct, IPixel + { + try + { + this.ReadFileHeader(stream); + + var image = new Image(this.configuration, this.fileHeader.Width, this.fileHeader.Height, this.metadata); + Buffer2D pixels = image.GetRootFramePixelBuffer(); + + using (IManagedByteBuffer row = this.memoryAllocator.AllocatePaddedPixelRowBuffer(this.fileHeader.Width, 3, 0)) + { + for (int y = 0; y < this.fileHeader.Height; y++) + { + this.currentStream.Read(row); + Span pixelSpan = pixels.GetRowSpan(y); + PixelOperations.Instance.FromBgr24Bytes( + this.configuration, + row.GetSpan(), + pixelSpan, + this.fileHeader.Width); + } + } + + return image; + } + catch (Exception e) + { + throw new ImageFormatException("TGA image does not have a valid format.", e); + } + } + + /// + /// Reads the raw image information from the specified stream. + /// + /// The containing image data. + public IImageInfo Identify(Stream stream) + { + this.ReadFileHeader(stream); + return new ImageInfo( + new PixelTypeInfo(this.fileHeader.PixelDepth), + this.fileHeader.Width, + this.fileHeader.Height, + this.metadata); + } + + /// + /// Reads the tga file header from the stream. + /// + /// The containing image data. + private void ReadFileHeader(Stream stream) + { + this.currentStream = stream; + +#if NETCOREAPP2_1 + Span buffer = stackalloc byte[TgaFileHeader.Size]; +#else + var buffer = new byte[TgaFileHeader.Size]; +#endif + this.currentStream.Read(buffer, 0, TgaFileHeader.Size); + this.fileHeader = TgaFileHeader.Parse(buffer); + } + } +} diff --git a/src/ImageSharp/Formats/Tga/TgaFileHeader.cs b/src/ImageSharp/Formats/Tga/TgaFileHeader.cs new file mode 100644 index 0000000000..51390e1b73 --- /dev/null +++ b/src/ImageSharp/Formats/Tga/TgaFileHeader.cs @@ -0,0 +1,139 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Runtime.InteropServices; + +namespace SixLabors.ImageSharp.Formats.Tga +{ + /// + /// This block of bytes tells the application detailed information about the targa image. + /// + /// + [StructLayout(LayoutKind.Sequential, Pack = 1)] + internal readonly struct TgaFileHeader + { + /// + /// Defines the size of the data structure in the targa file. + /// + public const int Size = 18; + + public TgaFileHeader( + byte idLength, + byte colorMapType, + TgaImageType imageType, + short cMapStart, + short cMapLength, + byte cMapDepth, + short xOffset, + short yOffset, + short width, + short height, + byte pixelDepth, + byte imageDescriptor) + { + this.IdLength = idLength; + this.ColorMapType = colorMapType; + this.ImageType = imageType; + this.CMapStart = cMapStart; + this.CMapLength = cMapLength; + this.CMapDepth = cMapDepth; + this.XOffset = xOffset; + this.YOffset = yOffset; + this.Width = width; + this.Height = height; + this.PixelDepth = pixelDepth; + this.ImageDescriptor = imageDescriptor; + } + + /// + /// Gets the id length. + /// This field identifies the number of bytes contained in Field 6, the Image ID Field. The maximum number + /// of characters is 255. A value of zero indicates that no Image ID field is included with the image. + /// + public byte IdLength { get; } + + /// + /// Gets the color map type. + /// This field indicates the type of color map (if any) included with the image. There are currently 2 defined + /// values for this field: + /// 0 - indicates that no color-map data is included with this image. + /// 1 - indicates that a color-map is included with this image. + /// + public byte ColorMapType { get; } + + /// + /// Gets the image type. + /// The TGA File Format can be used to store Pseudo-Color, True-Color and Direct-Color images of various + /// pixel depths. + /// + public TgaImageType ImageType { get; } + + /// + /// Gets the start of the color map. + /// This field and its sub-fields describe the color map (if any) used for the image. If the Color Map Type field + /// is set to zero, indicating that no color map exists, then these 5 bytes should be set to zero. + /// + public short CMapStart { get; } + + /// + /// Gets the total number of color map entries included. + /// + public short CMapLength { get; } + + /// + /// Gets the number of bits per entry. Typically 15, 16, 24 or 32-bit values are used. + /// + public byte CMapDepth { get; } + + /// + /// Gets the XOffset. + /// These bytes specify the absolute horizontal coordinate for the lower left + /// corner of the image as it is positioned on a display device having an + /// origin at the lower left of the screen. + /// + public short XOffset { get; } + + /// + /// Gets the YOffset. + /// These bytes specify the absolute vertical coordinate for the lower left + /// corner of the image as it is positioned on a display device having an + /// origin at the lower left of the screen. + /// + public short YOffset { get; } + + /// + /// Gets the width of the image in pixels. + /// + public short Width { get; } + + /// + /// Gets the height of the image in pixels. + /// + public short Height { get; } + + /// + /// Gets the number of bits per pixel. This number includes + /// the Attribute or Alpha channel bits. Common values are 8, 16, 24 and + /// 32 but other pixel depths could be used. + /// + public byte PixelDepth { get; } + + /// + /// Gets the ImageDescriptor. + /// ImageDescriptor contains two pieces of information. + /// Bits 0 through 3 contain the number of attribute bits per pixel. + /// Attribute bits are found only in pixels for the 16- and 32-bit flavors of the TGA format and are called alpha channel, + /// overlay, or interrupt bits. Bits 4 and 5 contain the image origin location (coordinate 0,0) of the image. + /// This position may be any of the four corners of the display screen. + /// When both of these bits are set to zero, the image origin is the lower-left corner of the screen. + /// Bits 6 and 7 of the ImageDescriptor field are unused and should be set to 0. + /// + public byte ImageDescriptor { get; } + + public static TgaFileHeader Parse(Span data) + { + return MemoryMarshal.Cast(data)[0]; + } + } +} diff --git a/src/ImageSharp/Formats/Tga/TgaFormat.cs b/src/ImageSharp/Formats/Tga/TgaFormat.cs new file mode 100644 index 0000000000..badb1d77a4 --- /dev/null +++ b/src/ImageSharp/Formats/Tga/TgaFormat.cs @@ -0,0 +1,33 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System.Collections.Generic; + +namespace SixLabors.ImageSharp.Formats.Tga +{ + /// + /// Registers the image encoders, decoders and mime type detectors for the tga format. + /// + public sealed class TgaFormat : IImageFormat + { + /// + /// Gets the current instance. + /// + public static TgaFormat Instance { get; } = new TgaFormat(); + + /// + public string Name => "TGA"; + + /// + public string DefaultMimeType => "image/tga"; + + /// + public IEnumerable MimeTypes => TgaConstants.MimeTypes; + + /// + public IEnumerable FileExtensions => TgaConstants.FileExtensions; + + /// + public TgaMetadata CreateDefaultFormatMetadata() => new TgaMetadata(); + } +} diff --git a/src/ImageSharp/Formats/Tga/TgaImageFormatDetector.cs b/src/ImageSharp/Formats/Tga/TgaImageFormatDetector.cs new file mode 100644 index 0000000000..e305728473 --- /dev/null +++ b/src/ImageSharp/Formats/Tga/TgaImageFormatDetector.cs @@ -0,0 +1,27 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System; + +namespace SixLabors.ImageSharp.Formats.Tga +{ + /// + /// Detects tga file headers. + /// + public sealed class TgaImageFormatDetector : IImageFormatDetector + { + /// + public int HeaderSize => 18; + + /// + public IImageFormat DetectFormat(ReadOnlySpan header) + { + return this.IsSupportedFileFormat(header) ? TgaFormat.Instance : null; + } + + private bool IsSupportedFileFormat(ReadOnlySpan header) + { + return header.Length >= this.HeaderSize; + } + } +} diff --git a/src/ImageSharp/Formats/Tga/TgaImageType.cs b/src/ImageSharp/Formats/Tga/TgaImageType.cs new file mode 100644 index 0000000000..d8140d5c6e --- /dev/null +++ b/src/ImageSharp/Formats/Tga/TgaImageType.cs @@ -0,0 +1,48 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +namespace SixLabors.ImageSharp.Formats.Tga +{ + /// + /// Defines the tga image type. The TGA File Format can be used to store Pseudo-Color, + /// True-Color and Direct-Color images of various pixel depths. + /// + public enum TgaImageType : byte + { + /// + /// No image data included. + /// Not sure what this is used for. + /// + NoImageData = 0, + + /// + /// Uncompressed, color mapped image. + /// + ColorMapped = 1, + + /// + /// Uncompressed true color image. + /// + TrueColor = 2, + + /// + /// Uncompressed Black and white (grayscale) image. + /// + BlackAndWhite = 3, + + /// + /// Run length encoded, color mapped image. + /// + RleColorMapped = 9, + + /// + /// Run length encoded, true color image. + /// + RleTrueColor = 10, + + /// + /// Run length encoded, black and white (grayscale) image. + /// + RleBlackAndWhite = 11, + } +} diff --git a/src/ImageSharp/Formats/Tga/TgaMetadata.cs b/src/ImageSharp/Formats/Tga/TgaMetadata.cs new file mode 100644 index 0000000000..185eaedc9a --- /dev/null +++ b/src/ImageSharp/Formats/Tga/TgaMetadata.cs @@ -0,0 +1,21 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +namespace SixLabors.ImageSharp.Formats.Tga +{ + /// + /// Provides TGA specific metadata information for the image. + /// + public class TgaMetadata : IDeepCloneable + { + /// + /// Initializes a new instance of the class. + /// + public TgaMetadata() + { + } + + /// + public IDeepCloneable DeepClone() => throw new System.NotImplementedException(); + } +} From 38589a0eb825c89c8b589294cce17b571341f2a1 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Sun, 6 Oct 2019 16:32:32 +0200 Subject: [PATCH 02/75] Add support for decoding 8, 16, 32 bit tga files --- src/ImageSharp/Formats/Tga/TgaDecoderCore.cs | 105 ++++++++++++++++--- src/ImageSharp/Formats/Tga/TgaThrowHelper.cs | 21 ++++ 2 files changed, 114 insertions(+), 12 deletions(-) create mode 100644 src/ImageSharp/Formats/Tga/TgaThrowHelper.cs diff --git a/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs b/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs index b8068c0825..a73f04bcdb 100644 --- a/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs +++ b/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs @@ -69,28 +69,109 @@ namespace SixLabors.ImageSharp.Formats.Tga var image = new Image(this.configuration, this.fileHeader.Width, this.fileHeader.Height, this.metadata); Buffer2D pixels = image.GetRootFramePixelBuffer(); - using (IManagedByteBuffer row = this.memoryAllocator.AllocatePaddedPixelRowBuffer(this.fileHeader.Width, 3, 0)) + switch (this.fileHeader.PixelDepth) { - for (int y = 0; y < this.fileHeader.Height; y++) - { - this.currentStream.Read(row); - Span pixelSpan = pixels.GetRowSpan(y); - PixelOperations.Instance.FromBgr24Bytes( - this.configuration, - row.GetSpan(), - pixelSpan, - this.fileHeader.Width); - } + case 8: + this.ReadMonoChrome(pixels); + break; + + case 16: + this.ReadBgra16(pixels); + break; + + case 24: + this.ReadBgr24(pixels); + break; + + case 32: + this.ReadBgra32(pixels); + break; + + default: + TgaThrowHelper.ThrowNotSupportedException("Does not support this kind of tga files."); + break; } return image; } - catch (Exception e) + catch (IndexOutOfRangeException e) { throw new ImageFormatException("TGA image does not have a valid format.", e); } } + private void ReadMonoChrome(Buffer2D pixels) + where TPixel : struct, IPixel + { + using (IManagedByteBuffer row = this.memoryAllocator.AllocatePaddedPixelRowBuffer(this.fileHeader.Width, 1, 0)) + { + for (int y = 0; y < this.fileHeader.Height; y++) + { + this.currentStream.Read(row); + Span pixelSpan = pixels.GetRowSpan(y); + PixelOperations.Instance.FromGray8Bytes( + this.configuration, + row.GetSpan(), + pixelSpan, + this.fileHeader.Width); + } + } + } + + private void ReadBgra16(Buffer2D pixels) + where TPixel : struct, IPixel + { + using (IManagedByteBuffer row = this.memoryAllocator.AllocatePaddedPixelRowBuffer(this.fileHeader.Width, 2, 0)) + { + for (int y = 0; y < this.fileHeader.Height; y++) + { + this.currentStream.Read(row); + Span pixelSpan = pixels.GetRowSpan(y); + PixelOperations.Instance.FromBgra5551Bytes( + this.configuration, + row.GetSpan(), + pixelSpan, + this.fileHeader.Width); + } + } + } + + private void ReadBgr24(Buffer2D pixels) + where TPixel : struct, IPixel + { + using (IManagedByteBuffer row = this.memoryAllocator.AllocatePaddedPixelRowBuffer(this.fileHeader.Width, 3, 0)) + { + for (int y = 0; y < this.fileHeader.Height; y++) + { + this.currentStream.Read(row); + Span pixelSpan = pixels.GetRowSpan(y); + PixelOperations.Instance.FromBgr24Bytes( + this.configuration, + row.GetSpan(), + pixelSpan, + this.fileHeader.Width); + } + } + } + + private void ReadBgra32(Buffer2D pixels) + where TPixel : struct, IPixel + { + using (IManagedByteBuffer row = this.memoryAllocator.AllocatePaddedPixelRowBuffer(this.fileHeader.Width, 4, 0)) + { + for (int y = 0; y < this.fileHeader.Height; y++) + { + this.currentStream.Read(row); + Span pixelSpan = pixels.GetRowSpan(y); + PixelOperations.Instance.FromBgra32Bytes( + this.configuration, + row.GetSpan(), + pixelSpan, + this.fileHeader.Width); + } + } + } + /// /// Reads the raw image information from the specified stream. /// diff --git a/src/ImageSharp/Formats/Tga/TgaThrowHelper.cs b/src/ImageSharp/Formats/Tga/TgaThrowHelper.cs new file mode 100644 index 0000000000..9e36b20539 --- /dev/null +++ b/src/ImageSharp/Formats/Tga/TgaThrowHelper.cs @@ -0,0 +1,21 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Runtime.CompilerServices; + +namespace SixLabors.ImageSharp.Formats.Tga +{ + internal static class TgaThrowHelper + { + /// + /// 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); + } + } +} From 3b48dc39dcff1d424e94b34bbc842c5fb779bec8 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Sun, 6 Oct 2019 18:08:30 +0200 Subject: [PATCH 03/75] Add tga decoder tests --- .gitattributes | 1 + src/ImageSharp/Formats/Tga/TgaDecoderCore.cs | 12 +- .../PixelOperations{TPixel}.Generated.tt | 4 +- .../Formats/Tga/TgaDecoderTests.cs | 107 ++++++++++++++++++ .../ImageSharp.Tests/ImageSharp.Tests.csproj | 2 +- tests/ImageSharp.Tests/TestImages.cs | 8 ++ .../TestUtilities/TestEnvironment.Formats.cs | 6 +- tests/Images/Input/Tga/bike_16bit.tga | 3 + tests/Images/Input/Tga/bike_24bit.tga | 3 + tests/Images/Input/Tga/bike_32bit.tga | 3 + tests/Images/Input/Tga/bike_8bit.tga | 3 + 11 files changed, 143 insertions(+), 9 deletions(-) create mode 100644 tests/ImageSharp.Tests/Formats/Tga/TgaDecoderTests.cs create mode 100644 tests/Images/Input/Tga/bike_16bit.tga create mode 100644 tests/Images/Input/Tga/bike_24bit.tga create mode 100644 tests/Images/Input/Tga/bike_32bit.tga create mode 100644 tests/Images/Input/Tga/bike_8bit.tga diff --git a/.gitattributes b/.gitattributes index b9a9ddd4c3..195506770b 100644 --- a/.gitattributes +++ b/.gitattributes @@ -68,6 +68,7 @@ *.gif binary *.jpg binary *.png binary +*.tga binary *.ttf binary *.snk binary # diff as plain text diff --git a/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs b/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs index a73f04bcdb..abad2c5e77 100644 --- a/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs +++ b/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs @@ -3,6 +3,7 @@ using System; using System.IO; +using System.Runtime.CompilerServices; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Metadata; @@ -108,7 +109,7 @@ namespace SixLabors.ImageSharp.Formats.Tga for (int y = 0; y < this.fileHeader.Height; y++) { this.currentStream.Read(row); - Span pixelSpan = pixels.GetRowSpan(y); + Span pixelSpan = pixels.GetRowSpan(this.fileHeader.Height - y - 1); PixelOperations.Instance.FromGray8Bytes( this.configuration, row.GetSpan(), @@ -126,7 +127,7 @@ namespace SixLabors.ImageSharp.Formats.Tga for (int y = 0; y < this.fileHeader.Height; y++) { this.currentStream.Read(row); - Span pixelSpan = pixels.GetRowSpan(y); + Span pixelSpan = pixels.GetRowSpan(this.fileHeader.Height - y - 1); PixelOperations.Instance.FromBgra5551Bytes( this.configuration, row.GetSpan(), @@ -144,7 +145,7 @@ namespace SixLabors.ImageSharp.Formats.Tga for (int y = 0; y < this.fileHeader.Height; y++) { this.currentStream.Read(row); - Span pixelSpan = pixels.GetRowSpan(y); + Span pixelSpan = pixels.GetRowSpan(this.fileHeader.Height - y - 1); PixelOperations.Instance.FromBgr24Bytes( this.configuration, row.GetSpan(), @@ -162,7 +163,7 @@ namespace SixLabors.ImageSharp.Formats.Tga for (int y = 0; y < this.fileHeader.Height; y++) { this.currentStream.Read(row); - Span pixelSpan = pixels.GetRowSpan(y); + Span pixelSpan = pixels.GetRowSpan(this.fileHeader.Height - y - 1); PixelOperations.Instance.FromBgra32Bytes( this.configuration, row.GetSpan(), @@ -201,6 +202,9 @@ namespace SixLabors.ImageSharp.Formats.Tga #endif this.currentStream.Read(buffer, 0, TgaFileHeader.Size); this.fileHeader = TgaFileHeader.Parse(buffer); + + // TODO: no meta data yet. + this.metadata = new ImageMetadata(); } } } diff --git a/src/ImageSharp/PixelFormats/PixelOperations{TPixel}.Generated.tt b/src/ImageSharp/PixelFormats/PixelOperations{TPixel}.Generated.tt index 8603012321..459924c318 100644 --- a/src/ImageSharp/PixelFormats/PixelOperations{TPixel}.Generated.tt +++ b/src/ImageSharp/PixelFormats/PixelOperations{TPixel}.Generated.tt @@ -18,7 +18,7 @@ /// /// Converts all pixels in 'source` span of into a span of -s. /// - /// A to configure internal operations + /// A to configure internal operations. /// The source of data. /// The to the destination pixels. internal virtual void From<#=pixelType#>(Configuration configuration, ReadOnlySpan<<#=pixelType#>> source, Span destPixels) @@ -41,7 +41,7 @@ /// A helper for that expects a byte span. /// The layout of the data in 'sourceBytes' must be compatible with layout. /// - /// A to configure internal operations + /// A to configure internal operations. /// The to the source bytes. /// The to the destination pixels. /// The number of pixels to convert. diff --git a/tests/ImageSharp.Tests/Formats/Tga/TgaDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Tga/TgaDecoderTests.cs new file mode 100644 index 0000000000..d998ac292b --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Tga/TgaDecoderTests.cs @@ -0,0 +1,107 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.IO; + +using ImageMagick; + +using SixLabors.ImageSharp.Advanced; +using SixLabors.ImageSharp.Formats.Tga; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison; + +using Xunit; + +namespace SixLabors.ImageSharp.Tests.Formats.Tga +{ + using static TestImages.Tga; + + public class TgaDecoderTests + { + [Theory] + [WithFile(Grey, PixelTypes.Rgba32)] + public void TgaDecoder_CanDecode_Uncompressed_MonoChrome(TestImageProvider provider) + where TPixel : struct, IPixel + { + using (Image image = provider.GetImage(new TgaDecoder())) + { + image.DebugSave(provider); + CompareWithReferenceDecoder(provider, image); + } + } + + [Theory] + [WithFile(Bit16, PixelTypes.Rgba32)] + public void TgaDecoder_CanDecode_Uncompressed_16Bit(TestImageProvider provider) + where TPixel : struct, IPixel + { + using (Image image = provider.GetImage(new TgaDecoder())) + { + image.DebugSave(provider); + CompareWithReferenceDecoder(provider, image); + } + } + + [Theory] + [WithFile(Bit24, PixelTypes.Rgba32)] + public void TgaDecoder_CanDecode_Uncompressed_24Bit(TestImageProvider provider) + where TPixel : struct, IPixel + { + using (Image image = provider.GetImage(new TgaDecoder())) + { + image.DebugSave(provider); + CompareWithReferenceDecoder(provider, image); + } + } + + [Theory] + [WithFile(Bit32, PixelTypes.Rgba32)] + public void TgaDecoder_CanDecode_Uncompressed_32Bit(TestImageProvider provider) + where TPixel : struct, IPixel + { + using (Image image = provider.GetImage(new TgaDecoder())) + { + image.DebugSave(provider); + CompareWithReferenceDecoder(provider, image); + } + } + + private void CompareWithReferenceDecoder(TestImageProvider provider, Image image) + where TPixel : struct, IPixel + { + string path = TestImageProvider.GetFilePathOrNull(provider); + if (path == null) + { + throw new InvalidOperationException("CompareToOriginal() works only with file providers!"); + } + + TestFile testFile = TestFile.Create(path); + Image magickImage = this.DecodeWithMagick(Configuration.Default, new FileInfo(testFile.FullPath)); + ImageComparer.Exact.VerifySimilarity(image, magickImage); + } + + private Image DecodeWithMagick(Configuration configuration, FileInfo fileInfo) + where TPixel : struct, IPixel + { + using (var magickImage = new MagickImage(fileInfo)) + { + var result = new Image(configuration, magickImage.Width, magickImage.Height); + Span resultPixels = result.GetPixelSpan(); + + using (IPixelCollection pixels = magickImage.GetPixelsUnsafe()) + { + byte[] data = pixels.ToByteArray(PixelMapping.RGBA); + + PixelOperations.Instance.FromRgba32Bytes( + configuration, + data, + resultPixels, + resultPixels.Length); + } + + return result; + } + } + } +} diff --git a/tests/ImageSharp.Tests/ImageSharp.Tests.csproj b/tests/ImageSharp.Tests/ImageSharp.Tests.csproj index 1ac5f8085a..1f6b8b4d95 100644 --- a/tests/ImageSharp.Tests/ImageSharp.Tests.csproj +++ b/tests/ImageSharp.Tests/ImageSharp.Tests.csproj @@ -14,7 +14,7 @@ - + diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs index 146f2efcdb..bd1a0d3913 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/tests/ImageSharp.Tests/TestImages.cs @@ -365,5 +365,13 @@ namespace SixLabors.ImageSharp.Tests public static readonly string[] All = { Rings, Giphy, Cheers, Trans, Kumin, Leo, Ratio4x1, Ratio1x4 }; } + + public static class Tga + { + public const string Bit32 = "Tga/bike_32bit.tga"; + public const string Bit24 = "Tga/bike_24bit.tga"; + public const string Bit16 = "Tga/bike_16bit.tga"; + public const string Grey = "Tga/bike_8bit.tga"; + } } } diff --git a/tests/ImageSharp.Tests/TestUtilities/TestEnvironment.Formats.cs b/tests/ImageSharp.Tests/TestUtilities/TestEnvironment.Formats.cs index 7d06847223..e09b27c714 100644 --- a/tests/ImageSharp.Tests/TestUtilities/TestEnvironment.Formats.cs +++ b/tests/ImageSharp.Tests/TestUtilities/TestEnvironment.Formats.cs @@ -8,6 +8,7 @@ using SixLabors.ImageSharp.Formats.Bmp; using SixLabors.ImageSharp.Formats.Gif; using SixLabors.ImageSharp.Formats.Jpeg; using SixLabors.ImageSharp.Formats.Png; +using SixLabors.ImageSharp.Formats.Tga; using SixLabors.ImageSharp.Tests.TestUtilities.ReferenceCodecs; namespace SixLabors.ImageSharp.Tests @@ -53,7 +54,8 @@ namespace SixLabors.ImageSharp.Tests { var cfg = new Configuration( new JpegConfigurationModule(), - new GifConfigurationModule() + new GifConfigurationModule(), + new TgaConfigurationModule() ); // Magick codecs should work on all platforms @@ -75,4 +77,4 @@ namespace SixLabors.ImageSharp.Tests return cfg; } } -} \ No newline at end of file +} diff --git a/tests/Images/Input/Tga/bike_16bit.tga b/tests/Images/Input/Tga/bike_16bit.tga new file mode 100644 index 0000000000..00489d94a0 --- /dev/null +++ b/tests/Images/Input/Tga/bike_16bit.tga @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3b2bc922e2397ce8cd8b1e7792f20e2c7edad68ad8fac037a5b91bba5148a80b +size 97518 diff --git a/tests/Images/Input/Tga/bike_24bit.tga b/tests/Images/Input/Tga/bike_24bit.tga new file mode 100644 index 0000000000..a5e46f794d --- /dev/null +++ b/tests/Images/Input/Tga/bike_24bit.tga @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:26d55794c9b012b07517d1129630ccc35ca56015cbdd481debea00826130f925 +size 146268 diff --git a/tests/Images/Input/Tga/bike_32bit.tga b/tests/Images/Input/Tga/bike_32bit.tga new file mode 100644 index 0000000000..70e755d1f1 --- /dev/null +++ b/tests/Images/Input/Tga/bike_32bit.tga @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cb3774b695d2409f3e7584dc67ce7b3d8a75377c194e7f33e4cc315b3ae36a35 +size 195018 diff --git a/tests/Images/Input/Tga/bike_8bit.tga b/tests/Images/Input/Tga/bike_8bit.tga new file mode 100644 index 0000000000..ba11619a5e --- /dev/null +++ b/tests/Images/Input/Tga/bike_8bit.tga @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:75f3ce893f38d90767c692eb9628026c0421330934a5cbb686813ec26a9606d2 +size 48768 From aa110740e057bab1c1b17d2c4697e681c1165fc1 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Sun, 6 Oct 2019 20:55:03 +0200 Subject: [PATCH 04/75] Fix decoding 16 bit tga files by making them opaque --- src/ImageSharp/Formats/Tga/TgaDecoderCore.cs | 26 ++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs b/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs index abad2c5e77..92811fa46d 100644 --- a/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs +++ b/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. using System; +using System.Buffers; using System.IO; using System.Runtime.CompilerServices; @@ -123,7 +124,10 @@ namespace SixLabors.ImageSharp.Formats.Tga where TPixel : struct, IPixel { using (IManagedByteBuffer row = this.memoryAllocator.AllocatePaddedPixelRowBuffer(this.fileHeader.Width, 2, 0)) + using (IMemoryOwner bgraRow = this.memoryAllocator.Allocate(this.fileHeader.Width)) { + Span bgraRowSpan = bgraRow.GetSpan(); + long currentPosition = this.currentStream.Position; for (int y = 0; y < this.fileHeader.Height; y++) { this.currentStream.Read(row); @@ -134,6 +138,28 @@ namespace SixLabors.ImageSharp.Formats.Tga pixelSpan, this.fileHeader.Width); } + + // We need to set each alpha component value to fully opaque. + // Reset our stream for a second pass. + this.currentStream.Position = currentPosition; + for (int y = 0; y < this.fileHeader.Height; y++) + { + this.currentStream.Read(row); + PixelOperations.Instance.FromBgra5551Bytes( + this.configuration, + row.GetSpan(), + bgraRowSpan, + this.fileHeader.Width); + Span pixelSpan = pixels.GetRowSpan(this.fileHeader.Height - y - 1); + + for (int x = 0; x < this.fileHeader.Width; x++) + { + Bgra5551 bgra = bgraRowSpan[x]; + bgra.PackedValue = (ushort)(bgra.PackedValue | (1 << 15)); + ref TPixel pixel = ref pixelSpan[x]; + pixel.FromBgra5551(bgra); + } + } } } From 7a8a132558f0398a04d0f912e80f2d82eed67c25 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Sun, 6 Oct 2019 21:03:29 +0200 Subject: [PATCH 05/75] Add RLE test images --- .../Formats/Tga/TgaDecoderTests.cs | 48 +++++++++++++++++++ tests/ImageSharp.Tests/TestImages.cs | 4 ++ tests/Images/Input/Tga/bike_16bit_rle.tga | 3 ++ tests/Images/Input/Tga/bike_24bit_rle.tga | 3 ++ tests/Images/Input/Tga/bike_32bit_rle.tga | 3 ++ tests/Images/Input/Tga/bike_8bit_rle.tga | 3 ++ 6 files changed, 64 insertions(+) create mode 100644 tests/Images/Input/Tga/bike_16bit_rle.tga create mode 100644 tests/Images/Input/Tga/bike_24bit_rle.tga create mode 100644 tests/Images/Input/Tga/bike_32bit_rle.tga create mode 100644 tests/Images/Input/Tga/bike_8bit_rle.tga diff --git a/tests/ImageSharp.Tests/Formats/Tga/TgaDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Tga/TgaDecoderTests.cs index d998ac292b..47983a3ade 100644 --- a/tests/ImageSharp.Tests/Formats/Tga/TgaDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Tga/TgaDecoderTests.cs @@ -67,6 +67,54 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tga } } + [Theory] + [WithFile(GreyRle, PixelTypes.Rgba32)] + public void TgaDecoder_CanDecode_RunLenghtEncoded_MonoChrome(TestImageProvider provider) + where TPixel : struct, IPixel + { + using (Image image = provider.GetImage(new TgaDecoder())) + { + image.DebugSave(provider); + CompareWithReferenceDecoder(provider, image); + } + } + + [Theory] + [WithFile(Bit16Rle, PixelTypes.Rgba32)] + public void TgaDecoder_CanDecode_RunLenghtEncoded_16Bit(TestImageProvider provider) + where TPixel : struct, IPixel + { + using (Image image = provider.GetImage(new TgaDecoder())) + { + image.DebugSave(provider); + CompareWithReferenceDecoder(provider, image); + } + } + + [Theory] + [WithFile(Bit24Rle, PixelTypes.Rgba32)] + public void TgaDecoder_CanDecode_RunLenghtEncoded_24Bit(TestImageProvider provider) + where TPixel : struct, IPixel + { + using (Image image = provider.GetImage(new TgaDecoder())) + { + image.DebugSave(provider); + CompareWithReferenceDecoder(provider, image); + } + } + + [Theory] + [WithFile(Bit32Rle, PixelTypes.Rgba32)] + public void TgaDecoder_CanDecode_RunLenghtEncoded_32Bit(TestImageProvider provider) + where TPixel : struct, IPixel + { + using (Image image = provider.GetImage(new TgaDecoder())) + { + image.DebugSave(provider); + CompareWithReferenceDecoder(provider, image); + } + } + private void CompareWithReferenceDecoder(TestImageProvider provider, Image image) where TPixel : struct, IPixel { diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs index bd1a0d3913..51ca48c375 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/tests/ImageSharp.Tests/TestImages.cs @@ -372,6 +372,10 @@ namespace SixLabors.ImageSharp.Tests public const string Bit24 = "Tga/bike_24bit.tga"; public const string Bit16 = "Tga/bike_16bit.tga"; public const string Grey = "Tga/bike_8bit.tga"; + public const string Bit32Rle = "Tga/bike_32bit_rle.tga"; + public const string Bit24Rle = "Tga/bike_24bit_rle.tga"; + public const string Bit16Rle = "Tga/bike_16bit_rle.tga"; + public const string GreyRle = "Tga/bike_8bit_rle.tga"; } } } diff --git a/tests/Images/Input/Tga/bike_16bit_rle.tga b/tests/Images/Input/Tga/bike_16bit_rle.tga new file mode 100644 index 0000000000..47c37c50b6 --- /dev/null +++ b/tests/Images/Input/Tga/bike_16bit_rle.tga @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c9f7381f04b3ce23e3144f6b5789d7eccc0c0f010fc901f42d01171556181c1c +size 56938 diff --git a/tests/Images/Input/Tga/bike_24bit_rle.tga b/tests/Images/Input/Tga/bike_24bit_rle.tga new file mode 100644 index 0000000000..65dfb9a1ab --- /dev/null +++ b/tests/Images/Input/Tga/bike_24bit_rle.tga @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:88f7936db92cf536c9656a8ff00c25842c71b93823e05322a1efc3aef2c0a80e +size 106721 diff --git a/tests/Images/Input/Tga/bike_32bit_rle.tga b/tests/Images/Input/Tga/bike_32bit_rle.tga new file mode 100644 index 0000000000..967e9ce7ee --- /dev/null +++ b/tests/Images/Input/Tga/bike_32bit_rle.tga @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:815e9f32f29a8c51bbd0f2c7d507d6331a709f157fb7bd65188fd07677764c59 +size 141452 diff --git a/tests/Images/Input/Tga/bike_8bit_rle.tga b/tests/Images/Input/Tga/bike_8bit_rle.tga new file mode 100644 index 0000000000..0c6ee06a44 --- /dev/null +++ b/tests/Images/Input/Tga/bike_8bit_rle.tga @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1531a0f8a95fb4a57e0bde1842b6ee65f5cabfc62ad883d1f9dbc2c5616e498f +size 37259 From 3cbc741a18be2981b1f852cf97d80df1cdafb0a7 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Tue, 8 Oct 2019 19:13:01 +0200 Subject: [PATCH 06/75] Add decoding of 24bit RLE tga images --- src/ImageSharp/Formats/Tga/TgaDecoderCore.cs | 84 +++++++++++++++++++- src/ImageSharp/Formats/Tga/TgaImageType.cs | 24 +++++- src/ImageSharp/Formats/Tga/TgaThrowHelper.cs | 10 +++ 3 files changed, 116 insertions(+), 2 deletions(-) diff --git a/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs b/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs index 92811fa46d..692876cea3 100644 --- a/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs +++ b/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs @@ -82,7 +82,15 @@ namespace SixLabors.ImageSharp.Formats.Tga break; case 24: - this.ReadBgr24(pixels); + if (this.fileHeader.ImageType.IsRunLengthEncoded()) + { + this.ReadRle24(pixels, this.fileHeader.Width, this.fileHeader.Height); + } + else + { + this.ReadBgr24(pixels); + } + break; case 32: @@ -181,6 +189,80 @@ namespace SixLabors.ImageSharp.Formats.Tga } } + private void ReadRle24(Buffer2D pixels, int width, int height) + where TPixel : struct, IPixel + { + TPixel color = default; + using (IMemoryOwner buffer = this.memoryAllocator.Allocate(width * height * 3, AllocationOptions.Clean)) + { + Span bufferSpan = buffer.GetSpan(); + this.UncompressRle24(width, bufferSpan); + for (int y = 0; y < height; y++) + { + Span pixelRow = pixels.GetRowSpan(this.fileHeader.Height - y - 1); + for (int x = 0; x < width; x++) + { + int idx = (y * width * 3) + (x * 3); + color.FromBgr24(Unsafe.As(ref bufferSpan[idx])); + pixelRow[x] = color; + } + } + } + } + + private void UncompressRle24(int w, Span buffer) + { + int uncompressedPixels = 0; +#if NETCOREAPP2_1 + Span pixel = stackalloc byte[3]; +#else + var pixel = new byte[3]; +#endif + int totalPixels = this.fileHeader.Height * this.fileHeader.Width; + while (uncompressedPixels < totalPixels) + { + byte runLengthByte = (byte)this.currentStream.ReadByte(); + + // The high bit of the run length value is always 1, to indicate that this is a run-length encoded packet. + int highBit = runLengthByte >> 7; + if (highBit == 1) + { + int runLength = runLengthByte & 127; + if (runLength == 0) + { + // TgaThrowHelper.ThrowImageFormatException("invalid run length of zero"); + } + + this.currentStream.Read(pixel, 0, 3); + int bufferIdx = uncompressedPixels * 3; + for (int i = 0; i < runLength + 1; i++, uncompressedPixels++) + { + buffer[bufferIdx++] = pixel[0]; + buffer[bufferIdx++] = pixel[1]; + buffer[bufferIdx++] = pixel[2]; + } + } + else + { + // Non-run-length encoded packet. + int runLength = runLengthByte; + if (runLength == 0) + { + // TgaThrowHelper.ThrowImageFormatException("invalid run length of zero"); + } + + int bufferIdx = uncompressedPixels * 3; + for (int i = 0; i < runLength + 1; i++, uncompressedPixels++) + { + this.currentStream.Read(pixel, 0, 3); + buffer[bufferIdx++] = pixel[0]; + buffer[bufferIdx++] = pixel[1]; + buffer[bufferIdx++] = pixel[2]; + } + } + } + } + private void ReadBgra32(Buffer2D pixels) where TPixel : struct, IPixel { diff --git a/src/ImageSharp/Formats/Tga/TgaImageType.cs b/src/ImageSharp/Formats/Tga/TgaImageType.cs index d8140d5c6e..2c19a06954 100644 --- a/src/ImageSharp/Formats/Tga/TgaImageType.cs +++ b/src/ImageSharp/Formats/Tga/TgaImageType.cs @@ -1,7 +1,8 @@ // Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. -namespace SixLabors.ImageSharp.Formats.Tga +namespace SixLabors. + ImageSharp.Formats.Tga { /// /// Defines the tga image type. The TGA File Format can be used to store Pseudo-Color, @@ -45,4 +46,25 @@ namespace SixLabors.ImageSharp.Formats.Tga /// RleBlackAndWhite = 11, } + + /// + /// Extension methods for TgaImageType enum. + /// + public static class TgaImageTypeExtensions + { + /// + /// Checks if this tga image type is run length encoded. + /// + /// The tga image type. + /// True, if this image type is run length encoded, otherwise false. + public static bool IsRunLengthEncoded(this TgaImageType imageType) + { + if (imageType is TgaImageType.RleColorMapped || imageType is TgaImageType.RleBlackAndWhite || imageType is TgaImageType.RleTrueColor) + { + return true; + } + + return false; + } + } } diff --git a/src/ImageSharp/Formats/Tga/TgaThrowHelper.cs b/src/ImageSharp/Formats/Tga/TgaThrowHelper.cs index 9e36b20539..845d009227 100644 --- a/src/ImageSharp/Formats/Tga/TgaThrowHelper.cs +++ b/src/ImageSharp/Formats/Tga/TgaThrowHelper.cs @@ -8,6 +8,16 @@ namespace SixLabors.ImageSharp.Formats.Tga { internal static class TgaThrowHelper { + /// + /// 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); + } + /// /// Cold path optimization for throwing -s /// From fec6cad3ea77a74a5cba0e7c741425d226fce0e7 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Tue, 8 Oct 2019 19:32:17 +0200 Subject: [PATCH 07/75] Add decoding of 32 bit RLE --- src/ImageSharp/Formats/Tga/TgaDecoderCore.cs | 100 ++++++++++++++----- 1 file changed, 75 insertions(+), 25 deletions(-) diff --git a/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs b/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs index 692876cea3..0627be98ca 100644 --- a/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs +++ b/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs @@ -94,7 +94,15 @@ namespace SixLabors.ImageSharp.Formats.Tga break; case 32: - this.ReadBgra32(pixels); + if (this.fileHeader.ImageType.IsRunLengthEncoded()) + { + this.ReadRle32(pixels, this.fileHeader.Width, this.fileHeader.Height); + } + else + { + this.ReadBgra32(pixels); + } + break; default: @@ -196,7 +204,7 @@ namespace SixLabors.ImageSharp.Formats.Tga using (IMemoryOwner buffer = this.memoryAllocator.Allocate(width * height * 3, AllocationOptions.Clean)) { Span bufferSpan = buffer.GetSpan(); - this.UncompressRle24(width, bufferSpan); + this.UncompressRle24(width, height, bufferSpan); for (int y = 0; y < height; y++) { Span pixelRow = pixels.GetRowSpan(this.fileHeader.Height - y - 1); @@ -210,54 +218,38 @@ namespace SixLabors.ImageSharp.Formats.Tga } } - private void UncompressRle24(int w, Span buffer) + private void UncompressRle24(int width, int height, Span buffer) { int uncompressedPixels = 0; -#if NETCOREAPP2_1 - Span pixel = stackalloc byte[3]; -#else var pixel = new byte[3]; -#endif - int totalPixels = this.fileHeader.Height * this.fileHeader.Width; + int totalPixels = width * height; while (uncompressedPixels < totalPixels) { byte runLengthByte = (byte)this.currentStream.ReadByte(); - // The high bit of the run length value is always 1, to indicate that this is a run-length encoded packet. + // The high bit of a run length packet is set to 1. int highBit = runLengthByte >> 7; if (highBit == 1) { int runLength = runLengthByte & 127; - if (runLength == 0) - { - // TgaThrowHelper.ThrowImageFormatException("invalid run length of zero"); - } - this.currentStream.Read(pixel, 0, 3); int bufferIdx = uncompressedPixels * 3; for (int i = 0; i < runLength + 1; i++, uncompressedPixels++) { - buffer[bufferIdx++] = pixel[0]; - buffer[bufferIdx++] = pixel[1]; - buffer[bufferIdx++] = pixel[2]; + pixel.AsSpan().CopyTo(buffer.Slice(bufferIdx)); + bufferIdx += 3; } } else { // Non-run-length encoded packet. int runLength = runLengthByte; - if (runLength == 0) - { - // TgaThrowHelper.ThrowImageFormatException("invalid run length of zero"); - } - int bufferIdx = uncompressedPixels * 3; for (int i = 0; i < runLength + 1; i++, uncompressedPixels++) { this.currentStream.Read(pixel, 0, 3); - buffer[bufferIdx++] = pixel[0]; - buffer[bufferIdx++] = pixel[1]; - buffer[bufferIdx++] = pixel[2]; + pixel.AsSpan().CopyTo(buffer.Slice(bufferIdx)); + bufferIdx += 3; } } } @@ -281,6 +273,64 @@ namespace SixLabors.ImageSharp.Formats.Tga } } + private void ReadRle32(Buffer2D pixels, int width, int height) + where TPixel : struct, IPixel + { + TPixel color = default; + using (IMemoryOwner buffer = this.memoryAllocator.Allocate(width * height * 4, AllocationOptions.Clean)) + { + Span bufferSpan = buffer.GetSpan(); + this.UncompressRle32(width, height, bufferSpan); + for (int y = 0; y < height; y++) + { + Span pixelRow = pixels.GetRowSpan(this.fileHeader.Height - y - 1); + for (int x = 0; x < width; x++) + { + int idx = (y * width * 4) + (x * 4); + color.FromBgra32(Unsafe.As(ref bufferSpan[idx])); + pixelRow[x] = color; + } + } + } + } + + private void UncompressRle32(int width, int height, Span buffer) + { + int uncompressedPixels = 0; + var pixel = new byte[4]; + int totalPixels = width * height; + while (uncompressedPixels < totalPixels) + { + byte runLengthByte = (byte)this.currentStream.ReadByte(); + + // The high bit of a run length packet is set to 1. + int highBit = runLengthByte >> 7; + if (highBit == 1) + { + int runLength = runLengthByte & 127; + this.currentStream.Read(pixel, 0, 4); + int bufferIdx = uncompressedPixels * 4; + for (int i = 0; i < runLength + 1; i++, uncompressedPixels++) + { + pixel.AsSpan().CopyTo(buffer.Slice(bufferIdx)); + bufferIdx += 4; + } + } + else + { + // Non-run-length encoded packet. + int runLength = runLengthByte; + int bufferIdx = uncompressedPixels * 4; + for (int i = 0; i < runLength + 1; i++, uncompressedPixels++) + { + this.currentStream.Read(pixel, 0, 4); + pixel.AsSpan().CopyTo(buffer.Slice(bufferIdx)); + bufferIdx += 4; + } + } + } + } + /// /// Reads the raw image information from the specified stream. /// From 81ed8ca8e2fad8df8090ccea819fb3e80d146418 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Tue, 8 Oct 2019 19:41:12 +0200 Subject: [PATCH 08/75] Avoid calculating the row start index inside the loop --- src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs b/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs index b7733e0269..03e082cce0 100644 --- a/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs +++ b/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs @@ -387,9 +387,10 @@ namespace SixLabors.ImageSharp.Formats.Bmp if (rowHasUndefinedPixels) { // Slow path with undefined pixels. + int rowStartIdx = y * width * 3; for (int x = 0; x < width; x++) { - int idx = (y * width * 3) + (x * 3); + int idx = rowStartIdx + (x * 3); if (undefinedPixels[x, y]) { switch (this.options.RleSkippedPixelHandling) @@ -418,9 +419,10 @@ namespace SixLabors.ImageSharp.Formats.Bmp else { // Fast path without any undefined pixels. + int rowStartIdx = y * width * 3; for (int x = 0; x < width; x++) { - int idx = (y * width * 3) + (x * 3); + int idx = rowStartIdx + (x * 3); color.FromBgr24(Unsafe.As(ref bufferSpan[idx])); pixelRow[x] = color; } From 2bfe960d6a9f8a5a0be8071fb7094856469ffdce Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Tue, 8 Oct 2019 19:50:13 +0200 Subject: [PATCH 09/75] Unified reading rle images --- src/ImageSharp/Formats/Tga/TgaDecoderCore.cs | 98 +++++--------------- 1 file changed, 25 insertions(+), 73 deletions(-) diff --git a/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs b/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs index 0627be98ca..224a14131c 100644 --- a/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs +++ b/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs @@ -84,7 +84,7 @@ namespace SixLabors.ImageSharp.Formats.Tga case 24: if (this.fileHeader.ImageType.IsRunLengthEncoded()) { - this.ReadRle24(pixels, this.fileHeader.Width, this.fileHeader.Height); + this.ReadRle(this.fileHeader.Width, this.fileHeader.Height, pixels, 3); } else { @@ -96,7 +96,7 @@ namespace SixLabors.ImageSharp.Formats.Tga case 32: if (this.fileHeader.ImageType.IsRunLengthEncoded()) { - this.ReadRle32(pixels, this.fileHeader.Width, this.fileHeader.Height); + this.ReadRle(this.fileHeader.Width, this.fileHeader.Height, pixels, 4); } else { @@ -197,64 +197,6 @@ namespace SixLabors.ImageSharp.Formats.Tga } } - private void ReadRle24(Buffer2D pixels, int width, int height) - where TPixel : struct, IPixel - { - TPixel color = default; - using (IMemoryOwner buffer = this.memoryAllocator.Allocate(width * height * 3, AllocationOptions.Clean)) - { - Span bufferSpan = buffer.GetSpan(); - this.UncompressRle24(width, height, bufferSpan); - for (int y = 0; y < height; y++) - { - Span pixelRow = pixels.GetRowSpan(this.fileHeader.Height - y - 1); - for (int x = 0; x < width; x++) - { - int idx = (y * width * 3) + (x * 3); - color.FromBgr24(Unsafe.As(ref bufferSpan[idx])); - pixelRow[x] = color; - } - } - } - } - - private void UncompressRle24(int width, int height, Span buffer) - { - int uncompressedPixels = 0; - var pixel = new byte[3]; - int totalPixels = width * height; - while (uncompressedPixels < totalPixels) - { - byte runLengthByte = (byte)this.currentStream.ReadByte(); - - // The high bit of a run length packet is set to 1. - int highBit = runLengthByte >> 7; - if (highBit == 1) - { - int runLength = runLengthByte & 127; - this.currentStream.Read(pixel, 0, 3); - int bufferIdx = uncompressedPixels * 3; - for (int i = 0; i < runLength + 1; i++, uncompressedPixels++) - { - pixel.AsSpan().CopyTo(buffer.Slice(bufferIdx)); - bufferIdx += 3; - } - } - else - { - // Non-run-length encoded packet. - int runLength = runLengthByte; - int bufferIdx = uncompressedPixels * 3; - for (int i = 0; i < runLength + 1; i++, uncompressedPixels++) - { - this.currentStream.Read(pixel, 0, 3); - pixel.AsSpan().CopyTo(buffer.Slice(bufferIdx)); - bufferIdx += 3; - } - } - } - } - private void ReadBgra32(Buffer2D pixels) where TPixel : struct, IPixel { @@ -273,31 +215,41 @@ namespace SixLabors.ImageSharp.Formats.Tga } } - private void ReadRle32(Buffer2D pixels, int width, int height) + private void ReadRle(int width, int height, Buffer2D pixels, int bytesPerPixel) where TPixel : struct, IPixel { TPixel color = default; - using (IMemoryOwner buffer = this.memoryAllocator.Allocate(width * height * 4, AllocationOptions.Clean)) + using (IMemoryOwner buffer = this.memoryAllocator.Allocate(width * height * bytesPerPixel, AllocationOptions.Clean)) { Span bufferSpan = buffer.GetSpan(); - this.UncompressRle32(width, height, bufferSpan); + this.UncompressRle(width, height, bufferSpan, bytesPerPixel); for (int y = 0; y < height; y++) { Span pixelRow = pixels.GetRowSpan(this.fileHeader.Height - y - 1); + int rowStartIdx = y * width * bytesPerPixel; for (int x = 0; x < width; x++) { - int idx = (y * width * 4) + (x * 4); - color.FromBgra32(Unsafe.As(ref bufferSpan[idx])); + int idx = rowStartIdx + (x * bytesPerPixel); + switch (bytesPerPixel) + { + case 4: + color.FromBgra32(Unsafe.As(ref bufferSpan[idx])); + break; + case 3: + color.FromBgr24(Unsafe.As(ref bufferSpan[idx])); + break; + } + pixelRow[x] = color; } } } } - private void UncompressRle32(int width, int height, Span buffer) + private void UncompressRle(int width, int height, Span buffer, int bytesPerPixel) { int uncompressedPixels = 0; - var pixel = new byte[4]; + var pixel = new byte[bytesPerPixel]; int totalPixels = width * height; while (uncompressedPixels < totalPixels) { @@ -308,24 +260,24 @@ namespace SixLabors.ImageSharp.Formats.Tga if (highBit == 1) { int runLength = runLengthByte & 127; - this.currentStream.Read(pixel, 0, 4); - int bufferIdx = uncompressedPixels * 4; + this.currentStream.Read(pixel, 0, bytesPerPixel); + int bufferIdx = uncompressedPixels * bytesPerPixel; for (int i = 0; i < runLength + 1; i++, uncompressedPixels++) { pixel.AsSpan().CopyTo(buffer.Slice(bufferIdx)); - bufferIdx += 4; + bufferIdx += bytesPerPixel; } } else { // Non-run-length encoded packet. int runLength = runLengthByte; - int bufferIdx = uncompressedPixels * 4; + int bufferIdx = uncompressedPixels * bytesPerPixel; for (int i = 0; i < runLength + 1; i++, uncompressedPixels++) { - this.currentStream.Read(pixel, 0, 4); + this.currentStream.Read(pixel, 0, bytesPerPixel); pixel.AsSpan().CopyTo(buffer.Slice(bufferIdx)); - bufferIdx += 4; + bufferIdx += bytesPerPixel; } } } From fabd7f4bbe30565628fea0b9ec69b586057e5494 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Tue, 8 Oct 2019 20:53:49 +0200 Subject: [PATCH 10/75] Add decoding of 16 and 8 bit rle images --- src/ImageSharp/Formats/Tga/TgaDecoderCore.cs | 78 ++++++++++++++------ 1 file changed, 54 insertions(+), 24 deletions(-) diff --git a/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs b/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs index 224a14131c..b6b078f6de 100644 --- a/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs +++ b/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs @@ -74,11 +74,28 @@ namespace SixLabors.ImageSharp.Formats.Tga switch (this.fileHeader.PixelDepth) { case 8: - this.ReadMonoChrome(pixels); + if (this.fileHeader.ImageType.IsRunLengthEncoded()) + { + this.ReadRle(this.fileHeader.Width, this.fileHeader.Height, pixels, 1); + } + else + { + this.ReadMonoChrome(pixels); + } + break; case 16: - this.ReadBgra16(pixels); + if (this.fileHeader.ImageType.IsRunLengthEncoded()) + { + long currentPosition = this.currentStream.Position; + this.ReadRle(this.fileHeader.Width, this.fileHeader.Height, pixels, 2); + } + else + { + this.ReadBgra16(pixels); + } + break; case 24: @@ -156,26 +173,7 @@ namespace SixLabors.ImageSharp.Formats.Tga } // We need to set each alpha component value to fully opaque. - // Reset our stream for a second pass. - this.currentStream.Position = currentPosition; - for (int y = 0; y < this.fileHeader.Height; y++) - { - this.currentStream.Read(row); - PixelOperations.Instance.FromBgra5551Bytes( - this.configuration, - row.GetSpan(), - bgraRowSpan, - this.fileHeader.Width); - Span pixelSpan = pixels.GetRowSpan(this.fileHeader.Height - y - 1); - - for (int x = 0; x < this.fileHeader.Width; x++) - { - Bgra5551 bgra = bgraRowSpan[x]; - bgra.PackedValue = (ushort)(bgra.PackedValue | (1 << 15)); - ref TPixel pixel = ref pixelSpan[x]; - pixel.FromBgra5551(bgra); - } - } + this.MakeOpaque(pixels, currentPosition, row, bgraRowSpan); } } @@ -232,12 +230,19 @@ namespace SixLabors.ImageSharp.Formats.Tga int idx = rowStartIdx + (x * bytesPerPixel); switch (bytesPerPixel) { - case 4: - color.FromBgra32(Unsafe.As(ref bufferSpan[idx])); + case 1: + color.FromGray8(Unsafe.As(ref bufferSpan[idx])); + break; + case 2: + bufferSpan[idx + 1] = (byte)(bufferSpan[idx + 1] | 128); + color.FromBgra5551(Unsafe.As(ref bufferSpan[idx])); break; case 3: color.FromBgr24(Unsafe.As(ref bufferSpan[idx])); break; + case 4: + color.FromBgra32(Unsafe.As(ref bufferSpan[idx])); + break; } pixelRow[x] = color; @@ -283,6 +288,31 @@ namespace SixLabors.ImageSharp.Formats.Tga } } + private void MakeOpaque(Buffer2D pixels, long currentPosition, IManagedByteBuffer row, Span bgraRowSpan) + where TPixel : struct, IPixel + { + // Reset our stream for a second pass. + this.currentStream.Position = currentPosition; + for (int y = 0; y < this.fileHeader.Height; y++) + { + this.currentStream.Read(row); + PixelOperations.Instance.FromBgra5551Bytes( + this.configuration, + row.GetSpan(), + bgraRowSpan, + this.fileHeader.Width); + Span pixelSpan = pixels.GetRowSpan(this.fileHeader.Height - y - 1); + + for (int x = 0; x < this.fileHeader.Width; x++) + { + Bgra5551 bgra = bgraRowSpan[x]; + bgra.PackedValue = (ushort)(bgra.PackedValue | (1 << 15)); + ref TPixel pixel = ref pixelSpan[x]; + pixel.FromBgra5551(bgra); + } + } + } + /// /// Reads the raw image information from the specified stream. /// From 167eec1a165c28b7f98ec0db9250136a1d054488 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Wed, 9 Oct 2019 19:30:25 +0200 Subject: [PATCH 11/75] Add support for encoding 24 bit tga files --- .../Formats/Tga/ITgaEncoderOptions.cs | 16 +++ src/ImageSharp/Formats/Tga/TgaBitsPerPixel.cs | 31 +++++ .../Formats/Tga/TgaConfigurationModule.cs | 1 + src/ImageSharp/Formats/Tga/TgaEncoder.cs | 26 ++++ src/ImageSharp/Formats/Tga/TgaEncoderCore.cs | 131 ++++++++++++++++++ src/ImageSharp/Formats/Tga/TgaFileHeader.cs | 8 ++ src/ImageSharp/Formats/Tga/TgaMetadata.cs | 16 ++- 7 files changed, 228 insertions(+), 1 deletion(-) create mode 100644 src/ImageSharp/Formats/Tga/ITgaEncoderOptions.cs create mode 100644 src/ImageSharp/Formats/Tga/TgaBitsPerPixel.cs create mode 100644 src/ImageSharp/Formats/Tga/TgaEncoder.cs create mode 100644 src/ImageSharp/Formats/Tga/TgaEncoderCore.cs diff --git a/src/ImageSharp/Formats/Tga/ITgaEncoderOptions.cs b/src/ImageSharp/Formats/Tga/ITgaEncoderOptions.cs new file mode 100644 index 0000000000..a6e9871e0a --- /dev/null +++ b/src/ImageSharp/Formats/Tga/ITgaEncoderOptions.cs @@ -0,0 +1,16 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +namespace SixLabors.ImageSharp.Formats.Tga +{ + /// + /// Configuration options for use during tga encoding. + /// + internal interface ITgaEncoderOptions + { + /// + /// Gets the number of bits per pixel. + /// + TgaBitsPerPixel? BitsPerPixel { get; } + } +} diff --git a/src/ImageSharp/Formats/Tga/TgaBitsPerPixel.cs b/src/ImageSharp/Formats/Tga/TgaBitsPerPixel.cs new file mode 100644 index 0000000000..a0666fa84d --- /dev/null +++ b/src/ImageSharp/Formats/Tga/TgaBitsPerPixel.cs @@ -0,0 +1,31 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +namespace SixLabors.ImageSharp.Formats.Tga +{ + /// + /// Enumerates the available bits per pixel the tga encoder supports. + /// + public enum TgaBitsPerPixel : byte + { + /// + /// 8 bits per pixel. Each pixel consists of 1 byte. + /// + Pixel8 = 8, + + /// + /// 16 bits per pixel. Each pixel consists of 2 bytes. + /// + Pixel16 = 16, + + /// + /// 24 bits per pixel. Each pixel consists of 3 bytes. + /// + Pixel24 = 24, + + /// + /// 32 bits per pixel. Each pixel consists of 4 bytes. + /// + Pixel32 = 32 + } +} diff --git a/src/ImageSharp/Formats/Tga/TgaConfigurationModule.cs b/src/ImageSharp/Formats/Tga/TgaConfigurationModule.cs index c7bf6cc93d..18fbf4acd0 100644 --- a/src/ImageSharp/Formats/Tga/TgaConfigurationModule.cs +++ b/src/ImageSharp/Formats/Tga/TgaConfigurationModule.cs @@ -11,6 +11,7 @@ namespace SixLabors.ImageSharp.Formats.Tga /// public void Configure(Configuration configuration) { + configuration.ImageFormatsManager.SetEncoder(TgaFormat.Instance, new TgaEncoder()); configuration.ImageFormatsManager.SetDecoder(TgaFormat.Instance, new TgaDecoder()); configuration.ImageFormatsManager.AddImageFormatDetector(new TgaImageFormatDetector()); } diff --git a/src/ImageSharp/Formats/Tga/TgaEncoder.cs b/src/ImageSharp/Formats/Tga/TgaEncoder.cs new file mode 100644 index 0000000000..f5b9fa752c --- /dev/null +++ b/src/ImageSharp/Formats/Tga/TgaEncoder.cs @@ -0,0 +1,26 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System.IO; + +using SixLabors.ImageSharp.Advanced; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Tga +{ + public sealed class TgaEncoder : IImageEncoder, ITgaEncoderOptions + { + /// + /// Gets or sets the number of bits per pixel. + /// + public TgaBitsPerPixel? BitsPerPixel { get; set; } + + /// + public void Encode(Image image, Stream stream) + where TPixel : struct, IPixel + { + var encoder = new TgaEncoderCore(this, image.GetMemoryAllocator()); + encoder.Encode(image, stream); + } + } +} diff --git a/src/ImageSharp/Formats/Tga/TgaEncoderCore.cs b/src/ImageSharp/Formats/Tga/TgaEncoderCore.cs new file mode 100644 index 0000000000..349963f7fb --- /dev/null +++ b/src/ImageSharp/Formats/Tga/TgaEncoderCore.cs @@ -0,0 +1,131 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.IO; + +using SixLabors.ImageSharp.Advanced; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.Metadata; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.Memory; + +namespace SixLabors.ImageSharp.Formats.Tga +{ + /// + /// Image encoder for writing an image to a stream as a truevision targa image. + /// + internal sealed class TgaEncoderCore + { + /// + /// Used for allocating memory during processing operations. + /// + private readonly MemoryAllocator memoryAllocator; + + /// + /// The global configuration. + /// + private Configuration configuration; + + /// + /// The color depth, in number of bits per pixel. + /// + private TgaBitsPerPixel? bitsPerPixel; + + /// + /// Initializes a new instance of the class. + /// + /// The encoder options. + /// The memory manager. + public TgaEncoderCore(ITgaEncoderOptions options, MemoryAllocator memoryAllocator) + { + this.memoryAllocator = memoryAllocator; + this.bitsPerPixel = options.BitsPerPixel; + } + + public void Encode(Image image, Stream stream) + where TPixel : struct, IPixel + { + Guard.NotNull(image, nameof(image)); + Guard.NotNull(stream, nameof(stream)); + + this.configuration = image.GetConfiguration(); + ImageMetadata metadata = image.Metadata; + TgaMetadata tgaMetadata = metadata.GetFormatMetadata(TgaFormat.Instance); + this.bitsPerPixel = this.bitsPerPixel ?? tgaMetadata.BitsPerPixel; + + var fileHeader = new TgaFileHeader( + idLength: 0, + colorMapType: 0, + imageType: TgaImageType.TrueColor, + cMapStart: 0, + cMapLength: 0, + cMapDepth: 0, + xOffset: 0, + yOffset: 0, + width: (short)image.Width, + height: (short)image.Height, + pixelDepth: (byte)this.bitsPerPixel.Value, + imageDescriptor: 0); + +#if NETCOREAPP2_1 + Span buffer = stackalloc byte[TgaFileHeader.Size]; +#else + var buffer = new byte[TgaFileHeader.Size]; +#endif + fileHeader.WriteTo(buffer); + + stream.Write(buffer, 0, TgaFileHeader.Size); + + this.WriteImage(stream, image.Frames.RootFrame); + + stream.Flush(); + } + + /// + /// Writes the pixel data to the binary stream. + /// + /// The pixel format. + /// The to write to. + /// + /// The containing pixel data. + /// + private void WriteImage(Stream stream, ImageFrame image) + where TPixel : struct, IPixel + { + Buffer2D pixels = image.PixelBuffer; + switch (this.bitsPerPixel) + { + case TgaBitsPerPixel.Pixel24: + this.Write24Bit(stream, pixels); + break; + } + } + + private IManagedByteBuffer AllocateRow(int width, int bytesPerPixel) => this.memoryAllocator.AllocatePaddedPixelRowBuffer(width, bytesPerPixel, 0); + + /// + /// Writes the 24bit pixels uncompressed to the stream. + /// + /// The pixel format. + /// The to write to. + /// The containing pixel data. + private void Write24Bit(Stream stream, Buffer2D pixels) + where TPixel : struct, IPixel + { + using (IManagedByteBuffer row = this.AllocateRow(pixels.Width, 3)) + { + for (int y = pixels.Height - 1; y >= 0; y--) + { + Span pixelSpan = pixels.GetRowSpan(y); + PixelOperations.Instance.ToBgr24Bytes( + this.configuration, + pixelSpan, + row.GetSpan(), + pixelSpan.Length); + stream.Write(row.Array, 0, row.Length()); + } + } + } + } +} diff --git a/src/ImageSharp/Formats/Tga/TgaFileHeader.cs b/src/ImageSharp/Formats/Tga/TgaFileHeader.cs index 51390e1b73..72c275b289 100644 --- a/src/ImageSharp/Formats/Tga/TgaFileHeader.cs +++ b/src/ImageSharp/Formats/Tga/TgaFileHeader.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. using System; +using System.Runtime.CompilerServices; using System.Runtime.InteropServices; namespace SixLabors.ImageSharp.Formats.Tga @@ -135,5 +136,12 @@ namespace SixLabors.ImageSharp.Formats.Tga { return MemoryMarshal.Cast(data)[0]; } + + public void WriteTo(Span buffer) + { + ref TgaFileHeader dest = ref Unsafe.As(ref MemoryMarshal.GetReference(buffer)); + + dest = this; + } } } diff --git a/src/ImageSharp/Formats/Tga/TgaMetadata.cs b/src/ImageSharp/Formats/Tga/TgaMetadata.cs index 185eaedc9a..4ce61d2e48 100644 --- a/src/ImageSharp/Formats/Tga/TgaMetadata.cs +++ b/src/ImageSharp/Formats/Tga/TgaMetadata.cs @@ -15,7 +15,21 @@ namespace SixLabors.ImageSharp.Formats.Tga { } + /// + /// Initializes a new instance of the class. + /// + /// The metadata to create an instance from. + private TgaMetadata(TgaMetadata other) + { + this.BitsPerPixel = other.BitsPerPixel; + } + + /// + /// Gets or sets the number of bits per pixel. + /// + public TgaBitsPerPixel BitsPerPixel { get; set; } = TgaBitsPerPixel.Pixel24; + /// - public IDeepCloneable DeepClone() => throw new System.NotImplementedException(); + public IDeepCloneable DeepClone() => new TgaMetadata(this); } } From 98f4def9a918b6857d2f94969211c02772917957 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Wed, 9 Oct 2019 19:51:28 +0200 Subject: [PATCH 12/75] Support for encoding 8, 16 and 32 bit tga files --- src/ImageSharp/Formats/Tga/TgaDecoderCore.cs | 9 ++- src/ImageSharp/Formats/Tga/TgaEncoderCore.cs | 84 ++++++++++++++++++++ 2 files changed, 91 insertions(+), 2 deletions(-) diff --git a/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs b/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs index b6b078f6de..a9002fd8cf 100644 --- a/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs +++ b/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs @@ -20,6 +20,11 @@ namespace SixLabors.ImageSharp.Formats.Tga /// private ImageMetadata metadata; + /// + /// The tga specific metadata. + /// + private TgaMetadata tgaMetadata; + /// /// The file header containing general information about the image. /// @@ -342,9 +347,9 @@ namespace SixLabors.ImageSharp.Formats.Tga #endif this.currentStream.Read(buffer, 0, TgaFileHeader.Size); this.fileHeader = TgaFileHeader.Parse(buffer); - - // TODO: no meta data yet. this.metadata = new ImageMetadata(); + this.tgaMetadata = this.metadata.GetFormatMetadata(TgaFormat.Instance); + this.tgaMetadata.BitsPerPixel = (TgaBitsPerPixel)this.fileHeader.PixelDepth; } } } diff --git a/src/ImageSharp/Formats/Tga/TgaEncoderCore.cs b/src/ImageSharp/Formats/Tga/TgaEncoderCore.cs index 349963f7fb..ddd430b055 100644 --- a/src/ImageSharp/Formats/Tga/TgaEncoderCore.cs +++ b/src/ImageSharp/Formats/Tga/TgaEncoderCore.cs @@ -96,14 +96,74 @@ namespace SixLabors.ImageSharp.Formats.Tga Buffer2D pixels = image.PixelBuffer; switch (this.bitsPerPixel) { + case TgaBitsPerPixel.Pixel8: + this.Write8Bit(stream, pixels); + break; + + case TgaBitsPerPixel.Pixel16: + this.Write16Bit(stream, pixels); + break; + case TgaBitsPerPixel.Pixel24: this.Write24Bit(stream, pixels); break; + + case TgaBitsPerPixel.Pixel32: + this.Write32Bit(stream, pixels); + break; } } private IManagedByteBuffer AllocateRow(int width, int bytesPerPixel) => this.memoryAllocator.AllocatePaddedPixelRowBuffer(width, bytesPerPixel, 0); + /// + /// Writes the 8bit pixels uncompressed to the stream. + /// + /// The pixel format. + /// The to write to. + /// The containing pixel data. + private void Write8Bit(Stream stream, Buffer2D pixels) + where TPixel : struct, IPixel + { + using (IManagedByteBuffer row = this.AllocateRow(pixels.Width, 1)) + { + for (int y = pixels.Height - 1; y >= 0; y--) + { + Span pixelSpan = pixels.GetRowSpan(y); + PixelOperations.Instance.ToGray8Bytes( + this.configuration, + pixelSpan, + row.GetSpan(), + pixelSpan.Length); + stream.Write(row.Array, 0, row.Length()); + } + } + } + + /// + /// Writes the 16bit pixels uncompressed to the stream. + /// + /// The pixel format. + /// The to write to. + /// The containing pixel data. + private void Write16Bit(Stream stream, Buffer2D pixels) + where TPixel : struct, IPixel + { + using (IManagedByteBuffer row = this.AllocateRow(pixels.Width, 2)) + { + for (int y = pixels.Height - 1; y >= 0; y--) + { + Span pixelSpan = pixels.GetRowSpan(y); + PixelOperations.Instance.ToBgra5551Bytes( + this.configuration, + pixelSpan, + row.GetSpan(), + pixelSpan.Length); + stream.Write(row.Array, 0, row.Length()); + } + } + } + /// /// Writes the 24bit pixels uncompressed to the stream. /// @@ -127,5 +187,29 @@ namespace SixLabors.ImageSharp.Formats.Tga } } } + + /// + /// Writes the 32bit pixels uncompressed to the stream. + /// + /// The pixel format. + /// The to write to. + /// The containing pixel data. + private void Write32Bit(Stream stream, Buffer2D pixels) + where TPixel : struct, IPixel + { + using (IManagedByteBuffer row = this.AllocateRow(pixels.Width, 4)) + { + for (int y = pixels.Height - 1; y >= 0; y--) + { + Span pixelSpan = pixels.GetRowSpan(y); + PixelOperations.Instance.ToBgra32Bytes( + this.configuration, + pixelSpan, + row.GetSpan(), + pixelSpan.Length); + stream.Write(row.Array, 0, row.Length()); + } + } + } } } From 462ba485f58a3ecb5f4e6739bd6df2abd9bc0d20 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Thu, 10 Oct 2019 20:55:52 +0200 Subject: [PATCH 13/75] Add support for decoding tga image with palette --- src/ImageSharp/Formats/Tga/TgaDecoderCore.cs | 144 +++++++++++++++---- 1 file changed, 120 insertions(+), 24 deletions(-) diff --git a/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs b/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs index a9002fd8cf..12f81a2690 100644 --- a/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs +++ b/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs @@ -73,9 +73,47 @@ namespace SixLabors.ImageSharp.Formats.Tga { this.ReadFileHeader(stream); + // TODO: parse ID + + // Parse the color map, if present. + if (this.fileHeader.ColorMapType != 0 && this.fileHeader.ColorMapType != 1) + { + TgaThrowHelper.ThrowNotSupportedException($"Unknown tga colormap type {this.fileHeader.ColorMapType} found"); + } + + byte[] palette = null; + int colorMapPixelSizeInBytes = 0; + if (this.fileHeader.ColorMapType == 1) + { + if (this.fileHeader.CMapLength <= 0) + { + TgaThrowHelper.ThrowImageFormatException("Missing tga color map length"); + } + + if (this.fileHeader.CMapDepth <= 0) + { + TgaThrowHelper.ThrowImageFormatException("Missing tga color map depth"); + } + + colorMapPixelSizeInBytes = this.fileHeader.CMapDepth / 8; + palette = new byte[this.fileHeader.CMapLength * colorMapPixelSizeInBytes]; + this.currentStream.Read(palette, this.fileHeader.CMapStart, palette.Length); + } + var image = new Image(this.configuration, this.fileHeader.Width, this.fileHeader.Height, this.metadata); Buffer2D pixels = image.GetRootFramePixelBuffer(); + if (this.fileHeader.ImageType == TgaImageType.ColorMapped) + { + if (palette is null) + { + TgaThrowHelper.ThrowImageFormatException("Tga image is missing a color palette"); + } + + this.ReadPaletted(this.fileHeader.Width, this.fileHeader.Height, pixels, palette, colorMapPixelSizeInBytes); + return image; + } + switch (this.fileHeader.PixelDepth) { case 8: @@ -85,7 +123,7 @@ namespace SixLabors.ImageSharp.Formats.Tga } else { - this.ReadMonoChrome(pixels); + this.ReadMonoChrome(this.fileHeader.Width, this.fileHeader.Height, pixels); } break; @@ -98,7 +136,7 @@ namespace SixLabors.ImageSharp.Formats.Tga } else { - this.ReadBgra16(pixels); + this.ReadBgra16(this.fileHeader.Width, this.fileHeader.Height, pixels); } break; @@ -110,7 +148,7 @@ namespace SixLabors.ImageSharp.Formats.Tga } else { - this.ReadBgr24(pixels); + this.ReadBgr24(this.fileHeader.Width, this.fileHeader.Height, pixels); } break; @@ -122,7 +160,7 @@ namespace SixLabors.ImageSharp.Formats.Tga } else { - this.ReadBgra32(pixels); + this.ReadBgra32(this.fileHeader.Width, this.fileHeader.Height, pixels); } break; @@ -140,41 +178,99 @@ namespace SixLabors.ImageSharp.Formats.Tga } } - private void ReadMonoChrome(Buffer2D pixels) + private void ReadPaletted(int width, int height, Buffer2D pixels, byte[] palette, int pixelSizeInBytes) where TPixel : struct, IPixel { - using (IManagedByteBuffer row = this.memoryAllocator.AllocatePaddedPixelRowBuffer(this.fileHeader.Width, 1, 0)) + using (IManagedByteBuffer row = this.memoryAllocator.AllocateManagedByteBuffer(width, AllocationOptions.Clean)) { - for (int y = 0; y < this.fileHeader.Height; y++) + TPixel color = default; + Span rowSpan = row.GetSpan(); + + for (int y = 0; y < height; y++) { this.currentStream.Read(row); - Span pixelSpan = pixels.GetRowSpan(this.fileHeader.Height - y - 1); + Span pixelRow = pixels.GetRowSpan(height - y - 1); + switch (pixelSizeInBytes) + { + case 1: + for (int x = 0; x < width; x++) + { + int colorIndex = rowSpan[x]; + color.FromGray8(Unsafe.As(ref palette[colorIndex])); + pixelRow[x] = color; + } + + break; + + case 2: + for (int x = 0; x < width; x++) + { + int colorIndex = rowSpan[x]; + color.FromBgra5551(Unsafe.As(ref palette[colorIndex * pixelSizeInBytes])); + pixelRow[x] = color; + } + + break; + + case 3: + for (int x = 0; x < width; x++) + { + int colorIndex = rowSpan[x]; + color.FromBgr24(Unsafe.As(ref palette[colorIndex * pixelSizeInBytes])); + pixelRow[x] = color; + } + + break; + + case 4: + for (int x = 0; x < width; x++) + { + int colorIndex = rowSpan[x]; + color.FromBgra32(Unsafe.As(ref palette[colorIndex * pixelSizeInBytes])); + pixelRow[x] = color; + } + + break; + } + } + } + } + + private void ReadMonoChrome(int width, int height, Buffer2D pixels) + where TPixel : struct, IPixel + { + using (IManagedByteBuffer row = this.memoryAllocator.AllocatePaddedPixelRowBuffer(width, 1, 0)) + { + for (int y = 0; y < height; y++) + { + this.currentStream.Read(row); + Span pixelSpan = pixels.GetRowSpan(height - y - 1); PixelOperations.Instance.FromGray8Bytes( this.configuration, row.GetSpan(), pixelSpan, - this.fileHeader.Width); + width); } } } - private void ReadBgra16(Buffer2D pixels) + private void ReadBgra16(int width, int height, Buffer2D pixels) where TPixel : struct, IPixel { - using (IManagedByteBuffer row = this.memoryAllocator.AllocatePaddedPixelRowBuffer(this.fileHeader.Width, 2, 0)) - using (IMemoryOwner bgraRow = this.memoryAllocator.Allocate(this.fileHeader.Width)) + using (IManagedByteBuffer row = this.memoryAllocator.AllocatePaddedPixelRowBuffer(width, 2, 0)) + using (IMemoryOwner bgraRow = this.memoryAllocator.Allocate(width)) { Span bgraRowSpan = bgraRow.GetSpan(); long currentPosition = this.currentStream.Position; for (int y = 0; y < this.fileHeader.Height; y++) { this.currentStream.Read(row); - Span pixelSpan = pixels.GetRowSpan(this.fileHeader.Height - y - 1); + Span pixelSpan = pixels.GetRowSpan(height - y - 1); PixelOperations.Instance.FromBgra5551Bytes( this.configuration, row.GetSpan(), pixelSpan, - this.fileHeader.Width); + width); } // We need to set each alpha component value to fully opaque. @@ -182,38 +278,38 @@ namespace SixLabors.ImageSharp.Formats.Tga } } - private void ReadBgr24(Buffer2D pixels) + private void ReadBgr24(int width, int height, Buffer2D pixels) where TPixel : struct, IPixel { - using (IManagedByteBuffer row = this.memoryAllocator.AllocatePaddedPixelRowBuffer(this.fileHeader.Width, 3, 0)) + using (IManagedByteBuffer row = this.memoryAllocator.AllocatePaddedPixelRowBuffer(width, 3, 0)) { - for (int y = 0; y < this.fileHeader.Height; y++) + for (int y = 0; y < height; y++) { this.currentStream.Read(row); - Span pixelSpan = pixels.GetRowSpan(this.fileHeader.Height - y - 1); + Span pixelSpan = pixels.GetRowSpan(height - y - 1); PixelOperations.Instance.FromBgr24Bytes( this.configuration, row.GetSpan(), pixelSpan, - this.fileHeader.Width); + width); } } } - private void ReadBgra32(Buffer2D pixels) + private void ReadBgra32(int width, int height, Buffer2D pixels) where TPixel : struct, IPixel { - using (IManagedByteBuffer row = this.memoryAllocator.AllocatePaddedPixelRowBuffer(this.fileHeader.Width, 4, 0)) + using (IManagedByteBuffer row = this.memoryAllocator.AllocatePaddedPixelRowBuffer(width, 4, 0)) { - for (int y = 0; y < this.fileHeader.Height; y++) + for (int y = 0; y < height; y++) { this.currentStream.Read(row); - Span pixelSpan = pixels.GetRowSpan(this.fileHeader.Height - y - 1); + Span pixelSpan = pixels.GetRowSpan(height - y - 1); PixelOperations.Instance.FromBgra32Bytes( this.configuration, row.GetSpan(), pixelSpan, - this.fileHeader.Width); + width); } } } From dce04067d81e46e9f2084374a47ed97a5deb5f14 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Fri, 11 Oct 2019 19:55:59 +0200 Subject: [PATCH 14/75] Add support for decoding rle tga with palette --- src/ImageSharp/Formats/Tga/TgaDecoderCore.cs | 84 +++++++++++++------- 1 file changed, 57 insertions(+), 27 deletions(-) diff --git a/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs b/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs index 12f81a2690..aa6ccb0305 100644 --- a/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs +++ b/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs @@ -72,8 +72,7 @@ namespace SixLabors.ImageSharp.Formats.Tga try { this.ReadFileHeader(stream); - - // TODO: parse ID + this.currentStream.Skip(this.fileHeader.IdLength); // Parse the color map, if present. if (this.fileHeader.ColorMapType != 0 && this.fileHeader.ColorMapType != 1) @@ -81,9 +80,12 @@ namespace SixLabors.ImageSharp.Formats.Tga TgaThrowHelper.ThrowNotSupportedException($"Unknown tga colormap type {this.fileHeader.ColorMapType} found"); } + var image = new Image(this.configuration, this.fileHeader.Width, this.fileHeader.Height, this.metadata); + Buffer2D pixels = image.GetRootFramePixelBuffer(); + byte[] palette = null; int colorMapPixelSizeInBytes = 0; - if (this.fileHeader.ColorMapType == 1) + if (this.fileHeader.ColorMapType is 1) { if (this.fileHeader.CMapLength <= 0) { @@ -98,19 +100,16 @@ namespace SixLabors.ImageSharp.Formats.Tga colorMapPixelSizeInBytes = this.fileHeader.CMapDepth / 8; palette = new byte[this.fileHeader.CMapLength * colorMapPixelSizeInBytes]; this.currentStream.Read(palette, this.fileHeader.CMapStart, palette.Length); - } - var image = new Image(this.configuration, this.fileHeader.Width, this.fileHeader.Height, this.metadata); - Buffer2D pixels = image.GetRootFramePixelBuffer(); - - if (this.fileHeader.ImageType == TgaImageType.ColorMapped) - { - if (palette is null) + if (this.fileHeader.ImageType is TgaImageType.RleColorMapped) + { + this.ReadPalettedRle(this.fileHeader.Width, this.fileHeader.Height, pixels, palette, colorMapPixelSizeInBytes); + } + else { - TgaThrowHelper.ThrowImageFormatException("Tga image is missing a color palette"); + this.ReadPaletted(this.fileHeader.Width, this.fileHeader.Height, pixels, palette, colorMapPixelSizeInBytes); } - this.ReadPaletted(this.fileHeader.Width, this.fileHeader.Height, pixels, palette, colorMapPixelSizeInBytes); return image; } @@ -128,6 +127,7 @@ namespace SixLabors.ImageSharp.Formats.Tga break; + case 15: case 16: if (this.fileHeader.ImageType.IsRunLengthEncoded()) { @@ -178,7 +178,7 @@ namespace SixLabors.ImageSharp.Formats.Tga } } - private void ReadPaletted(int width, int height, Buffer2D pixels, byte[] palette, int pixelSizeInBytes) + private void ReadPaletted(int width, int height, Buffer2D pixels, byte[] palette, int colorMapPixelSizeInBytes) where TPixel : struct, IPixel { using (IManagedByteBuffer row = this.memoryAllocator.AllocateManagedByteBuffer(width, AllocationOptions.Clean)) @@ -190,23 +190,13 @@ namespace SixLabors.ImageSharp.Formats.Tga { this.currentStream.Read(row); Span pixelRow = pixels.GetRowSpan(height - y - 1); - switch (pixelSizeInBytes) + switch (colorMapPixelSizeInBytes) { - case 1: - for (int x = 0; x < width; x++) - { - int colorIndex = rowSpan[x]; - color.FromGray8(Unsafe.As(ref palette[colorIndex])); - pixelRow[x] = color; - } - - break; - case 2: for (int x = 0; x < width; x++) { int colorIndex = rowSpan[x]; - color.FromBgra5551(Unsafe.As(ref palette[colorIndex * pixelSizeInBytes])); + color.FromBgra5551(Unsafe.As(ref palette[colorIndex * colorMapPixelSizeInBytes])); pixelRow[x] = color; } @@ -216,7 +206,7 @@ namespace SixLabors.ImageSharp.Formats.Tga for (int x = 0; x < width; x++) { int colorIndex = rowSpan[x]; - color.FromBgr24(Unsafe.As(ref palette[colorIndex * pixelSizeInBytes])); + color.FromBgr24(Unsafe.As(ref palette[colorIndex * colorMapPixelSizeInBytes])); pixelRow[x] = color; } @@ -226,7 +216,7 @@ namespace SixLabors.ImageSharp.Formats.Tga for (int x = 0; x < width; x++) { int colorIndex = rowSpan[x]; - color.FromBgra32(Unsafe.As(ref palette[colorIndex * pixelSizeInBytes])); + color.FromBgra32(Unsafe.As(ref palette[colorIndex * colorMapPixelSizeInBytes])); pixelRow[x] = color; } @@ -236,6 +226,45 @@ namespace SixLabors.ImageSharp.Formats.Tga } } + private void ReadPalettedRle(int width, int height, Buffer2D pixels, byte[] palette, int colorMapPixelSizeInBytes) + where TPixel : struct, IPixel + { + int bytesPerPixel = 1; + using (IMemoryOwner buffer = this.memoryAllocator.Allocate(width * height * bytesPerPixel, AllocationOptions.Clean)) + { + TPixel color = default; + Span bufferSpan = buffer.GetSpan(); + this.UncompressRle(width, height, bufferSpan, bytesPerPixel: 1); + + for (int y = 0; y < height; y++) + { + Span pixelRow = pixels.GetRowSpan(this.fileHeader.Height - y - 1); + int rowStartIdx = y * width * bytesPerPixel; + for (int x = 0; x < width; x++) + { + int idx = rowStartIdx + x; + switch (colorMapPixelSizeInBytes) + { + case 1: + color.FromGray8(Unsafe.As(ref palette[bufferSpan[idx] * colorMapPixelSizeInBytes])); + break; + case 2: + color.FromBgra5551(Unsafe.As(ref palette[bufferSpan[idx] * colorMapPixelSizeInBytes])); + break; + case 3: + color.FromBgr24(Unsafe.As(ref palette[bufferSpan[idx] * colorMapPixelSizeInBytes])); + break; + case 4: + color.FromBgra32(Unsafe.As(ref palette[bufferSpan[idx] * colorMapPixelSizeInBytes])); + break; + } + + pixelRow[x] = color; + } + } + } + } + private void ReadMonoChrome(int width, int height, Buffer2D pixels) where TPixel : struct, IPixel { @@ -335,6 +364,7 @@ namespace SixLabors.ImageSharp.Formats.Tga color.FromGray8(Unsafe.As(ref bufferSpan[idx])); break; case 2: + // Set bit 16 to 1, to treat it as opaque for Bgra5551. bufferSpan[idx + 1] = (byte)(bufferSpan[idx + 1] | 128); color.FromBgra5551(Unsafe.As(ref bufferSpan[idx])); break; From 296e75bb3f793572e0612221f951add158109c35 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Fri, 11 Oct 2019 21:27:08 +0200 Subject: [PATCH 15/75] Change test images, add additional tests --- .../Formats/Tga/TgaDecoderTests.cs | 60 +++++++++++++++++++ tests/ImageSharp.Tests/TestImages.cs | 21 ++++--- tests/Images/Input/Tga/bike_16bit.tga | 3 - tests/Images/Input/Tga/bike_16bit_rle.tga | 3 - tests/Images/Input/Tga/bike_24bit.tga | 3 - tests/Images/Input/Tga/bike_24bit_rle.tga | 3 - tests/Images/Input/Tga/bike_32bit.tga | 3 - tests/Images/Input/Tga/bike_32bit_rle.tga | 3 - tests/Images/Input/Tga/bike_8bit.tga | 3 - tests/Images/Input/Tga/bike_8bit_rle.tga | 3 - tests/Images/Input/Tga/ccm8.tga | 3 + tests/Images/Input/Tga/rgb15.tga | 3 + tests/Images/Input/Tga/rgb15rle.tga | 3 + tests/Images/Input/Tga/targa.png | 3 + tests/Images/Input/Tga/targa_16bit.tga | 3 + tests/Images/Input/Tga/targa_16bit_pal.tga | 3 + tests/Images/Input/Tga/targa_16bit_rle.tga | 3 + tests/Images/Input/Tga/targa_24bit.tga | 3 + tests/Images/Input/Tga/targa_24bit_pal.tga | 3 + tests/Images/Input/Tga/targa_24bit_rle.tga | 3 + tests/Images/Input/Tga/targa_32bit.tga | 3 + tests/Images/Input/Tga/targa_32bit_rle.tga | 3 + tests/Images/Input/Tga/targa_8bit.tga | 3 + tests/Images/Input/Tga/targa_8bit_rle.tga | 3 + 24 files changed, 115 insertions(+), 32 deletions(-) delete mode 100644 tests/Images/Input/Tga/bike_16bit.tga delete mode 100644 tests/Images/Input/Tga/bike_16bit_rle.tga delete mode 100644 tests/Images/Input/Tga/bike_24bit.tga delete mode 100644 tests/Images/Input/Tga/bike_24bit_rle.tga delete mode 100644 tests/Images/Input/Tga/bike_32bit.tga delete mode 100644 tests/Images/Input/Tga/bike_32bit_rle.tga delete mode 100644 tests/Images/Input/Tga/bike_8bit.tga delete mode 100644 tests/Images/Input/Tga/bike_8bit_rle.tga create mode 100644 tests/Images/Input/Tga/ccm8.tga create mode 100644 tests/Images/Input/Tga/rgb15.tga create mode 100644 tests/Images/Input/Tga/rgb15rle.tga create mode 100644 tests/Images/Input/Tga/targa.png create mode 100644 tests/Images/Input/Tga/targa_16bit.tga create mode 100644 tests/Images/Input/Tga/targa_16bit_pal.tga create mode 100644 tests/Images/Input/Tga/targa_16bit_rle.tga create mode 100644 tests/Images/Input/Tga/targa_24bit.tga create mode 100644 tests/Images/Input/Tga/targa_24bit_pal.tga create mode 100644 tests/Images/Input/Tga/targa_24bit_rle.tga create mode 100644 tests/Images/Input/Tga/targa_32bit.tga create mode 100644 tests/Images/Input/Tga/targa_32bit_rle.tga create mode 100644 tests/Images/Input/Tga/targa_8bit.tga create mode 100644 tests/Images/Input/Tga/targa_8bit_rle.tga diff --git a/tests/ImageSharp.Tests/Formats/Tga/TgaDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Tga/TgaDecoderTests.cs index 47983a3ade..a2a50ead2d 100644 --- a/tests/ImageSharp.Tests/Formats/Tga/TgaDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Tga/TgaDecoderTests.cs @@ -31,6 +31,30 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tga } } + [Theory] + [WithFile(Bit15, PixelTypes.Rgba32)] + public void TgaDecoder_CanDecode_Uncompressed_15Bit(TestImageProvider provider) + where TPixel : struct, IPixel + { + using (Image image = provider.GetImage(new TgaDecoder())) + { + image.DebugSave(provider); + CompareWithReferenceDecoder(provider, image); + } + } + + [Theory] + [WithFile(Bit15Rle, PixelTypes.Rgba32)] + public void TgaDecoder_CanDecode_RunLengthEncoded_15Bit(TestImageProvider provider) + where TPixel : struct, IPixel + { + using (Image image = provider.GetImage(new TgaDecoder())) + { + image.DebugSave(provider); + CompareWithReferenceDecoder(provider, image); + } + } + [Theory] [WithFile(Bit16, PixelTypes.Rgba32)] public void TgaDecoder_CanDecode_Uncompressed_16Bit(TestImageProvider provider) @@ -43,6 +67,18 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tga } } + [Theory] + [WithFile(Bit16PalRle, PixelTypes.Rgba32)] + public void TgaDecoder_CanDecode_RunLengthEncoded_WithPalette_16Bit(TestImageProvider provider) + where TPixel : struct, IPixel + { + using (Image image = provider.GetImage(new TgaDecoder())) + { + image.DebugSave(provider); + CompareWithReferenceDecoder(provider, image); + } + } + [Theory] [WithFile(Bit24, PixelTypes.Rgba32)] public void TgaDecoder_CanDecode_Uncompressed_24Bit(TestImageProvider provider) @@ -115,6 +151,30 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tga } } + [Theory] + [WithFile(Bit16Pal, PixelTypes.Rgba32)] + public void TgaDecoder_CanDecode_WithPalette_16Bit(TestImageProvider provider) + where TPixel : struct, IPixel + { + using (Image image = provider.GetImage(new TgaDecoder())) + { + image.DebugSave(provider); + CompareWithReferenceDecoder(provider, image); + } + } + + [Theory] + [WithFile(Bit24Pal, PixelTypes.Rgba32)] + public void TgaDecoder_CanDecode_WithPalette_24Bit(TestImageProvider provider) + where TPixel : struct, IPixel + { + using (Image image = provider.GetImage(new TgaDecoder())) + { + image.DebugSave(provider); + CompareWithReferenceDecoder(provider, image); + } + } + private void CompareWithReferenceDecoder(TestImageProvider provider, Image image) where TPixel : struct, IPixel { diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs index 51ca48c375..8499e51ebd 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/tests/ImageSharp.Tests/TestImages.cs @@ -368,14 +368,19 @@ namespace SixLabors.ImageSharp.Tests public static class Tga { - public const string Bit32 = "Tga/bike_32bit.tga"; - public const string Bit24 = "Tga/bike_24bit.tga"; - public const string Bit16 = "Tga/bike_16bit.tga"; - public const string Grey = "Tga/bike_8bit.tga"; - public const string Bit32Rle = "Tga/bike_32bit_rle.tga"; - public const string Bit24Rle = "Tga/bike_24bit_rle.tga"; - public const string Bit16Rle = "Tga/bike_16bit_rle.tga"; - public const string GreyRle = "Tga/bike_8bit_rle.tga"; + public const string Bit15 = "Tga/rgb15.tga"; + public const string Bit15Rle = "Tga/rgb15rle.tga"; + public const string Bit16 = "Tga/targa_16bit.tga"; + public const string Bit16PalRle = "Tga/ccm8.tga"; + public const string Bit24 = "Tga/targa_24bit.tga"; + public const string Bit32 = "Tga/targa_32bit.tga"; + public const string Grey = "Tga/targa_8bit.tga"; + public const string GreyRle = "Tga/targa_8bit_rle.tga"; + public const string Bit16Rle = "Tga/targa_16bit_rle.tga"; + public const string Bit24Rle = "Tga/targa_24bit_rle.tga"; + public const string Bit32Rle = "Tga/targa_32bit_rle.tga"; + public const string Bit16Pal = "Tga/targa_16bit_pal.tga"; + public const string Bit24Pal = "Tga/targa_24bit_pal.tga"; } } } diff --git a/tests/Images/Input/Tga/bike_16bit.tga b/tests/Images/Input/Tga/bike_16bit.tga deleted file mode 100644 index 00489d94a0..0000000000 --- a/tests/Images/Input/Tga/bike_16bit.tga +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:3b2bc922e2397ce8cd8b1e7792f20e2c7edad68ad8fac037a5b91bba5148a80b -size 97518 diff --git a/tests/Images/Input/Tga/bike_16bit_rle.tga b/tests/Images/Input/Tga/bike_16bit_rle.tga deleted file mode 100644 index 47c37c50b6..0000000000 --- a/tests/Images/Input/Tga/bike_16bit_rle.tga +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:c9f7381f04b3ce23e3144f6b5789d7eccc0c0f010fc901f42d01171556181c1c -size 56938 diff --git a/tests/Images/Input/Tga/bike_24bit.tga b/tests/Images/Input/Tga/bike_24bit.tga deleted file mode 100644 index a5e46f794d..0000000000 --- a/tests/Images/Input/Tga/bike_24bit.tga +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:26d55794c9b012b07517d1129630ccc35ca56015cbdd481debea00826130f925 -size 146268 diff --git a/tests/Images/Input/Tga/bike_24bit_rle.tga b/tests/Images/Input/Tga/bike_24bit_rle.tga deleted file mode 100644 index 65dfb9a1ab..0000000000 --- a/tests/Images/Input/Tga/bike_24bit_rle.tga +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:88f7936db92cf536c9656a8ff00c25842c71b93823e05322a1efc3aef2c0a80e -size 106721 diff --git a/tests/Images/Input/Tga/bike_32bit.tga b/tests/Images/Input/Tga/bike_32bit.tga deleted file mode 100644 index 70e755d1f1..0000000000 --- a/tests/Images/Input/Tga/bike_32bit.tga +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:cb3774b695d2409f3e7584dc67ce7b3d8a75377c194e7f33e4cc315b3ae36a35 -size 195018 diff --git a/tests/Images/Input/Tga/bike_32bit_rle.tga b/tests/Images/Input/Tga/bike_32bit_rle.tga deleted file mode 100644 index 967e9ce7ee..0000000000 --- a/tests/Images/Input/Tga/bike_32bit_rle.tga +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:815e9f32f29a8c51bbd0f2c7d507d6331a709f157fb7bd65188fd07677764c59 -size 141452 diff --git a/tests/Images/Input/Tga/bike_8bit.tga b/tests/Images/Input/Tga/bike_8bit.tga deleted file mode 100644 index ba11619a5e..0000000000 --- a/tests/Images/Input/Tga/bike_8bit.tga +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:75f3ce893f38d90767c692eb9628026c0421330934a5cbb686813ec26a9606d2 -size 48768 diff --git a/tests/Images/Input/Tga/bike_8bit_rle.tga b/tests/Images/Input/Tga/bike_8bit_rle.tga deleted file mode 100644 index 0c6ee06a44..0000000000 --- a/tests/Images/Input/Tga/bike_8bit_rle.tga +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:1531a0f8a95fb4a57e0bde1842b6ee65f5cabfc62ad883d1f9dbc2c5616e498f -size 37259 diff --git a/tests/Images/Input/Tga/ccm8.tga b/tests/Images/Input/Tga/ccm8.tga new file mode 100644 index 0000000000..ab92516355 --- /dev/null +++ b/tests/Images/Input/Tga/ccm8.tga @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:67b3ffaaa75561d8b959258d6b26a1f9ca3228b02a3df98a614ea43241aaea52 +size 9271 diff --git a/tests/Images/Input/Tga/rgb15.tga b/tests/Images/Input/Tga/rgb15.tga new file mode 100644 index 0000000000..870295b45a --- /dev/null +++ b/tests/Images/Input/Tga/rgb15.tga @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:390cfff190bc41386fa134eca70ea0d3ffdc32a285c73278ed34046b09c46c9d +size 80537 diff --git a/tests/Images/Input/Tga/rgb15rle.tga b/tests/Images/Input/Tga/rgb15rle.tga new file mode 100644 index 0000000000..a45940fc98 --- /dev/null +++ b/tests/Images/Input/Tga/rgb15rle.tga @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3219186fc9a9f859c99c2b31cf81e7f0ab4292956d22fc659e714d0cdb51cfa7 +size 19941 diff --git a/tests/Images/Input/Tga/targa.png b/tests/Images/Input/Tga/targa.png new file mode 100644 index 0000000000..c4933c0ebd --- /dev/null +++ b/tests/Images/Input/Tga/targa.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:abc382cec34a04815bd53ad30707c6cdeeceece8731244244e2ab91026d60957 +size 106139 diff --git a/tests/Images/Input/Tga/targa_16bit.tga b/tests/Images/Input/Tga/targa_16bit.tga new file mode 100644 index 0000000000..6c4143c2ee --- /dev/null +++ b/tests/Images/Input/Tga/targa_16bit.tga @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f3adea897f8843b73d0042e23bdfbd0115a7f534df90699134e768df57061f46 +size 70518 diff --git a/tests/Images/Input/Tga/targa_16bit_pal.tga b/tests/Images/Input/Tga/targa_16bit_pal.tga new file mode 100644 index 0000000000..b25def7798 --- /dev/null +++ b/tests/Images/Input/Tga/targa_16bit_pal.tga @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:97a4ac0cecfe69e1b5c74db5288fb8ca3bf29968e3b5288c4e5ce03bb4f06915 +size 35780 diff --git a/tests/Images/Input/Tga/targa_16bit_rle.tga b/tests/Images/Input/Tga/targa_16bit_rle.tga new file mode 100644 index 0000000000..49ef0e998b --- /dev/null +++ b/tests/Images/Input/Tga/targa_16bit_rle.tga @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:47d7ebf37672ea846ce071155733697e34083de36aeaafaebd78317708feffde +size 19566 diff --git a/tests/Images/Input/Tga/targa_24bit.tga b/tests/Images/Input/Tga/targa_24bit.tga new file mode 100644 index 0000000000..82c22e2425 --- /dev/null +++ b/tests/Images/Input/Tga/targa_24bit.tga @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:35921b6250e43ba8e1fb125ebe4939a57a67efb0aa9eac0d3605bf90e93309b1 +size 105768 diff --git a/tests/Images/Input/Tga/targa_24bit_pal.tga b/tests/Images/Input/Tga/targa_24bit_pal.tga new file mode 100644 index 0000000000..abfbf588a6 --- /dev/null +++ b/tests/Images/Input/Tga/targa_24bit_pal.tga @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4926969e5ae6c9af38d33fa18429de756c48d06edd87c5d27cb8d5232b066ab2 +size 36036 diff --git a/tests/Images/Input/Tga/targa_24bit_rle.tga b/tests/Images/Input/Tga/targa_24bit_rle.tga new file mode 100644 index 0000000000..d6af44c0a6 --- /dev/null +++ b/tests/Images/Input/Tga/targa_24bit_rle.tga @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:56a79ab92d84bbe8c7efbc2711051938fa3ba97b48830aea0cb1dafd7d1fe222 +size 37711 diff --git a/tests/Images/Input/Tga/targa_32bit.tga b/tests/Images/Input/Tga/targa_32bit.tga new file mode 100644 index 0000000000..8b2a57c810 --- /dev/null +++ b/tests/Images/Input/Tga/targa_32bit.tga @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e3a220619e25e86bab01b01a2e231ee64fd004e047fa86016bf68de576877352 +size 141018 diff --git a/tests/Images/Input/Tga/targa_32bit_rle.tga b/tests/Images/Input/Tga/targa_32bit_rle.tga new file mode 100644 index 0000000000..b021a2cc15 --- /dev/null +++ b/tests/Images/Input/Tga/targa_32bit_rle.tga @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f415d6a246909c18fe604248ab5fe27c74aff9a63df58d8cdeab7c4c3cbe056a +size 49994 diff --git a/tests/Images/Input/Tga/targa_8bit.tga b/tests/Images/Input/Tga/targa_8bit.tga new file mode 100644 index 0000000000..9b0512971e --- /dev/null +++ b/tests/Images/Input/Tga/targa_8bit.tga @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6aaae46d0e55f32a72732fbe48ed9dc4044c53432999ab66e9475e45e40f0133 +size 35268 diff --git a/tests/Images/Input/Tga/targa_8bit_rle.tga b/tests/Images/Input/Tga/targa_8bit_rle.tga new file mode 100644 index 0000000000..d6a66def15 --- /dev/null +++ b/tests/Images/Input/Tga/targa_8bit_rle.tga @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a18d7fd98bc9ab62276103b4e7b474be93b3d7241f4f06aa564e32150e205a71 +size 13145 From e12873c115330332980b2c093f57cf70d812d651 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Sat, 12 Oct 2019 21:23:44 +0200 Subject: [PATCH 16/75] Add tests for topleft origin --- .../Formats/Tga/TgaDecoderTests.cs | 24 +++++++++++++++++++ tests/ImageSharp.Tests/TestImages.cs | 2 ++ .../Input/Tga/targa_24bit_origin_topleft.tga | 3 +++ .../Tga/targa_24bit_rle_origin_topleft.tga | 3 +++ 4 files changed, 32 insertions(+) create mode 100644 tests/Images/Input/Tga/targa_24bit_origin_topleft.tga create mode 100644 tests/Images/Input/Tga/targa_24bit_rle_origin_topleft.tga diff --git a/tests/ImageSharp.Tests/Formats/Tga/TgaDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Tga/TgaDecoderTests.cs index a2a50ead2d..e1b23a48e2 100644 --- a/tests/ImageSharp.Tests/Formats/Tga/TgaDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Tga/TgaDecoderTests.cs @@ -91,6 +91,30 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tga } } + [Theory] + [WithFile(Bit24RleTopLeft, PixelTypes.Rgba32)] + public void TgaDecoder_CanDecode_RunLengthEncoded_WithTopLeftOrigin_24Bit(TestImageProvider provider) + where TPixel : struct, IPixel + { + using (Image image = provider.GetImage(new TgaDecoder())) + { + image.DebugSave(provider); + CompareWithReferenceDecoder(provider, image); + } + } + + [Theory] + [WithFile(Bit24TopLeft, PixelTypes.Rgba32)] + public void TgaDecoder_CanDecode_Uncompressed_WithTopLeftOrigin_24Bit(TestImageProvider provider) + where TPixel : struct, IPixel + { + using (Image image = provider.GetImage(new TgaDecoder())) + { + image.DebugSave(provider); + CompareWithReferenceDecoder(provider, image); + } + } + [Theory] [WithFile(Bit32, PixelTypes.Rgba32)] public void TgaDecoder_CanDecode_Uncompressed_32Bit(TestImageProvider provider) diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs index 8499e51ebd..e60219eed5 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/tests/ImageSharp.Tests/TestImages.cs @@ -373,6 +373,8 @@ namespace SixLabors.ImageSharp.Tests public const string Bit16 = "Tga/targa_16bit.tga"; public const string Bit16PalRle = "Tga/ccm8.tga"; public const string Bit24 = "Tga/targa_24bit.tga"; + public const string Bit24TopLeft = "Tga/targa_24bit_origin_topleft.tga"; + public const string Bit24RleTopLeft = "Tga/targa_24bit_rle_origin_topleft.tga"; public const string Bit32 = "Tga/targa_32bit.tga"; public const string Grey = "Tga/targa_8bit.tga"; public const string GreyRle = "Tga/targa_8bit_rle.tga"; diff --git a/tests/Images/Input/Tga/targa_24bit_origin_topleft.tga b/tests/Images/Input/Tga/targa_24bit_origin_topleft.tga new file mode 100644 index 0000000000..b8c4071745 --- /dev/null +++ b/tests/Images/Input/Tga/targa_24bit_origin_topleft.tga @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b1c52e538a7d134b20ff57e44b7e304d1b5effacac03a4481d169702796fb195 +size 36062 diff --git a/tests/Images/Input/Tga/targa_24bit_rle_origin_topleft.tga b/tests/Images/Input/Tga/targa_24bit_rle_origin_topleft.tga new file mode 100644 index 0000000000..9310c51a70 --- /dev/null +++ b/tests/Images/Input/Tga/targa_24bit_rle_origin_topleft.tga @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:30e8b6d01ebf9d227d2e9dcdd7b2641bf8f335107110dfff780351870217d4f4 +size 37102 From deb1bf5284948bad4dcb0fb4a9d4f600326b1373 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Sun, 13 Oct 2019 10:42:25 +0200 Subject: [PATCH 17/75] Add support for images with top left origin --- src/ImageSharp/Formats/Tga/TgaDecoderCore.cs | 102 ++++++++++-------- .../Formats/Tga/TgaDecoderTests.cs | 2 +- tests/ImageSharp.Tests/TestImages.cs | 2 +- ...tga => targa_24bit_pal_origin_topleft.tga} | 0 4 files changed, 61 insertions(+), 45 deletions(-) rename tests/Images/Input/Tga/{targa_24bit_origin_topleft.tga => targa_24bit_pal_origin_topleft.tga} (100%) diff --git a/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs b/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs index aa6ccb0305..64635ea8a9 100644 --- a/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs +++ b/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs @@ -71,7 +71,7 @@ namespace SixLabors.ImageSharp.Formats.Tga { try { - this.ReadFileHeader(stream); + bool inverted = this.ReadFileHeader(stream); this.currentStream.Skip(this.fileHeader.IdLength); // Parse the color map, if present. @@ -83,8 +83,6 @@ namespace SixLabors.ImageSharp.Formats.Tga var image = new Image(this.configuration, this.fileHeader.Width, this.fileHeader.Height, this.metadata); Buffer2D pixels = image.GetRootFramePixelBuffer(); - byte[] palette = null; - int colorMapPixelSizeInBytes = 0; if (this.fileHeader.ColorMapType is 1) { if (this.fileHeader.CMapLength <= 0) @@ -97,17 +95,17 @@ namespace SixLabors.ImageSharp.Formats.Tga TgaThrowHelper.ThrowImageFormatException("Missing tga color map depth"); } - colorMapPixelSizeInBytes = this.fileHeader.CMapDepth / 8; - palette = new byte[this.fileHeader.CMapLength * colorMapPixelSizeInBytes]; + int colorMapPixelSizeInBytes = this.fileHeader.CMapDepth / 8; + var palette = new byte[this.fileHeader.CMapLength * colorMapPixelSizeInBytes]; this.currentStream.Read(palette, this.fileHeader.CMapStart, palette.Length); if (this.fileHeader.ImageType is TgaImageType.RleColorMapped) { - this.ReadPalettedRle(this.fileHeader.Width, this.fileHeader.Height, pixels, palette, colorMapPixelSizeInBytes); + this.ReadPalettedRle(this.fileHeader.Width, this.fileHeader.Height, pixels, palette, colorMapPixelSizeInBytes, inverted); } else { - this.ReadPaletted(this.fileHeader.Width, this.fileHeader.Height, pixels, palette, colorMapPixelSizeInBytes); + this.ReadPaletted(this.fileHeader.Width, this.fileHeader.Height, pixels, palette, colorMapPixelSizeInBytes, inverted); } return image; @@ -118,11 +116,11 @@ namespace SixLabors.ImageSharp.Formats.Tga case 8: if (this.fileHeader.ImageType.IsRunLengthEncoded()) { - this.ReadRle(this.fileHeader.Width, this.fileHeader.Height, pixels, 1); + this.ReadRle(this.fileHeader.Width, this.fileHeader.Height, pixels, 1, inverted); } else { - this.ReadMonoChrome(this.fileHeader.Width, this.fileHeader.Height, pixels); + this.ReadMonoChrome(this.fileHeader.Width, this.fileHeader.Height, pixels, inverted); } break; @@ -132,11 +130,11 @@ namespace SixLabors.ImageSharp.Formats.Tga if (this.fileHeader.ImageType.IsRunLengthEncoded()) { long currentPosition = this.currentStream.Position; - this.ReadRle(this.fileHeader.Width, this.fileHeader.Height, pixels, 2); + this.ReadRle(this.fileHeader.Width, this.fileHeader.Height, pixels, 2, inverted); } else { - this.ReadBgra16(this.fileHeader.Width, this.fileHeader.Height, pixels); + this.ReadBgra16(this.fileHeader.Width, this.fileHeader.Height, pixels, inverted); } break; @@ -144,11 +142,11 @@ namespace SixLabors.ImageSharp.Formats.Tga case 24: if (this.fileHeader.ImageType.IsRunLengthEncoded()) { - this.ReadRle(this.fileHeader.Width, this.fileHeader.Height, pixels, 3); + this.ReadRle(this.fileHeader.Width, this.fileHeader.Height, pixels, 3, inverted); } else { - this.ReadBgr24(this.fileHeader.Width, this.fileHeader.Height, pixels); + this.ReadBgr24(this.fileHeader.Width, this.fileHeader.Height, pixels, inverted); } break; @@ -156,11 +154,11 @@ namespace SixLabors.ImageSharp.Formats.Tga case 32: if (this.fileHeader.ImageType.IsRunLengthEncoded()) { - this.ReadRle(this.fileHeader.Width, this.fileHeader.Height, pixels, 4); + this.ReadRle(this.fileHeader.Width, this.fileHeader.Height, pixels, 4, inverted); } else { - this.ReadBgra32(this.fileHeader.Width, this.fileHeader.Height, pixels); + this.ReadBgra32(this.fileHeader.Width, this.fileHeader.Height, pixels, inverted); } break; @@ -178,7 +176,7 @@ namespace SixLabors.ImageSharp.Formats.Tga } } - private void ReadPaletted(int width, int height, Buffer2D pixels, byte[] palette, int colorMapPixelSizeInBytes) + private void ReadPaletted(int width, int height, Buffer2D pixels, byte[] palette, int colorMapPixelSizeInBytes, bool inverted) where TPixel : struct, IPixel { using (IManagedByteBuffer row = this.memoryAllocator.AllocateManagedByteBuffer(width, AllocationOptions.Clean)) @@ -189,7 +187,8 @@ namespace SixLabors.ImageSharp.Formats.Tga for (int y = 0; y < height; y++) { this.currentStream.Read(row); - Span pixelRow = pixels.GetRowSpan(height - y - 1); + int newY = Invert(y, height, inverted); + Span pixelRow = pixels.GetRowSpan(newY); switch (colorMapPixelSizeInBytes) { case 2: @@ -226,7 +225,7 @@ namespace SixLabors.ImageSharp.Formats.Tga } } - private void ReadPalettedRle(int width, int height, Buffer2D pixels, byte[] palette, int colorMapPixelSizeInBytes) + private void ReadPalettedRle(int width, int height, Buffer2D pixels, byte[] palette, int colorMapPixelSizeInBytes, bool inverted) where TPixel : struct, IPixel { int bytesPerPixel = 1; @@ -238,7 +237,8 @@ namespace SixLabors.ImageSharp.Formats.Tga for (int y = 0; y < height; y++) { - Span pixelRow = pixels.GetRowSpan(this.fileHeader.Height - y - 1); + int newY = Invert(y, height, inverted); + Span pixelRow = pixels.GetRowSpan(newY); int rowStartIdx = y * width * bytesPerPixel; for (int x = 0; x < width; x++) { @@ -265,7 +265,7 @@ namespace SixLabors.ImageSharp.Formats.Tga } } - private void ReadMonoChrome(int width, int height, Buffer2D pixels) + private void ReadMonoChrome(int width, int height, Buffer2D pixels, bool inverted) where TPixel : struct, IPixel { using (IManagedByteBuffer row = this.memoryAllocator.AllocatePaddedPixelRowBuffer(width, 1, 0)) @@ -273,7 +273,8 @@ namespace SixLabors.ImageSharp.Formats.Tga for (int y = 0; y < height; y++) { this.currentStream.Read(row); - Span pixelSpan = pixels.GetRowSpan(height - y - 1); + int newY = Invert(y, height, inverted); + Span pixelSpan = pixels.GetRowSpan(newY); PixelOperations.Instance.FromGray8Bytes( this.configuration, row.GetSpan(), @@ -283,7 +284,7 @@ namespace SixLabors.ImageSharp.Formats.Tga } } - private void ReadBgra16(int width, int height, Buffer2D pixels) + private void ReadBgra16(int width, int height, Buffer2D pixels, bool inverted) where TPixel : struct, IPixel { using (IManagedByteBuffer row = this.memoryAllocator.AllocatePaddedPixelRowBuffer(width, 2, 0)) @@ -294,7 +295,8 @@ namespace SixLabors.ImageSharp.Formats.Tga for (int y = 0; y < this.fileHeader.Height; y++) { this.currentStream.Read(row); - Span pixelSpan = pixels.GetRowSpan(height - y - 1); + int newY = Invert(y, height, inverted); + Span pixelSpan = pixels.GetRowSpan(newY); PixelOperations.Instance.FromBgra5551Bytes( this.configuration, row.GetSpan(), @@ -307,7 +309,7 @@ namespace SixLabors.ImageSharp.Formats.Tga } } - private void ReadBgr24(int width, int height, Buffer2D pixels) + private void ReadBgr24(int width, int height, Buffer2D pixels, bool inverted) where TPixel : struct, IPixel { using (IManagedByteBuffer row = this.memoryAllocator.AllocatePaddedPixelRowBuffer(width, 3, 0)) @@ -315,7 +317,8 @@ namespace SixLabors.ImageSharp.Formats.Tga for (int y = 0; y < height; y++) { this.currentStream.Read(row); - Span pixelSpan = pixels.GetRowSpan(height - y - 1); + int newY = Invert(y, height, inverted); + Span pixelSpan = pixels.GetRowSpan(newY); PixelOperations.Instance.FromBgr24Bytes( this.configuration, row.GetSpan(), @@ -325,7 +328,7 @@ namespace SixLabors.ImageSharp.Formats.Tga } } - private void ReadBgra32(int width, int height, Buffer2D pixels) + private void ReadBgra32(int width, int height, Buffer2D pixels, bool inverted) where TPixel : struct, IPixel { using (IManagedByteBuffer row = this.memoryAllocator.AllocatePaddedPixelRowBuffer(width, 4, 0)) @@ -333,7 +336,8 @@ namespace SixLabors.ImageSharp.Formats.Tga for (int y = 0; y < height; y++) { this.currentStream.Read(row); - Span pixelSpan = pixels.GetRowSpan(height - y - 1); + int newY = Invert(y, height, inverted); + Span pixelSpan = pixels.GetRowSpan(newY); PixelOperations.Instance.FromBgra32Bytes( this.configuration, row.GetSpan(), @@ -343,7 +347,7 @@ namespace SixLabors.ImageSharp.Formats.Tga } } - private void ReadRle(int width, int height, Buffer2D pixels, int bytesPerPixel) + private void ReadRle(int width, int height, Buffer2D pixels, int bytesPerPixel, bool inverted) where TPixel : struct, IPixel { TPixel color = default; @@ -353,7 +357,8 @@ namespace SixLabors.ImageSharp.Formats.Tga this.UncompressRle(width, height, bufferSpan, bytesPerPixel); for (int y = 0; y < height; y++) { - Span pixelRow = pixels.GetRowSpan(this.fileHeader.Height - y - 1); + int newY = Invert(y, height, inverted); + Span pixelRow = pixels.GetRowSpan(newY); int rowStartIdx = y * width * bytesPerPixel; for (int x = 0; x < width; x++) { @@ -382,6 +387,20 @@ namespace SixLabors.ImageSharp.Formats.Tga } } + /// + /// Reads the raw image information from the specified stream. + /// + /// The containing image data. + public IImageInfo Identify(Stream stream) + { + this.ReadFileHeader(stream); + return new ImageInfo( + new PixelTypeInfo(this.fileHeader.PixelDepth), + this.fileHeader.Width, + this.fileHeader.Height, + this.metadata); + } + private void UncompressRle(int width, int height, Span buffer, int bytesPerPixel) { int uncompressedPixels = 0; @@ -444,25 +463,15 @@ namespace SixLabors.ImageSharp.Formats.Tga } } - /// - /// Reads the raw image information from the specified stream. - /// - /// The containing image data. - public IImageInfo Identify(Stream stream) - { - this.ReadFileHeader(stream); - return new ImageInfo( - new PixelTypeInfo(this.fileHeader.PixelDepth), - this.fileHeader.Width, - this.fileHeader.Height, - this.metadata); - } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int Invert(int y, int height, bool inverted) => (!inverted) ? height - y - 1 : y; /// /// Reads the tga file header from the stream. /// /// The containing image data. - private void ReadFileHeader(Stream stream) + /// true, if the image origin is top left. + private bool ReadFileHeader(Stream stream) { this.currentStream = stream; @@ -476,6 +485,13 @@ namespace SixLabors.ImageSharp.Formats.Tga this.metadata = new ImageMetadata(); this.tgaMetadata = this.metadata.GetFormatMetadata(TgaFormat.Instance); this.tgaMetadata.BitsPerPixel = (TgaBitsPerPixel)this.fileHeader.PixelDepth; + + if (this.fileHeader.YOffset > 0) + { + return true; + } + + return false; } } } diff --git a/tests/ImageSharp.Tests/Formats/Tga/TgaDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Tga/TgaDecoderTests.cs index e1b23a48e2..54d94c7651 100644 --- a/tests/ImageSharp.Tests/Formats/Tga/TgaDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Tga/TgaDecoderTests.cs @@ -105,7 +105,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tga [Theory] [WithFile(Bit24TopLeft, PixelTypes.Rgba32)] - public void TgaDecoder_CanDecode_Uncompressed_WithTopLeftOrigin_24Bit(TestImageProvider provider) + public void TgaDecoder_CanDecode_Palette_WithTopLeftOrigin_24Bit(TestImageProvider provider) where TPixel : struct, IPixel { using (Image image = provider.GetImage(new TgaDecoder())) diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs index e60219eed5..1777498694 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/tests/ImageSharp.Tests/TestImages.cs @@ -373,7 +373,7 @@ namespace SixLabors.ImageSharp.Tests public const string Bit16 = "Tga/targa_16bit.tga"; public const string Bit16PalRle = "Tga/ccm8.tga"; public const string Bit24 = "Tga/targa_24bit.tga"; - public const string Bit24TopLeft = "Tga/targa_24bit_origin_topleft.tga"; + public const string Bit24TopLeft = "Tga/targa_24bit_pal_origin_topleft.tga"; public const string Bit24RleTopLeft = "Tga/targa_24bit_rle_origin_topleft.tga"; public const string Bit32 = "Tga/targa_32bit.tga"; public const string Grey = "Tga/targa_8bit.tga"; diff --git a/tests/Images/Input/Tga/targa_24bit_origin_topleft.tga b/tests/Images/Input/Tga/targa_24bit_pal_origin_topleft.tga similarity index 100% rename from tests/Images/Input/Tga/targa_24bit_origin_topleft.tga rename to tests/Images/Input/Tga/targa_24bit_pal_origin_topleft.tga From 20f1a1fa65618a8333e8610d82c8612b4ebda8cc Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Sun, 13 Oct 2019 11:23:34 +0200 Subject: [PATCH 18/75] Treat bgra5551 pixels as opaque --- src/ImageSharp/Formats/Tga/TgaDecoderCore.cs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs b/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs index 64635ea8a9..eef4dc71b3 100644 --- a/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs +++ b/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs @@ -195,7 +195,11 @@ namespace SixLabors.ImageSharp.Formats.Tga for (int x = 0; x < width; x++) { int colorIndex = rowSpan[x]; - color.FromBgra5551(Unsafe.As(ref palette[colorIndex * colorMapPixelSizeInBytes])); + + // Set bit 16 to 1, to treat it as opaque for Bgra5551. + Bgra5551 bgra = Unsafe.As(ref palette[colorIndex * colorMapPixelSizeInBytes]); + bgra.PackedValue = (ushort)(bgra.PackedValue | 0x8000); + color.FromBgra5551(bgra); pixelRow[x] = color; } @@ -249,7 +253,10 @@ namespace SixLabors.ImageSharp.Formats.Tga color.FromGray8(Unsafe.As(ref palette[bufferSpan[idx] * colorMapPixelSizeInBytes])); break; case 2: - color.FromBgra5551(Unsafe.As(ref palette[bufferSpan[idx] * colorMapPixelSizeInBytes])); + // Set bit 16 to 1, to treat it as opaque for Bgra5551. + Bgra5551 bgra = Unsafe.As(ref palette[bufferSpan[idx] * colorMapPixelSizeInBytes]); + bgra.PackedValue = (ushort)(bgra.PackedValue | 0x8000); + color.FromBgra5551(bgra); break; case 3: color.FromBgr24(Unsafe.As(ref palette[bufferSpan[idx] * colorMapPixelSizeInBytes])); From ca03cebdda88df6149504937213304cd82d3469b Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Sun, 13 Oct 2019 13:05:08 +0200 Subject: [PATCH 19/75] Add support for encoding RLE tga images --- .../Formats/Tga/ITgaEncoderOptions.cs | 5 ++ src/ImageSharp/Formats/Tga/TgaEncoder.cs | 8 +++ src/ImageSharp/Formats/Tga/TgaEncoderCore.cs | 63 ++++++++++++++++++- 3 files changed, 74 insertions(+), 2 deletions(-) diff --git a/src/ImageSharp/Formats/Tga/ITgaEncoderOptions.cs b/src/ImageSharp/Formats/Tga/ITgaEncoderOptions.cs index a6e9871e0a..ef1fecc93a 100644 --- a/src/ImageSharp/Formats/Tga/ITgaEncoderOptions.cs +++ b/src/ImageSharp/Formats/Tga/ITgaEncoderOptions.cs @@ -12,5 +12,10 @@ namespace SixLabors.ImageSharp.Formats.Tga /// Gets the number of bits per pixel. /// TgaBitsPerPixel? BitsPerPixel { get; } + + /// + /// Gets a value indicating whether run length compression should be used. + /// + bool Compress { get; } } } diff --git a/src/ImageSharp/Formats/Tga/TgaEncoder.cs b/src/ImageSharp/Formats/Tga/TgaEncoder.cs index f5b9fa752c..85b4fadfcd 100644 --- a/src/ImageSharp/Formats/Tga/TgaEncoder.cs +++ b/src/ImageSharp/Formats/Tga/TgaEncoder.cs @@ -8,6 +8,9 @@ using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Formats.Tga { + /// + /// Image encoder for writing an image to a stream as a targa truevision image. + /// public sealed class TgaEncoder : IImageEncoder, ITgaEncoderOptions { /// @@ -15,6 +18,11 @@ namespace SixLabors.ImageSharp.Formats.Tga /// public TgaBitsPerPixel? BitsPerPixel { get; set; } + /// + /// Gets or sets a value indicating whether run length compression should be used. + /// + public bool Compress { get; set; } + /// public void Encode(Image image, Stream stream) where TPixel : struct, IPixel diff --git a/src/ImageSharp/Formats/Tga/TgaEncoderCore.cs b/src/ImageSharp/Formats/Tga/TgaEncoderCore.cs index ddd430b055..b48c35e05f 100644 --- a/src/ImageSharp/Formats/Tga/TgaEncoderCore.cs +++ b/src/ImageSharp/Formats/Tga/TgaEncoderCore.cs @@ -32,6 +32,11 @@ namespace SixLabors.ImageSharp.Formats.Tga /// private TgaBitsPerPixel? bitsPerPixel; + /// + /// Indicates if run length compression should be used. + /// + private readonly bool useCompression; + /// /// Initializes a new instance of the class. /// @@ -41,6 +46,7 @@ namespace SixLabors.ImageSharp.Formats.Tga { this.memoryAllocator = memoryAllocator; this.bitsPerPixel = options.BitsPerPixel; + this.useCompression = options.Compress; } public void Encode(Image image, Stream stream) @@ -54,10 +60,11 @@ namespace SixLabors.ImageSharp.Formats.Tga TgaMetadata tgaMetadata = metadata.GetFormatMetadata(TgaFormat.Instance); this.bitsPerPixel = this.bitsPerPixel ?? tgaMetadata.BitsPerPixel; + TgaImageType imageType = this.useCompression ? TgaImageType.RleTrueColor : TgaImageType.TrueColor; var fileHeader = new TgaFileHeader( idLength: 0, colorMapType: 0, - imageType: TgaImageType.TrueColor, + imageType: imageType, cMapStart: 0, cMapLength: 0, cMapDepth: 0, @@ -77,7 +84,14 @@ namespace SixLabors.ImageSharp.Formats.Tga stream.Write(buffer, 0, TgaFileHeader.Size); - this.WriteImage(stream, image.Frames.RootFrame); + if (this.useCompression) + { + this.WriteRunLengthEndcodedImage(stream, image.Frames.RootFrame); + } + else + { + this.WriteImage(stream, image.Frames.RootFrame); + } stream.Flush(); } @@ -114,6 +128,51 @@ namespace SixLabors.ImageSharp.Formats.Tga } } + private void WriteRunLengthEndcodedImage(Stream stream, ImageFrame image) + where TPixel : struct, IPixel + { + Rgba32 color = default; + Buffer2D pixels = image.PixelBuffer; + Span pixelSpan = pixels.Span; + int totalPixels = image.Width * image.Height; + int encodedPixels = 0; + while (encodedPixels < totalPixels) + { + TPixel currentPixel = pixelSpan[encodedPixels]; + currentPixel.ToRgba32(ref color); + byte equalPixelCount = this.FindEqualPixels(pixelSpan.Slice(encodedPixels)); + stream.WriteByte((byte)(equalPixelCount | 128)); + stream.WriteByte(color.B); + stream.WriteByte(color.G); + stream.WriteByte(color.R); + encodedPixels += equalPixelCount + 1; + } + } + + private byte FindEqualPixels(Span pixelSpan) + where TPixel : struct, IPixel + { + int idx = 0; + byte equalPixelCount = 0; + while (equalPixelCount < 127 && idx < pixelSpan.Length - 1) + { + TPixel currentPixel = pixelSpan[idx]; + TPixel nextPixel = pixelSpan[idx + 1]; + if (currentPixel.Equals(nextPixel)) + { + equalPixelCount++; + } + else + { + return equalPixelCount; + } + + idx++; + } + + return equalPixelCount; + } + private IManagedByteBuffer AllocateRow(int width, int bytesPerPixel) => this.memoryAllocator.AllocatePaddedPixelRowBuffer(width, bytesPerPixel, 0); /// From 7b3018c3e191893fffaff38975ce7fc1c35ee95d Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Sun, 13 Oct 2019 14:26:14 +0200 Subject: [PATCH 20/75] Add support for encoding rle 8, 16 and bit tga images --- src/ImageSharp/Formats/Tga/TgaEncoderCore.cs | 61 +++++++++++++++- .../Formats/Tga/TgaEncoderTests.cs | 73 +++++++++++++++++++ 2 files changed, 131 insertions(+), 3 deletions(-) create mode 100644 tests/ImageSharp.Tests/Formats/Tga/TgaEncoderTests.cs diff --git a/src/ImageSharp/Formats/Tga/TgaEncoderCore.cs b/src/ImageSharp/Formats/Tga/TgaEncoderCore.cs index b48c35e05f..4a283260c5 100644 --- a/src/ImageSharp/Formats/Tga/TgaEncoderCore.cs +++ b/src/ImageSharp/Formats/Tga/TgaEncoderCore.cs @@ -3,6 +3,8 @@ using System; using System.IO; +using System.Numerics; +using System.Runtime.CompilerServices; using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Memory; @@ -61,6 +63,11 @@ namespace SixLabors.ImageSharp.Formats.Tga this.bitsPerPixel = this.bitsPerPixel ?? tgaMetadata.BitsPerPixel; TgaImageType imageType = this.useCompression ? TgaImageType.RleTrueColor : TgaImageType.TrueColor; + if (this.bitsPerPixel == TgaBitsPerPixel.Pixel8) + { + imageType = this.useCompression ? TgaImageType.RleBlackAndWhite : TgaImageType.BlackAndWhite; + } + var fileHeader = new TgaFileHeader( idLength: 0, colorMapType: 0, @@ -141,10 +148,38 @@ namespace SixLabors.ImageSharp.Formats.Tga TPixel currentPixel = pixelSpan[encodedPixels]; currentPixel.ToRgba32(ref color); byte equalPixelCount = this.FindEqualPixels(pixelSpan.Slice(encodedPixels)); + + // Write the number of equal pixels, with the high bit set, indicating ist a compressed pixel run. stream.WriteByte((byte)(equalPixelCount | 128)); - stream.WriteByte(color.B); - stream.WriteByte(color.G); - stream.WriteByte(color.R); + switch (this.bitsPerPixel) + { + case TgaBitsPerPixel.Pixel8: + int luminance = GetLuminance(currentPixel); + stream.WriteByte((byte)luminance); + break; + + case TgaBitsPerPixel.Pixel16: + // TODO: this seems to be wrong + var bgra5551 = new Bgra5551(color.ToVector4()); + stream.WriteByte((byte)(bgra5551.PackedValue & 0xFF)); + stream.WriteByte((byte)(bgra5551.PackedValue & 0xFF00)); + + break; + + case TgaBitsPerPixel.Pixel24: + stream.WriteByte(color.B); + stream.WriteByte(color.G); + stream.WriteByte(color.R); + break; + + case TgaBitsPerPixel.Pixel32: + stream.WriteByte(color.B); + stream.WriteByte(color.G); + stream.WriteByte(color.R); + stream.WriteByte(color.A); + break; + } + encodedPixels += equalPixelCount + 1; } } @@ -270,5 +305,25 @@ namespace SixLabors.ImageSharp.Formats.Tga } } } + + /// + /// Convert the pixel values to grayscale using ITU-R Recommendation BT.709. + /// + /// The pixel to get the luminance from + [MethodImpl(InliningOptions.ShortMethod)] + public static int GetLuminance(TPixel sourcePixel) + where TPixel : struct, IPixel + { + Vector4 vector = sourcePixel.ToVector4(); + return GetLuminance(ref vector); + } + + /// + /// Convert the pixel values to grayscale using ITU-R Recommendation BT.709. + /// + /// The vector to get the luminance from + [MethodImpl(InliningOptions.ShortMethod)] + public static int GetLuminance(ref Vector4 vector) + => (int)MathF.Round(((.2126F * vector.X) + (.7152F * vector.Y) + (.0722F * vector.Y)) * (256 - 1)); } } diff --git a/tests/ImageSharp.Tests/Formats/Tga/TgaEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Tga/TgaEncoderTests.cs new file mode 100644 index 0000000000..a92b50516d --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Tga/TgaEncoderTests.cs @@ -0,0 +1,73 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System.IO; + +using SixLabors.ImageSharp.Formats.Tga; +using SixLabors.ImageSharp.PixelFormats; + +using Xunit; + +namespace SixLabors.ImageSharp.Tests.Formats.Tga +{ + using static TestImages.Tga; + + public class TgaEncoderTests + { + public static readonly TheoryData TgaBitsPerPixelFiles = + new TheoryData + { + { Grey, TgaBitsPerPixel.Pixel8 }, + { Bit32, TgaBitsPerPixel.Pixel32 }, + { Bit24, TgaBitsPerPixel.Pixel24 }, + { Bit16, TgaBitsPerPixel.Pixel16 }, + }; + + [Theory] + [MemberData(nameof(TgaBitsPerPixelFiles))] + public void Encode_PreserveBitsPerPixel(string imagePath, TgaBitsPerPixel bmpBitsPerPixel) + { + var options = new TgaEncoder(); + + TestFile testFile = TestFile.Create(imagePath); + using (Image input = testFile.CreateRgba32Image()) + { + using (var memStream = new MemoryStream()) + { + input.Save(memStream, options); + memStream.Position = 0; + using (Image output = Image.Load(memStream)) + { + TgaMetadata meta = output.Metadata.GetFormatMetadata(TgaFormat.Instance); + Assert.Equal(bmpBitsPerPixel, meta.BitsPerPixel); + } + } + } + } + + [Theory] + [MemberData(nameof(TgaBitsPerPixelFiles))] + public void Encode_WithCompression_PreserveBitsPerPixel(string imagePath, TgaBitsPerPixel bmpBitsPerPixel) + { + var options = new TgaEncoder() + { + Compress = true + }; + + TestFile testFile = TestFile.Create(imagePath); + using (Image input = testFile.CreateRgba32Image()) + { + using (var memStream = new MemoryStream()) + { + input.Save(memStream, options); + memStream.Position = 0; + using (Image output = Image.Load(memStream)) + { + TgaMetadata meta = output.Metadata.GetFormatMetadata(TgaFormat.Instance); + Assert.Equal(bmpBitsPerPixel, meta.BitsPerPixel); + } + } + } + } + } +} From 3b5af03f5fceecf62714718e16217763c1184d2e Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Sun, 13 Oct 2019 17:17:56 +0200 Subject: [PATCH 21/75] Add test for the tga encoder --- src/ImageSharp/Formats/Tga/TgaEncoderCore.cs | 2 +- .../Formats/Tga/TgaDecoderTests.cs | 74 ++++--------------- .../Formats/Tga/TgaEncoderTests.cs | 69 +++++++++++++++++ .../Formats/Tga/TgaTestUtils.cs | 51 +++++++++++++ 4 files changed, 136 insertions(+), 60 deletions(-) create mode 100644 tests/ImageSharp.Tests/Formats/Tga/TgaTestUtils.cs diff --git a/src/ImageSharp/Formats/Tga/TgaEncoderCore.cs b/src/ImageSharp/Formats/Tga/TgaEncoderCore.cs index 4a283260c5..291c61a814 100644 --- a/src/ImageSharp/Formats/Tga/TgaEncoderCore.cs +++ b/src/ImageSharp/Formats/Tga/TgaEncoderCore.cs @@ -76,7 +76,7 @@ namespace SixLabors.ImageSharp.Formats.Tga cMapLength: 0, cMapDepth: 0, xOffset: 0, - yOffset: 0, + yOffset: this.useCompression ? (short)image.Height : (short)0, // When run length encoding is used, the origin should be top left instead of the default bottom left. width: (short)image.Width, height: (short)image.Height, pixelDepth: (byte)this.bitsPerPixel.Value, diff --git a/tests/ImageSharp.Tests/Formats/Tga/TgaDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Tga/TgaDecoderTests.cs index 54d94c7651..68a3fbe28a 100644 --- a/tests/ImageSharp.Tests/Formats/Tga/TgaDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Tga/TgaDecoderTests.cs @@ -1,15 +1,8 @@ // Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. -using System; -using System.IO; - -using ImageMagick; - -using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Formats.Tga; using SixLabors.ImageSharp.PixelFormats; -using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison; using Xunit; @@ -27,7 +20,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tga using (Image image = provider.GetImage(new TgaDecoder())) { image.DebugSave(provider); - CompareWithReferenceDecoder(provider, image); + TgaTestUtils.CompareWithReferenceDecoder(provider, image); } } @@ -39,7 +32,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tga using (Image image = provider.GetImage(new TgaDecoder())) { image.DebugSave(provider); - CompareWithReferenceDecoder(provider, image); + TgaTestUtils.CompareWithReferenceDecoder(provider, image); } } @@ -51,7 +44,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tga using (Image image = provider.GetImage(new TgaDecoder())) { image.DebugSave(provider); - CompareWithReferenceDecoder(provider, image); + TgaTestUtils.CompareWithReferenceDecoder(provider, image); } } @@ -63,7 +56,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tga using (Image image = provider.GetImage(new TgaDecoder())) { image.DebugSave(provider); - CompareWithReferenceDecoder(provider, image); + TgaTestUtils.CompareWithReferenceDecoder(provider, image); } } @@ -75,7 +68,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tga using (Image image = provider.GetImage(new TgaDecoder())) { image.DebugSave(provider); - CompareWithReferenceDecoder(provider, image); + TgaTestUtils.CompareWithReferenceDecoder(provider, image); } } @@ -87,7 +80,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tga using (Image image = provider.GetImage(new TgaDecoder())) { image.DebugSave(provider); - CompareWithReferenceDecoder(provider, image); + TgaTestUtils.CompareWithReferenceDecoder(provider, image); } } @@ -99,7 +92,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tga using (Image image = provider.GetImage(new TgaDecoder())) { image.DebugSave(provider); - CompareWithReferenceDecoder(provider, image); + TgaTestUtils.CompareWithReferenceDecoder(provider, image); } } @@ -111,7 +104,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tga using (Image image = provider.GetImage(new TgaDecoder())) { image.DebugSave(provider); - CompareWithReferenceDecoder(provider, image); + TgaTestUtils.CompareWithReferenceDecoder(provider, image); } } @@ -123,7 +116,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tga using (Image image = provider.GetImage(new TgaDecoder())) { image.DebugSave(provider); - CompareWithReferenceDecoder(provider, image); + TgaTestUtils.CompareWithReferenceDecoder(provider, image); } } @@ -135,7 +128,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tga using (Image image = provider.GetImage(new TgaDecoder())) { image.DebugSave(provider); - CompareWithReferenceDecoder(provider, image); + TgaTestUtils.CompareWithReferenceDecoder(provider, image); } } @@ -147,7 +140,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tga using (Image image = provider.GetImage(new TgaDecoder())) { image.DebugSave(provider); - CompareWithReferenceDecoder(provider, image); + TgaTestUtils.CompareWithReferenceDecoder(provider, image); } } @@ -159,7 +152,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tga using (Image image = provider.GetImage(new TgaDecoder())) { image.DebugSave(provider); - CompareWithReferenceDecoder(provider, image); + TgaTestUtils.CompareWithReferenceDecoder(provider, image); } } @@ -171,7 +164,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tga using (Image image = provider.GetImage(new TgaDecoder())) { image.DebugSave(provider); - CompareWithReferenceDecoder(provider, image); + TgaTestUtils.CompareWithReferenceDecoder(provider, image); } } @@ -183,7 +176,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tga using (Image image = provider.GetImage(new TgaDecoder())) { image.DebugSave(provider); - CompareWithReferenceDecoder(provider, image); + TgaTestUtils.CompareWithReferenceDecoder(provider, image); } } @@ -195,44 +188,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tga using (Image image = provider.GetImage(new TgaDecoder())) { image.DebugSave(provider); - CompareWithReferenceDecoder(provider, image); - } - } - - private void CompareWithReferenceDecoder(TestImageProvider provider, Image image) - where TPixel : struct, IPixel - { - string path = TestImageProvider.GetFilePathOrNull(provider); - if (path == null) - { - throw new InvalidOperationException("CompareToOriginal() works only with file providers!"); - } - - TestFile testFile = TestFile.Create(path); - Image magickImage = this.DecodeWithMagick(Configuration.Default, new FileInfo(testFile.FullPath)); - ImageComparer.Exact.VerifySimilarity(image, magickImage); - } - - private Image DecodeWithMagick(Configuration configuration, FileInfo fileInfo) - where TPixel : struct, IPixel - { - using (var magickImage = new MagickImage(fileInfo)) - { - var result = new Image(configuration, magickImage.Width, magickImage.Height); - Span resultPixels = result.GetPixelSpan(); - - using (IPixelCollection pixels = magickImage.GetPixelsUnsafe()) - { - byte[] data = pixels.ToByteArray(PixelMapping.RGBA); - - PixelOperations.Instance.FromRgba32Bytes( - configuration, - data, - resultPixels, - resultPixels.Length); - } - - return result; + TgaTestUtils.CompareWithReferenceDecoder(provider, image); } } } diff --git a/tests/ImageSharp.Tests/Formats/Tga/TgaEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Tga/TgaEncoderTests.cs index a92b50516d..4e1cea226d 100644 --- a/tests/ImageSharp.Tests/Formats/Tga/TgaEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Tga/TgaEncoderTests.cs @@ -14,6 +14,13 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tga public class TgaEncoderTests { + public static readonly TheoryData BitsPerPixel = + new TheoryData + { + TgaBitsPerPixel.Pixel24, + TgaBitsPerPixel.Pixel32 + }; + public static readonly TheoryData TgaBitsPerPixelFiles = new TheoryData { @@ -69,5 +76,67 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tga } } } + + [Theory] + [WithFile(Bit32, PixelTypes.Rgba32)] + public void Encode_Bit8_Works(TestImageProvider provider, TgaBitsPerPixel bitsPerPixel = TgaBitsPerPixel.Pixel8) + where TPixel : struct, IPixel => TestTgaEncoderCore(provider, bitsPerPixel); + + [Theory] + [WithFile(Bit32, PixelTypes.Rgba32)] + public void Encode_Bit16_Works(TestImageProvider provider, TgaBitsPerPixel bitsPerPixel = TgaBitsPerPixel.Pixel16) + where TPixel : struct, IPixel => TestTgaEncoderCore(provider, bitsPerPixel); + + [Theory] + [WithFile(Bit32, PixelTypes.Rgba32)] + public void Encode_Bit24_Works(TestImageProvider provider, TgaBitsPerPixel bitsPerPixel = TgaBitsPerPixel.Pixel24) + where TPixel : struct, IPixel => TestTgaEncoderCore(provider, bitsPerPixel); + + [Theory] + [WithFile(Bit32, PixelTypes.Rgba32)] + public void Encode_Bit32_Works(TestImageProvider provider, TgaBitsPerPixel bitsPerPixel = TgaBitsPerPixel.Pixel32) + where TPixel : struct, IPixel => TestTgaEncoderCore(provider, bitsPerPixel); + + [Theory] + [WithFile(Bit32, PixelTypes.Rgba32)] + public void Encode_Bit8_WithRunLengthEncoding_Works(TestImageProvider provider, TgaBitsPerPixel bitsPerPixel = TgaBitsPerPixel.Pixel8) + where TPixel : struct, IPixel => TestTgaEncoderCore(provider, bitsPerPixel, true); + + [Theory] + [WithFile(Bit32, PixelTypes.Rgba32)] + public void Encode_Bit16_WithRunLengthEncoding_Works(TestImageProvider provider, TgaBitsPerPixel bitsPerPixel = TgaBitsPerPixel.Pixel16) + where TPixel : struct, IPixel => TestTgaEncoderCore(provider, bitsPerPixel, true); + + [Theory] + [WithFile(Bit32, PixelTypes.Rgba32)] + public void Encode_Bit24_WithRunLengthEncoding_Works(TestImageProvider provider, TgaBitsPerPixel bitsPerPixel = TgaBitsPerPixel.Pixel24) + where TPixel : struct, IPixel => TestTgaEncoderCore(provider, bitsPerPixel, true); + + [Theory] + [WithFile(Bit32, PixelTypes.Rgba32)] + public void Encode_Bit32_WithRunLengthEncoding_Works(TestImageProvider provider, TgaBitsPerPixel bitsPerPixel = TgaBitsPerPixel.Pixel32) + where TPixel : struct, IPixel => TestTgaEncoderCore(provider, bitsPerPixel, true); + + private static void TestTgaEncoderCore( + TestImageProvider provider, + TgaBitsPerPixel bitsPerPixel, + bool useCompression = false) + where TPixel : struct, IPixel + { + using (Image image = provider.GetImage()) + { + var encoder = new TgaEncoder { BitsPerPixel = bitsPerPixel, Compress = useCompression}; + + using (var memStream = new MemoryStream()) + { + image.Save(memStream, encoder); + memStream.Position = 0; + using (var encodedImage = (Image)Image.Load(memStream)) + { + TgaTestUtils.CompareWithReferenceDecoder(provider, encodedImage); + } + } + } + } } } diff --git a/tests/ImageSharp.Tests/Formats/Tga/TgaTestUtils.cs b/tests/ImageSharp.Tests/Formats/Tga/TgaTestUtils.cs new file mode 100644 index 0000000000..f127322fda --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Tga/TgaTestUtils.cs @@ -0,0 +1,51 @@ +using System; +using System.IO; + +using ImageMagick; + +using SixLabors.ImageSharp.Advanced; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison; + +namespace SixLabors.ImageSharp.Tests.Formats.Tga +{ + public static class TgaTestUtils + { + public static void CompareWithReferenceDecoder(TestImageProvider provider, Image image) + where TPixel : struct, IPixel + { + string path = TestImageProvider.GetFilePathOrNull(provider); + if (path == null) + { + throw new InvalidOperationException("CompareToOriginal() works only with file providers!"); + } + + TestFile testFile = TestFile.Create(path); + Image magickImage = DecodeWithMagick(Configuration.Default, new FileInfo(testFile.FullPath)); + ImageComparer.Exact.VerifySimilarity(image, magickImage); + } + + public static Image DecodeWithMagick(Configuration configuration, FileInfo fileInfo) + where TPixel : struct, IPixel + { + using (var magickImage = new MagickImage(fileInfo)) + { + var result = new Image(configuration, magickImage.Width, magickImage.Height); + Span resultPixels = result.GetPixelSpan(); + + using (IPixelCollection pixels = magickImage.GetPixelsUnsafe()) + { + byte[] data = pixels.ToByteArray(PixelMapping.RGBA); + + PixelOperations.Instance.FromRgba32Bytes( + configuration, + data, + resultPixels, + resultPixels.Length); + } + + return result; + } + } + } +} From 96d6afe944ab744c6be38a6cb8634da14b09df2e Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Sun, 13 Oct 2019 17:23:17 +0200 Subject: [PATCH 22/75] Skip palette bytes if image type indicates its no palette image --- src/ImageSharp/Formats/Tga/TgaDecoderCore.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs b/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs index eef4dc71b3..7b7f803ca1 100644 --- a/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs +++ b/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs @@ -111,6 +111,13 @@ namespace SixLabors.ImageSharp.Formats.Tga return image; } + // Even if the image type indicates it is not a paletted image, it can still contain a palette. Skip those bytes. + if (this.fileHeader.CMapLength > 0) + { + int colorMapPixelSizeInBytes = this.fileHeader.CMapDepth / 8; + this.currentStream.Skip(this.fileHeader.CMapLength * colorMapPixelSizeInBytes); + } + switch (this.fileHeader.PixelDepth) { case 8: From ad50ab87189677457fe601529421a858667dd79a Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Sun, 13 Oct 2019 17:55:01 +0200 Subject: [PATCH 23/75] A little cleanup and comments --- src/ImageSharp/Formats/README.md | 6 ++ src/ImageSharp/Formats/Tga/TgaDecoderCore.cs | 90 +++++++++++++++++++ src/ImageSharp/Formats/Tga/TgaEncoderCore.cs | 18 ++++ src/ImageSharp/Formats/Tga/TgaImageType.cs | 21 ----- .../Formats/Tga/TgaImageTypeExtensions.cs | 26 ++++++ 5 files changed, 140 insertions(+), 21 deletions(-) create mode 100644 src/ImageSharp/Formats/README.md create mode 100644 src/ImageSharp/Formats/Tga/TgaImageTypeExtensions.cs diff --git a/src/ImageSharp/Formats/README.md b/src/ImageSharp/Formats/README.md new file mode 100644 index 0000000000..4a2b401b1d --- /dev/null +++ b/src/ImageSharp/Formats/README.md @@ -0,0 +1,6 @@ +# Encoder/Decoder for true vision targa files + +Useful links for reference: + +- [FileFront](https://www.fileformat.info/format/tga/egff.htm) +- [Tga Specification](http://www.dca.fee.unicamp.br/~martino/disciplinas/ea978/tgaffs.pdf) diff --git a/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs b/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs index 7b7f803ca1..8f36a7626b 100644 --- a/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs +++ b/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs @@ -50,6 +50,11 @@ namespace SixLabors.ImageSharp.Formats.Tga /// private readonly ITgaDecoderOptions options; + /// + /// Initializes a new instance of the class. + /// + /// The configuration. + /// The options. public TgaDecoderCore(Configuration configuration, ITgaDecoderOptions options) { this.configuration = configuration; @@ -183,6 +188,16 @@ namespace SixLabors.ImageSharp.Formats.Tga } } + /// + /// Reads a uncompressed TGA image with a palette. + /// + /// The pixel type. + /// The width of the image. + /// The height of the image. + /// The to assign the palette to. + /// The color palette. + /// Color map size of one entry in bytes. + /// Indicates, if the origin of the image is top left rather the bottom left (the default). private void ReadPaletted(int width, int height, Buffer2D pixels, byte[] palette, int colorMapPixelSizeInBytes, bool inverted) where TPixel : struct, IPixel { @@ -236,6 +251,16 @@ namespace SixLabors.ImageSharp.Formats.Tga } } + /// + /// Reads a run length encoded TGA image with a palette. + /// + /// The pixel type. + /// The width of the image. + /// The height of the image. + /// The to assign the palette to. + /// The color palette. + /// Color map size of one entry in bytes. + /// Indicates, if the origin of the image is top left rather the bottom left (the default). private void ReadPalettedRle(int width, int height, Buffer2D pixels, byte[] palette, int colorMapPixelSizeInBytes, bool inverted) where TPixel : struct, IPixel { @@ -279,6 +304,14 @@ namespace SixLabors.ImageSharp.Formats.Tga } } + /// + /// Reads a uncompressed monochrome TGA image. + /// + /// The pixel type. + /// The width of the image. + /// The height of the image. + /// The to assign the palette to. + /// Indicates, if the origin of the image is top left rather the bottom left (the default). private void ReadMonoChrome(int width, int height, Buffer2D pixels, bool inverted) where TPixel : struct, IPixel { @@ -298,6 +331,14 @@ namespace SixLabors.ImageSharp.Formats.Tga } } + /// + /// Reads a uncompressed TGA image where each pixels has 16 bit. + /// + /// The pixel type. + /// The width of the image. + /// The height of the image. + /// The to assign the palette to. + /// Indicates, if the origin of the image is top left rather the bottom left (the default). private void ReadBgra16(int width, int height, Buffer2D pixels, bool inverted) where TPixel : struct, IPixel { @@ -323,6 +364,14 @@ namespace SixLabors.ImageSharp.Formats.Tga } } + /// + /// Reads a uncompressed TGA image where each pixels has 24 bit. + /// + /// The pixel type. + /// The width of the image. + /// The height of the image. + /// The to assign the palette to. + /// Indicates, if the origin of the image is top left rather the bottom left (the default). private void ReadBgr24(int width, int height, Buffer2D pixels, bool inverted) where TPixel : struct, IPixel { @@ -342,6 +391,14 @@ namespace SixLabors.ImageSharp.Formats.Tga } } + /// + /// Reads a uncompressed TGA image where each pixels has 32 bit. + /// + /// The pixel type. + /// The width of the image. + /// The height of the image. + /// The to assign the palette to. + /// Indicates, if the origin of the image is top left rather the bottom left (the default). private void ReadBgra32(int width, int height, Buffer2D pixels, bool inverted) where TPixel : struct, IPixel { @@ -361,6 +418,15 @@ namespace SixLabors.ImageSharp.Formats.Tga } } + /// + /// Reads a run length encoded TGA image. + /// + /// The pixel type. + /// The width of the image. + /// The height of the image. + /// The to assign the palette to. + /// The bytes per pixel. + /// Indicates, if the origin of the image is top left rather the bottom left (the default). private void ReadRle(int width, int height, Buffer2D pixels, int bytesPerPixel, bool inverted) where TPixel : struct, IPixel { @@ -415,6 +481,13 @@ namespace SixLabors.ImageSharp.Formats.Tga this.metadata); } + /// + /// Produce uncompressed tga data from a run length encoded stream. + /// + /// The width of the image. + /// The height of the image. + /// Buffer for uncompressed data. + /// The bytes used per pixel. private void UncompressRle(int width, int height, Span buffer, int bytesPerPixel) { int uncompressedPixels = 0; @@ -452,6 +525,16 @@ namespace SixLabors.ImageSharp.Formats.Tga } } + /// + /// Helper method for decoding BGRA5551 images. Makes the pixels opaque, because the high bit does not + /// represent an alpha channel. + /// TODO: maybe there is a better/faster way to achieve this. + /// + /// The pixel type. + /// The destination pixel buffer. + /// The start position of pixel data. + /// A byte array to store the read pixel data. + /// Bgra pixel row span. private void MakeOpaque(Buffer2D pixels, long currentPosition, IManagedByteBuffer row, Span bgraRowSpan) where TPixel : struct, IPixel { @@ -477,6 +560,13 @@ namespace SixLabors.ImageSharp.Formats.Tga } } + /// + /// Returns the y- value based on the given height. + /// + /// The y- value representing the current row. + /// The height of the bitmap. + /// Whether the bitmap is inverted. + /// The representing the inverted value. [MethodImpl(MethodImplOptions.AggressiveInlining)] private static int Invert(int y, int height, bool inverted) => (!inverted) ? height - y - 1 : y; diff --git a/src/ImageSharp/Formats/Tga/TgaEncoderCore.cs b/src/ImageSharp/Formats/Tga/TgaEncoderCore.cs index 291c61a814..1bde05c937 100644 --- a/src/ImageSharp/Formats/Tga/TgaEncoderCore.cs +++ b/src/ImageSharp/Formats/Tga/TgaEncoderCore.cs @@ -51,6 +51,12 @@ namespace SixLabors.ImageSharp.Formats.Tga this.useCompression = options.Compress; } + /// + /// Encodes the image to the specified stream from the . + /// + /// The pixel format. + /// The to encode from. + /// The to encode the image data to. public void Encode(Image image, Stream stream) where TPixel : struct, IPixel { @@ -135,6 +141,12 @@ namespace SixLabors.ImageSharp.Formats.Tga } } + /// + /// Writes a run length encoded tga image to the stream. + /// + /// The pixel type. + /// The stream to write the image to. + /// The image to encode. private void WriteRunLengthEndcodedImage(Stream stream, ImageFrame image) where TPixel : struct, IPixel { @@ -184,6 +196,12 @@ namespace SixLabors.ImageSharp.Formats.Tga } } + /// + /// Finds consecutive pixels, which have the same value starting from the pixel span offset 0. + /// + /// The pixel type. + /// The pixel span to search in. + /// The number of equal pixels. private byte FindEqualPixels(Span pixelSpan) where TPixel : struct, IPixel { diff --git a/src/ImageSharp/Formats/Tga/TgaImageType.cs b/src/ImageSharp/Formats/Tga/TgaImageType.cs index 2c19a06954..cf0eda93c4 100644 --- a/src/ImageSharp/Formats/Tga/TgaImageType.cs +++ b/src/ImageSharp/Formats/Tga/TgaImageType.cs @@ -46,25 +46,4 @@ namespace SixLabors. /// RleBlackAndWhite = 11, } - - /// - /// Extension methods for TgaImageType enum. - /// - public static class TgaImageTypeExtensions - { - /// - /// Checks if this tga image type is run length encoded. - /// - /// The tga image type. - /// True, if this image type is run length encoded, otherwise false. - public static bool IsRunLengthEncoded(this TgaImageType imageType) - { - if (imageType is TgaImageType.RleColorMapped || imageType is TgaImageType.RleBlackAndWhite || imageType is TgaImageType.RleTrueColor) - { - return true; - } - - return false; - } - } } diff --git a/src/ImageSharp/Formats/Tga/TgaImageTypeExtensions.cs b/src/ImageSharp/Formats/Tga/TgaImageTypeExtensions.cs new file mode 100644 index 0000000000..406e12d08b --- /dev/null +++ b/src/ImageSharp/Formats/Tga/TgaImageTypeExtensions.cs @@ -0,0 +1,26 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +namespace SixLabors.ImageSharp.Formats.Tga +{ + /// + /// Extension methods for TgaImageType enum. + /// + public static class TgaImageTypeExtensions + { + /// + /// Checks if this tga image type is run length encoded. + /// + /// The tga image type. + /// True, if this image type is run length encoded, otherwise false. + public static bool IsRunLengthEncoded(this TgaImageType imageType) + { + if (imageType is TgaImageType.RleColorMapped || imageType is TgaImageType.RleBlackAndWhite || imageType is TgaImageType.RleTrueColor) + { + return true; + } + + return false; + } + } +} From f612fabf7901c623ffba2cc136bd5b6acfd6c851 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Sun, 13 Oct 2019 18:03:23 +0200 Subject: [PATCH 24/75] Add CompareToOriginal at the end of Issue1014 test --- tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs index 2a76310fcd..e064c0fb06 100644 --- a/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs @@ -218,7 +218,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Png using (Image image = provider.GetImage(new PngDecoder())) { image.DebugSave(provider); - // TODO: compare to expected output + image.CompareToOriginal(provider, ImageComparer.Exact); } }); Assert.Null(ex); From 2e034af05c6343f9ec6f90321d541e06febd73b0 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Sun, 13 Oct 2019 18:03:49 +0200 Subject: [PATCH 25/75] Set expected default configuration count to 5 --- tests/ImageSharp.Tests/ConfigurationTests.cs | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/tests/ImageSharp.Tests/ConfigurationTests.cs b/tests/ImageSharp.Tests/ConfigurationTests.cs index 6f68d04288..e029970f6f 100644 --- a/tests/ImageSharp.Tests/ConfigurationTests.cs +++ b/tests/ImageSharp.Tests/ConfigurationTests.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,6 +20,8 @@ namespace SixLabors.ImageSharp.Tests public Configuration ConfigurationEmpty { get; } public Configuration DefaultConfiguration { get; } + private readonly int expectedDefaultConfigurationCount = 5; + public ConfigurationTests() { // the shallow copy of configuration should behave exactly like the default configuration, @@ -92,14 +94,13 @@ namespace SixLabors.ImageSharp.Tests [Fact] public void ConfigurationCannotAddDuplicates() { - const int count = 4; Configuration config = this.DefaultConfiguration; - Assert.Equal(count, config.ImageFormats.Count()); + Assert.Equal(expectedDefaultConfigurationCount, config.ImageFormats.Count()); config.ImageFormatsManager.AddImageFormat(BmpFormat.Instance); - Assert.Equal(count, config.ImageFormats.Count()); + Assert.Equal(expectedDefaultConfigurationCount, config.ImageFormats.Count()); } [Fact] @@ -107,7 +108,7 @@ namespace SixLabors.ImageSharp.Tests { Configuration config = Configuration.CreateDefaultInstance(); - Assert.Equal(4, config.ImageFormats.Count()); + Assert.Equal(expectedDefaultConfigurationCount, config.ImageFormats.Count()); } [Fact] @@ -117,4 +118,4 @@ namespace SixLabors.ImageSharp.Tests Assert.True(config.WorkingBufferSizeHintInBytes > 1024); } } -} \ No newline at end of file +} From da6ff8b3d6a3e09fac10b62a232f05008fb778e2 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Sun, 13 Oct 2019 19:16:40 +0200 Subject: [PATCH 26/75] Fix orientation of RLE images --- src/ImageSharp/Formats/Tga/TgaDecoderCore.cs | 3 ++- src/ImageSharp/Formats/Tga/TgaEncoderCore.cs | 9 ++++++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs b/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs index 8f36a7626b..e3a09601aa 100644 --- a/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs +++ b/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs @@ -590,7 +590,8 @@ namespace SixLabors.ImageSharp.Formats.Tga this.tgaMetadata = this.metadata.GetFormatMetadata(TgaFormat.Instance); this.tgaMetadata.BitsPerPixel = (TgaBitsPerPixel)this.fileHeader.PixelDepth; - if (this.fileHeader.YOffset > 0) + // Bit at position 3 of the descriptor indicates, that the origin is top left instead of bottom right. + if ((this.fileHeader.ImageDescriptor & (1 << 5)) != 0) { return true; } diff --git a/src/ImageSharp/Formats/Tga/TgaEncoderCore.cs b/src/ImageSharp/Formats/Tga/TgaEncoderCore.cs index 1bde05c937..a441a40d82 100644 --- a/src/ImageSharp/Formats/Tga/TgaEncoderCore.cs +++ b/src/ImageSharp/Formats/Tga/TgaEncoderCore.cs @@ -74,6 +74,9 @@ namespace SixLabors.ImageSharp.Formats.Tga imageType = this.useCompression ? TgaImageType.RleBlackAndWhite : TgaImageType.BlackAndWhite; } + // If compression is used, set byte 3 of the image descriptor to indicate an left top origin. + byte imageDescriptor = (byte)(this.useCompression ? 32 : 0); + var fileHeader = new TgaFileHeader( idLength: 0, colorMapType: 0, @@ -86,7 +89,7 @@ namespace SixLabors.ImageSharp.Formats.Tga width: (short)image.Width, height: (short)image.Height, pixelDepth: (byte)this.bitsPerPixel.Value, - imageDescriptor: 0); + imageDescriptor: (byte)(this.useCompression ? 32 : 0)); #if NETCOREAPP2_1 Span buffer = stackalloc byte[TgaFileHeader.Size]; @@ -327,7 +330,7 @@ namespace SixLabors.ImageSharp.Formats.Tga /// /// Convert the pixel values to grayscale using ITU-R Recommendation BT.709. /// - /// The pixel to get the luminance from + /// The pixel to get the luminance from. [MethodImpl(InliningOptions.ShortMethod)] public static int GetLuminance(TPixel sourcePixel) where TPixel : struct, IPixel @@ -339,7 +342,7 @@ namespace SixLabors.ImageSharp.Formats.Tga /// /// Convert the pixel values to grayscale using ITU-R Recommendation BT.709. /// - /// The vector to get the luminance from + /// The vector to get the luminance from. [MethodImpl(InliningOptions.ShortMethod)] public static int GetLuminance(ref Vector4 vector) => (int)MathF.Round(((.2126F * vector.X) + (.7152F * vector.Y) + (.0722F * vector.Y)) * (256 - 1)); From c4413d5a55782b1844d9751c5d344a8d44382c79 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Sat, 26 Oct 2019 18:32:47 +0200 Subject: [PATCH 27/75] Fix encoding of 16 tga files --- src/ImageSharp/Formats/Tga/TgaDecoderCore.cs | 9 ++++----- src/ImageSharp/Formats/Tga/TgaEncoderCore.cs | 16 +++++++++++----- 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs b/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs index e3a09601aa..e1850a32b6 100644 --- a/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs +++ b/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs @@ -141,7 +141,6 @@ namespace SixLabors.ImageSharp.Formats.Tga case 16: if (this.fileHeader.ImageType.IsRunLengthEncoded()) { - long currentPosition = this.currentStream.Position; this.ReadRle(this.fileHeader.Width, this.fileHeader.Height, pixels, 2, inverted); } else @@ -218,7 +217,7 @@ namespace SixLabors.ImageSharp.Formats.Tga { int colorIndex = rowSpan[x]; - // Set bit 16 to 1, to treat it as opaque for Bgra5551. + // Set alpha value to 1, to treat it as opaque for Bgra5551. Bgra5551 bgra = Unsafe.As(ref palette[colorIndex * colorMapPixelSizeInBytes]); bgra.PackedValue = (ushort)(bgra.PackedValue | 0x8000); color.FromBgra5551(bgra); @@ -285,7 +284,7 @@ namespace SixLabors.ImageSharp.Formats.Tga color.FromGray8(Unsafe.As(ref palette[bufferSpan[idx] * colorMapPixelSizeInBytes])); break; case 2: - // Set bit 16 to 1, to treat it as opaque for Bgra5551. + // Set alpha value to 1, to treat it as opaque for Bgra5551. Bgra5551 bgra = Unsafe.As(ref palette[bufferSpan[idx] * colorMapPixelSizeInBytes]); bgra.PackedValue = (ushort)(bgra.PackedValue | 0x8000); color.FromBgra5551(bgra); @@ -449,7 +448,7 @@ namespace SixLabors.ImageSharp.Formats.Tga color.FromGray8(Unsafe.As(ref bufferSpan[idx])); break; case 2: - // Set bit 16 to 1, to treat it as opaque for Bgra5551. + // Set alpha value to 1, to treat it as opaque for Bgra5551. bufferSpan[idx + 1] = (byte)(bufferSpan[idx + 1] | 128); color.FromBgra5551(Unsafe.As(ref bufferSpan[idx])); break; @@ -590,7 +589,7 @@ namespace SixLabors.ImageSharp.Formats.Tga this.tgaMetadata = this.metadata.GetFormatMetadata(TgaFormat.Instance); this.tgaMetadata.BitsPerPixel = (TgaBitsPerPixel)this.fileHeader.PixelDepth; - // Bit at position 3 of the descriptor indicates, that the origin is top left instead of bottom right. + // Bit at position 5 of the descriptor indicates, that the origin is top left instead of bottom right. if ((this.fileHeader.ImageDescriptor & (1 << 5)) != 0) { return true; diff --git a/src/ImageSharp/Formats/Tga/TgaEncoderCore.cs b/src/ImageSharp/Formats/Tga/TgaEncoderCore.cs index a441a40d82..0b0e0b1935 100644 --- a/src/ImageSharp/Formats/Tga/TgaEncoderCore.cs +++ b/src/ImageSharp/Formats/Tga/TgaEncoderCore.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. using System; +using System.Buffers.Binary; using System.IO; using System.Numerics; using System.Runtime.CompilerServices; @@ -29,6 +30,11 @@ namespace SixLabors.ImageSharp.Formats.Tga /// private Configuration configuration; + /// + /// Reusable buffer for writing data. + /// + private readonly byte[] buffer = new byte[2]; + /// /// The color depth, in number of bits per pixel. /// @@ -74,7 +80,7 @@ namespace SixLabors.ImageSharp.Formats.Tga imageType = this.useCompression ? TgaImageType.RleBlackAndWhite : TgaImageType.BlackAndWhite; } - // If compression is used, set byte 3 of the image descriptor to indicate an left top origin. + // If compression is used, set bit 5 of the image descriptor to indicate an left top origin. byte imageDescriptor = (byte)(this.useCompression ? 32 : 0); var fileHeader = new TgaFileHeader( @@ -89,7 +95,7 @@ namespace SixLabors.ImageSharp.Formats.Tga width: (short)image.Width, height: (short)image.Height, pixelDepth: (byte)this.bitsPerPixel.Value, - imageDescriptor: (byte)(this.useCompression ? 32 : 0)); + imageDescriptor: imageDescriptor); #if NETCOREAPP2_1 Span buffer = stackalloc byte[TgaFileHeader.Size]; @@ -174,10 +180,10 @@ namespace SixLabors.ImageSharp.Formats.Tga break; case TgaBitsPerPixel.Pixel16: - // TODO: this seems to be wrong var bgra5551 = new Bgra5551(color.ToVector4()); - stream.WriteByte((byte)(bgra5551.PackedValue & 0xFF)); - stream.WriteByte((byte)(bgra5551.PackedValue & 0xFF00)); + BinaryPrimitives.TryWriteInt16LittleEndian(this.buffer, (short)bgra5551.PackedValue); + stream.WriteByte(this.buffer[0]); + stream.WriteByte(this.buffer[1]); break; From f1a021b065aff7558f6a1af7c1b91a25093e1c71 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Sat, 26 Oct 2019 20:18:19 +0200 Subject: [PATCH 28/75] Using tolerant comparer for 16 and 8 bit --- .../Formats/Tga/TgaDecoderTests.cs | 10 ++++++---- .../Formats/Tga/TgaEncoderTests.cs | 18 ++++++++++++------ .../Formats/Tga/TgaTestUtils.cs | 14 ++++++++++++-- 3 files changed, 30 insertions(+), 12 deletions(-) diff --git a/tests/ImageSharp.Tests/Formats/Tga/TgaDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Tga/TgaDecoderTests.cs index 68a3fbe28a..03ad10de40 100644 --- a/tests/ImageSharp.Tests/Formats/Tga/TgaDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Tga/TgaDecoderTests.cs @@ -1,6 +1,8 @@ // Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. +// ReSharper disable InconsistentNaming + using SixLabors.ImageSharp.Formats.Tga; using SixLabors.ImageSharp.PixelFormats; @@ -122,7 +124,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tga [Theory] [WithFile(GreyRle, PixelTypes.Rgba32)] - public void TgaDecoder_CanDecode_RunLenghtEncoded_MonoChrome(TestImageProvider provider) + public void TgaDecoder_CanDecode_RunLengthEncoded_MonoChrome(TestImageProvider provider) where TPixel : struct, IPixel { using (Image image = provider.GetImage(new TgaDecoder())) @@ -134,7 +136,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tga [Theory] [WithFile(Bit16Rle, PixelTypes.Rgba32)] - public void TgaDecoder_CanDecode_RunLenghtEncoded_16Bit(TestImageProvider provider) + public void TgaDecoder_CanDecode_RunLengthEncoded_16Bit(TestImageProvider provider) where TPixel : struct, IPixel { using (Image image = provider.GetImage(new TgaDecoder())) @@ -146,7 +148,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tga [Theory] [WithFile(Bit24Rle, PixelTypes.Rgba32)] - public void TgaDecoder_CanDecode_RunLenghtEncoded_24Bit(TestImageProvider provider) + public void TgaDecoder_CanDecode_RunLengthEncoded_24Bit(TestImageProvider provider) where TPixel : struct, IPixel { using (Image image = provider.GetImage(new TgaDecoder())) @@ -158,7 +160,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tga [Theory] [WithFile(Bit32Rle, PixelTypes.Rgba32)] - public void TgaDecoder_CanDecode_RunLenghtEncoded_32Bit(TestImageProvider provider) + public void TgaDecoder_CanDecode_RunLengthEncoded_32Bit(TestImageProvider provider) where TPixel : struct, IPixel { using (Image image = provider.GetImage(new TgaDecoder())) diff --git a/tests/ImageSharp.Tests/Formats/Tga/TgaEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Tga/TgaEncoderTests.cs index 4e1cea226d..5dd49f4faa 100644 --- a/tests/ImageSharp.Tests/Formats/Tga/TgaEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Tga/TgaEncoderTests.cs @@ -1,6 +1,8 @@ // Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. +// ReSharper disable InconsistentNaming + using System.IO; using SixLabors.ImageSharp.Formats.Tga; @@ -80,12 +82,13 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tga [Theory] [WithFile(Bit32, PixelTypes.Rgba32)] public void Encode_Bit8_Works(TestImageProvider provider, TgaBitsPerPixel bitsPerPixel = TgaBitsPerPixel.Pixel8) - where TPixel : struct, IPixel => TestTgaEncoderCore(provider, bitsPerPixel); + // using tolerant comparer here. The results from magick differ slightly. Maybe a different ToGrey method is used. The image looks otherwise ok. + where TPixel : struct, IPixel => TestTgaEncoderCore(provider, bitsPerPixel, useCompression: true, useExactComparer: false, compareTolerance: 0.03f); [Theory] [WithFile(Bit32, PixelTypes.Rgba32)] public void Encode_Bit16_Works(TestImageProvider provider, TgaBitsPerPixel bitsPerPixel = TgaBitsPerPixel.Pixel16) - where TPixel : struct, IPixel => TestTgaEncoderCore(provider, bitsPerPixel); + where TPixel : struct, IPixel => TestTgaEncoderCore(provider, bitsPerPixel, useCompression: false, useExactComparer: false); [Theory] [WithFile(Bit32, PixelTypes.Rgba32)] @@ -100,12 +103,13 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tga [Theory] [WithFile(Bit32, PixelTypes.Rgba32)] public void Encode_Bit8_WithRunLengthEncoding_Works(TestImageProvider provider, TgaBitsPerPixel bitsPerPixel = TgaBitsPerPixel.Pixel8) - where TPixel : struct, IPixel => TestTgaEncoderCore(provider, bitsPerPixel, true); + // using tolerant comparer here. The results from magick differ slightly. Maybe a different ToGrey method is used. The image looks otherwise ok. + where TPixel : struct, IPixel => TestTgaEncoderCore(provider, bitsPerPixel, useCompression: true, useExactComparer: false, compareTolerance: 0.03f); [Theory] [WithFile(Bit32, PixelTypes.Rgba32)] public void Encode_Bit16_WithRunLengthEncoding_Works(TestImageProvider provider, TgaBitsPerPixel bitsPerPixel = TgaBitsPerPixel.Pixel16) - where TPixel : struct, IPixel => TestTgaEncoderCore(provider, bitsPerPixel, true); + where TPixel : struct, IPixel => TestTgaEncoderCore(provider, bitsPerPixel, useCompression: true, useExactComparer: false); [Theory] [WithFile(Bit32, PixelTypes.Rgba32)] @@ -120,7 +124,9 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tga private static void TestTgaEncoderCore( TestImageProvider provider, TgaBitsPerPixel bitsPerPixel, - bool useCompression = false) + bool useCompression = false, + bool useExactComparer = true, + float compareTolerance = 0.01f) where TPixel : struct, IPixel { using (Image image = provider.GetImage()) @@ -133,7 +139,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tga memStream.Position = 0; using (var encodedImage = (Image)Image.Load(memStream)) { - TgaTestUtils.CompareWithReferenceDecoder(provider, encodedImage); + TgaTestUtils.CompareWithReferenceDecoder(provider, encodedImage, useExactComparer, compareTolerance); } } } diff --git a/tests/ImageSharp.Tests/Formats/Tga/TgaTestUtils.cs b/tests/ImageSharp.Tests/Formats/Tga/TgaTestUtils.cs index f127322fda..a2f2e86d7d 100644 --- a/tests/ImageSharp.Tests/Formats/Tga/TgaTestUtils.cs +++ b/tests/ImageSharp.Tests/Formats/Tga/TgaTestUtils.cs @@ -11,7 +11,10 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tga { public static class TgaTestUtils { - public static void CompareWithReferenceDecoder(TestImageProvider provider, Image image) + public static void CompareWithReferenceDecoder(TestImageProvider provider, + Image image, + bool useExactComparer = true, + float compareTolerance = 0.01f) where TPixel : struct, IPixel { string path = TestImageProvider.GetFilePathOrNull(provider); @@ -22,7 +25,14 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tga TestFile testFile = TestFile.Create(path); Image magickImage = DecodeWithMagick(Configuration.Default, new FileInfo(testFile.FullPath)); - ImageComparer.Exact.VerifySimilarity(image, magickImage); + if (useExactComparer) + { + ImageComparer.Exact.VerifySimilarity(magickImage, image); + } + else + { + ImageComparer.Tolerant(compareTolerance).VerifySimilarity(magickImage, image); + } } public static Image DecodeWithMagick(Configuration configuration, FileInfo fileInfo) From e4fcdd03c5e0ba705019f33f4e8603d384603211 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Sat, 26 Oct 2019 20:18:41 +0200 Subject: [PATCH 29/75] Add tga specification --- .../Formats/Tga/TGA_Specification.pdf | Bin 0 -> 181405 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 src/ImageSharp/Formats/Tga/TGA_Specification.pdf diff --git a/src/ImageSharp/Formats/Tga/TGA_Specification.pdf b/src/ImageSharp/Formats/Tga/TGA_Specification.pdf new file mode 100644 index 0000000000000000000000000000000000000000..09c9a4dddade89630ba198f14a3298c00e190e40 GIT binary patch literal 181405 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�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 kP Date: Sat, 26 Oct 2019 21:22:25 +0200 Subject: [PATCH 30/75] Fix build error in Release mode --- src/ImageSharp/Formats/Tga/TgaEncoderCore.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/ImageSharp/Formats/Tga/TgaEncoderCore.cs b/src/ImageSharp/Formats/Tga/TgaEncoderCore.cs index 0b0e0b1935..139cc13fd5 100644 --- a/src/ImageSharp/Formats/Tga/TgaEncoderCore.cs +++ b/src/ImageSharp/Formats/Tga/TgaEncoderCore.cs @@ -100,7 +100,7 @@ namespace SixLabors.ImageSharp.Formats.Tga #if NETCOREAPP2_1 Span buffer = stackalloc byte[TgaFileHeader.Size]; #else - var buffer = new byte[TgaFileHeader.Size]; + byte[] buffer = new byte[TgaFileHeader.Size]; #endif fileHeader.WriteTo(buffer); @@ -161,7 +161,7 @@ namespace SixLabors.ImageSharp.Formats.Tga { Rgba32 color = default; Buffer2D pixels = image.PixelBuffer; - Span pixelSpan = pixels.Span; + Span pixelSpan = pixels.GetSpan(); int totalPixels = image.Width * image.Height; int encodedPixels = 0; while (encodedPixels < totalPixels) @@ -341,7 +341,7 @@ namespace SixLabors.ImageSharp.Formats.Tga public static int GetLuminance(TPixel sourcePixel) where TPixel : struct, IPixel { - Vector4 vector = sourcePixel.ToVector4(); + var vector = sourcePixel.ToVector4(); return GetLuminance(ref vector); } From 340af921341289ef97792655fecfba1d24f50bc7 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Sun, 27 Oct 2019 12:07:52 +0100 Subject: [PATCH 31/75] Add check for valid tga image type in the format detector --- src/ImageSharp/Formats/Tga/TgaConstants.cs | 5 ++++ src/ImageSharp/Formats/Tga/TgaFileHeader.cs | 2 +- .../Formats/Tga/TgaImageFormatDetector.cs | 11 ++++++-- .../Formats/Tga/TgaImageTypeExtensions.cs | 25 +++++++++++++++++++ 4 files changed, 40 insertions(+), 3 deletions(-) diff --git a/src/ImageSharp/Formats/Tga/TgaConstants.cs b/src/ImageSharp/Formats/Tga/TgaConstants.cs index 88c98b06a9..5aabe92a1d 100644 --- a/src/ImageSharp/Formats/Tga/TgaConstants.cs +++ b/src/ImageSharp/Formats/Tga/TgaConstants.cs @@ -16,5 +16,10 @@ namespace SixLabors.ImageSharp.Formats.Tga /// The list of file extensions that equate to a targa file. /// public static readonly IEnumerable FileExtensions = new[] { "tga", "vda", "icb", "vst" }; + + /// + /// The file header length of a tga image in bytes. + /// + public const int FileHeaderLength = 18; } } diff --git a/src/ImageSharp/Formats/Tga/TgaFileHeader.cs b/src/ImageSharp/Formats/Tga/TgaFileHeader.cs index 72c275b289..e2bbb6fbd2 100644 --- a/src/ImageSharp/Formats/Tga/TgaFileHeader.cs +++ b/src/ImageSharp/Formats/Tga/TgaFileHeader.cs @@ -17,7 +17,7 @@ namespace SixLabors.ImageSharp.Formats.Tga /// /// Defines the size of the data structure in the targa file. /// - public const int Size = 18; + public const int Size = TgaConstants.FileHeaderLength; public TgaFileHeader( byte idLength, diff --git a/src/ImageSharp/Formats/Tga/TgaImageFormatDetector.cs b/src/ImageSharp/Formats/Tga/TgaImageFormatDetector.cs index e305728473..5a0b0f44ca 100644 --- a/src/ImageSharp/Formats/Tga/TgaImageFormatDetector.cs +++ b/src/ImageSharp/Formats/Tga/TgaImageFormatDetector.cs @@ -11,7 +11,7 @@ namespace SixLabors.ImageSharp.Formats.Tga public sealed class TgaImageFormatDetector : IImageFormatDetector { /// - public int HeaderSize => 18; + public int HeaderSize => TgaConstants.FileHeaderLength; /// public IImageFormat DetectFormat(ReadOnlySpan header) @@ -21,7 +21,14 @@ namespace SixLabors.ImageSharp.Formats.Tga private bool IsSupportedFileFormat(ReadOnlySpan header) { - return header.Length >= this.HeaderSize; + if (header.Length >= this.HeaderSize) + { + // There is no magick bytes in a tga file, so at least the image type in the header will be checked for a valid value. + var imageType = (TgaImageType)header[2]; + return imageType.IsValid(); + } + + return true; } } } diff --git a/src/ImageSharp/Formats/Tga/TgaImageTypeExtensions.cs b/src/ImageSharp/Formats/Tga/TgaImageTypeExtensions.cs index 406e12d08b..38477f09f5 100644 --- a/src/ImageSharp/Formats/Tga/TgaImageTypeExtensions.cs +++ b/src/ImageSharp/Formats/Tga/TgaImageTypeExtensions.cs @@ -22,5 +22,30 @@ namespace SixLabors.ImageSharp.Formats.Tga return false; } + + /// + /// Checks, if the image type has valid value. + /// + /// The image type. + /// true, if its a valid tga image type. + public static bool IsValid(this TgaImageType imageType) + { + byte imageTypeVal = (byte)imageType; + + switch (imageTypeVal) + { + case 0: + case 1: + case 2: + case 3: + case 9: + case 10: + case 11: + return true; + + default: + return false; + } + } } } From 862018228d0e519838ab70161397af3682f5f660 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Sun, 27 Oct 2019 12:29:12 +0100 Subject: [PATCH 32/75] Add tests for bmp and tga header to throw UnknownImageFormatException when there is insufficient data --- .../Formats/Bmp/BmpFileHeaderTests.cs | 21 ++++++++++++++ .../Formats/Tga/TgaFileHeaderTests.cs | 29 +++++++++++++++++++ 2 files changed, 50 insertions(+) create mode 100644 tests/ImageSharp.Tests/Formats/Tga/TgaFileHeaderTests.cs diff --git a/tests/ImageSharp.Tests/Formats/Bmp/BmpFileHeaderTests.cs b/tests/ImageSharp.Tests/Formats/Bmp/BmpFileHeaderTests.cs index 25cf29406e..1406867086 100644 --- a/tests/ImageSharp.Tests/Formats/Bmp/BmpFileHeaderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Bmp/BmpFileHeaderTests.cs @@ -1,4 +1,10 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + using System; +using System.IO; + +using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.Formats.Bmp; using Xunit; @@ -6,6 +12,10 @@ namespace SixLabors.ImageSharp.Tests.Formats.Bmp { public class BmpFileHeaderTests { + private static readonly byte[] Data = BitConverter.GetBytes(BmpConstants.TypeMarkers.Bitmap); + + private MemoryStream Stream { get; } = new MemoryStream(Data); + [Fact] public void TestWrite() { @@ -17,5 +27,16 @@ namespace SixLabors.ImageSharp.Tests.Formats.Bmp Assert.Equal("AQACAAAAAwAAAAQAAAA=", Convert.ToBase64String(buffer)); } + + [Fact] + public void ImageLoad_WithoutEnoughData_Throws_UnknownImageFormatException() + { + Assert.Throws(() => + { + using (Image.Load(Configuration.Default, this.Stream, out IImageFormat _)) + { + } + }); + } } } diff --git a/tests/ImageSharp.Tests/Formats/Tga/TgaFileHeaderTests.cs b/tests/ImageSharp.Tests/Formats/Tga/TgaFileHeaderTests.cs new file mode 100644 index 0000000000..943144d61a --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Tga/TgaFileHeaderTests.cs @@ -0,0 +1,29 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System.IO; + +using SixLabors.ImageSharp.Formats; + +using Xunit; + +namespace SixLabors.ImageSharp.Tests.Formats.Tga +{ + public class TgaFileHeaderTests + { + private static readonly byte[] Data = { 0, 0, 0 }; + + private MemoryStream Stream { get; } = new MemoryStream(Data); + + [Fact] + public void ImageLoad_WithoutEnoughData_Throws_UnknownImageFormatException() + { + Assert.Throws(() => + { + using (Image.Load(Configuration.Default, this.Stream, out IImageFormat _)) + { + } + }); + } + } +} From 7db0caaedd4ee1e78f73a5b958db7cf20e8cc1af Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Sun, 27 Oct 2019 17:19:03 +0100 Subject: [PATCH 33/75] Throw ImageFormatException when width or height is 0 --- src/ImageSharp/Formats/Tga/TgaDecoderCore.cs | 5 +++++ .../Formats/Tga/TgaImageFormatDetector.cs | 8 +++++++- .../Formats/Bmp/BmpFileHeaderTests.cs | 16 +--------------- .../Formats/Tga/TgaFileHeaderTests.cs | 8 ++++++-- 4 files changed, 19 insertions(+), 18 deletions(-) diff --git a/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs b/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs index e1850a32b6..2d70143351 100644 --- a/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs +++ b/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs @@ -85,6 +85,11 @@ namespace SixLabors.ImageSharp.Formats.Tga TgaThrowHelper.ThrowNotSupportedException($"Unknown tga colormap type {this.fileHeader.ColorMapType} found"); } + if (this.fileHeader.Width == 0 || this.fileHeader.Height == 0) + { + throw new UnknownImageFormatException("Width or height cannot be 0"); + } + var image = new Image(this.configuration, this.fileHeader.Width, this.fileHeader.Height, this.metadata); Buffer2D pixels = image.GetRootFramePixelBuffer(); diff --git a/src/ImageSharp/Formats/Tga/TgaImageFormatDetector.cs b/src/ImageSharp/Formats/Tga/TgaImageFormatDetector.cs index 5a0b0f44ca..bd9cfa900c 100644 --- a/src/ImageSharp/Formats/Tga/TgaImageFormatDetector.cs +++ b/src/ImageSharp/Formats/Tga/TgaImageFormatDetector.cs @@ -23,7 +23,13 @@ namespace SixLabors.ImageSharp.Formats.Tga { if (header.Length >= this.HeaderSize) { - // There is no magick bytes in a tga file, so at least the image type in the header will be checked for a valid value. + // There is no magick bytes in a tga file, so at least the image type + // and the colormap type in the header will be checked for a valid value. + if (header[1] != 0 && header[1] != 1) + { + return false; + } + var imageType = (TgaImageType)header[2]; return imageType.IsValid(); } diff --git a/tests/ImageSharp.Tests/Formats/Bmp/BmpFileHeaderTests.cs b/tests/ImageSharp.Tests/Formats/Bmp/BmpFileHeaderTests.cs index 1406867086..4c3fe31493 100644 --- a/tests/ImageSharp.Tests/Formats/Bmp/BmpFileHeaderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Bmp/BmpFileHeaderTests.cs @@ -3,6 +3,7 @@ using System; using System.IO; +using System.Linq; using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.Formats.Bmp; @@ -12,10 +13,6 @@ namespace SixLabors.ImageSharp.Tests.Formats.Bmp { public class BmpFileHeaderTests { - private static readonly byte[] Data = BitConverter.GetBytes(BmpConstants.TypeMarkers.Bitmap); - - private MemoryStream Stream { get; } = new MemoryStream(Data); - [Fact] public void TestWrite() { @@ -27,16 +24,5 @@ namespace SixLabors.ImageSharp.Tests.Formats.Bmp Assert.Equal("AQACAAAAAwAAAAQAAAA=", Convert.ToBase64String(buffer)); } - - [Fact] - public void ImageLoad_WithoutEnoughData_Throws_UnknownImageFormatException() - { - Assert.Throws(() => - { - using (Image.Load(Configuration.Default, this.Stream, out IImageFormat _)) - { - } - }); - } } } diff --git a/tests/ImageSharp.Tests/Formats/Tga/TgaFileHeaderTests.cs b/tests/ImageSharp.Tests/Formats/Tga/TgaFileHeaderTests.cs index 943144d61a..c227b79576 100644 --- a/tests/ImageSharp.Tests/Formats/Tga/TgaFileHeaderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Tga/TgaFileHeaderTests.cs @@ -11,12 +11,16 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tga { public class TgaFileHeaderTests { - private static readonly byte[] Data = { 0, 0, 0 }; + private static readonly byte[] Data = { + 0, + 0, + 15 // invalid tga image type + }; private MemoryStream Stream { get; } = new MemoryStream(Data); [Fact] - public void ImageLoad_WithoutEnoughData_Throws_UnknownImageFormatException() + public void ImageLoad_WithInvalidImageType_Throws_UnknownImageFormatException() { Assert.Throws(() => { From 8b0eaf8dfdeb655767337be1237e85cad0c55dbc Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Thu, 7 Nov 2019 20:49:12 +0100 Subject: [PATCH 34/75] Code review changes --- Directory.Build.targets | 2 +- .../Formats/Tga/ITgaEncoderOptions.cs | 2 +- src/ImageSharp/Formats/Tga/TgaCompression.cs | 21 ++++++++++++ src/ImageSharp/Formats/Tga/TgaDecoderCore.cs | 33 ++++++++++++++----- src/ImageSharp/Formats/Tga/TgaEncoder.cs | 4 +-- src/ImageSharp/Formats/Tga/TgaEncoderCore.cs | 21 +++++++----- src/ImageSharp/Formats/Tga/TgaImageType.cs | 1 - .../Formats/Tga/TgaEncoderTests.cs | 22 ++++++------- .../ImageSharp.Tests/ImageSharp.Tests.csproj | 2 +- 9 files changed, 74 insertions(+), 34 deletions(-) create mode 100644 src/ImageSharp/Formats/Tga/TgaCompression.cs diff --git a/Directory.Build.targets b/Directory.Build.targets index 1dc081782a..01c1f10397 100644 --- a/Directory.Build.targets +++ b/Directory.Build.targets @@ -24,7 +24,7 @@ - + diff --git a/src/ImageSharp/Formats/Tga/ITgaEncoderOptions.cs b/src/ImageSharp/Formats/Tga/ITgaEncoderOptions.cs index ef1fecc93a..49983d2369 100644 --- a/src/ImageSharp/Formats/Tga/ITgaEncoderOptions.cs +++ b/src/ImageSharp/Formats/Tga/ITgaEncoderOptions.cs @@ -16,6 +16,6 @@ namespace SixLabors.ImageSharp.Formats.Tga /// /// Gets a value indicating whether run length compression should be used. /// - bool Compress { get; } + TgaCompression Compression { get; } } } diff --git a/src/ImageSharp/Formats/Tga/TgaCompression.cs b/src/ImageSharp/Formats/Tga/TgaCompression.cs new file mode 100644 index 0000000000..cc6e005eda --- /dev/null +++ b/src/ImageSharp/Formats/Tga/TgaCompression.cs @@ -0,0 +1,21 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +namespace SixLabors.ImageSharp.Formats.Tga +{ + /// + /// Indicates if compression is used. + /// + public enum TgaCompression + { + /// + /// No compression is used. + /// + None, + + /// + /// Run length encoding is used. + /// + RunLength, + } +} diff --git a/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs b/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs index 2d70143351..c5a4df3f96 100644 --- a/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs +++ b/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs @@ -106,16 +106,31 @@ namespace SixLabors.ImageSharp.Formats.Tga } int colorMapPixelSizeInBytes = this.fileHeader.CMapDepth / 8; - var palette = new byte[this.fileHeader.CMapLength * colorMapPixelSizeInBytes]; - this.currentStream.Read(palette, this.fileHeader.CMapStart, palette.Length); - - if (this.fileHeader.ImageType is TgaImageType.RleColorMapped) - { - this.ReadPalettedRle(this.fileHeader.Width, this.fileHeader.Height, pixels, palette, colorMapPixelSizeInBytes, inverted); - } - else + int colorMapSizeInBytes = this.fileHeader.CMapLength * colorMapPixelSizeInBytes; + using (IManagedByteBuffer palette = this.memoryAllocator.AllocateManagedByteBuffer(colorMapSizeInBytes, AllocationOptions.Clean)) { - this.ReadPaletted(this.fileHeader.Width, this.fileHeader.Height, pixels, palette, colorMapPixelSizeInBytes, inverted); + this.currentStream.Read(palette.Array, this.fileHeader.CMapStart, colorMapSizeInBytes); + + if (this.fileHeader.ImageType is TgaImageType.RleColorMapped) + { + this.ReadPalettedRle( + this.fileHeader.Width, + this.fileHeader.Height, + pixels, + palette.Array, + colorMapPixelSizeInBytes, + inverted); + } + else + { + this.ReadPaletted( + this.fileHeader.Width, + this.fileHeader.Height, + pixels, + palette.Array, + colorMapPixelSizeInBytes, + inverted); + } } return image; diff --git a/src/ImageSharp/Formats/Tga/TgaEncoder.cs b/src/ImageSharp/Formats/Tga/TgaEncoder.cs index 85b4fadfcd..52a300f2ed 100644 --- a/src/ImageSharp/Formats/Tga/TgaEncoder.cs +++ b/src/ImageSharp/Formats/Tga/TgaEncoder.cs @@ -19,9 +19,9 @@ namespace SixLabors.ImageSharp.Formats.Tga public TgaBitsPerPixel? BitsPerPixel { get; set; } /// - /// Gets or sets a value indicating whether run length compression should be used. + /// Gets or sets a value indicating whether no compression or run length compression should be used. /// - public bool Compress { get; set; } + public TgaCompression Compression { get; set; } /// public void Encode(Image image, Stream stream) diff --git a/src/ImageSharp/Formats/Tga/TgaEncoderCore.cs b/src/ImageSharp/Formats/Tga/TgaEncoderCore.cs index 139cc13fd5..8022a0636c 100644 --- a/src/ImageSharp/Formats/Tga/TgaEncoderCore.cs +++ b/src/ImageSharp/Formats/Tga/TgaEncoderCore.cs @@ -43,7 +43,12 @@ namespace SixLabors.ImageSharp.Formats.Tga /// /// Indicates if run length compression should be used. /// - private readonly bool useCompression; + private readonly TgaCompression compression; + + /// + /// Vector for converting pixel to gray value. + /// + private static readonly Vector4 Bt709 = new Vector4(.2126f, .7152f, .0722f, 0.0f); /// /// Initializes a new instance of the class. @@ -54,7 +59,7 @@ namespace SixLabors.ImageSharp.Formats.Tga { this.memoryAllocator = memoryAllocator; this.bitsPerPixel = options.BitsPerPixel; - this.useCompression = options.Compress; + this.compression = options.Compression; } /// @@ -74,14 +79,14 @@ namespace SixLabors.ImageSharp.Formats.Tga TgaMetadata tgaMetadata = metadata.GetFormatMetadata(TgaFormat.Instance); this.bitsPerPixel = this.bitsPerPixel ?? tgaMetadata.BitsPerPixel; - TgaImageType imageType = this.useCompression ? TgaImageType.RleTrueColor : TgaImageType.TrueColor; + TgaImageType imageType = this.compression is TgaCompression.RunLength ? TgaImageType.RleTrueColor : TgaImageType.TrueColor; if (this.bitsPerPixel == TgaBitsPerPixel.Pixel8) { - imageType = this.useCompression ? TgaImageType.RleBlackAndWhite : TgaImageType.BlackAndWhite; + imageType = this.compression is TgaCompression.RunLength ? TgaImageType.RleBlackAndWhite : TgaImageType.BlackAndWhite; } // If compression is used, set bit 5 of the image descriptor to indicate an left top origin. - byte imageDescriptor = (byte)(this.useCompression ? 32 : 0); + byte imageDescriptor = (byte)(this.compression is TgaCompression.RunLength ? 32 : 0); var fileHeader = new TgaFileHeader( idLength: 0, @@ -91,7 +96,7 @@ namespace SixLabors.ImageSharp.Formats.Tga cMapLength: 0, cMapDepth: 0, xOffset: 0, - yOffset: this.useCompression ? (short)image.Height : (short)0, // When run length encoding is used, the origin should be top left instead of the default bottom left. + yOffset: this.compression is TgaCompression.RunLength ? (short)image.Height : (short)0, // When run length encoding is used, the origin should be top left instead of the default bottom left. width: (short)image.Width, height: (short)image.Height, pixelDepth: (byte)this.bitsPerPixel.Value, @@ -106,7 +111,7 @@ namespace SixLabors.ImageSharp.Formats.Tga stream.Write(buffer, 0, TgaFileHeader.Size); - if (this.useCompression) + if (this.compression is TgaCompression.RunLength) { this.WriteRunLengthEndcodedImage(stream, image.Frames.RootFrame); } @@ -351,6 +356,6 @@ namespace SixLabors.ImageSharp.Formats.Tga /// The vector to get the luminance from. [MethodImpl(InliningOptions.ShortMethod)] public static int GetLuminance(ref Vector4 vector) - => (int)MathF.Round(((.2126F * vector.X) + (.7152F * vector.Y) + (.0722F * vector.Y)) * (256 - 1)); + => (int)MathF.Round(Vector4.Dot(vector, Bt709) * (256 - 1)); } } diff --git a/src/ImageSharp/Formats/Tga/TgaImageType.cs b/src/ImageSharp/Formats/Tga/TgaImageType.cs index cf0eda93c4..491fd3ea77 100644 --- a/src/ImageSharp/Formats/Tga/TgaImageType.cs +++ b/src/ImageSharp/Formats/Tga/TgaImageType.cs @@ -12,7 +12,6 @@ namespace SixLabors. { /// /// No image data included. - /// Not sure what this is used for. /// NoImageData = 0, diff --git a/tests/ImageSharp.Tests/Formats/Tga/TgaEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Tga/TgaEncoderTests.cs index 5dd49f4faa..e946729a15 100644 --- a/tests/ImageSharp.Tests/Formats/Tga/TgaEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Tga/TgaEncoderTests.cs @@ -60,7 +60,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tga { var options = new TgaEncoder() { - Compress = true + Compression = TgaCompression.RunLength }; TestFile testFile = TestFile.Create(imagePath); @@ -83,55 +83,55 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tga [WithFile(Bit32, PixelTypes.Rgba32)] public void Encode_Bit8_Works(TestImageProvider provider, TgaBitsPerPixel bitsPerPixel = TgaBitsPerPixel.Pixel8) // using tolerant comparer here. The results from magick differ slightly. Maybe a different ToGrey method is used. The image looks otherwise ok. - where TPixel : struct, IPixel => TestTgaEncoderCore(provider, bitsPerPixel, useCompression: true, useExactComparer: false, compareTolerance: 0.03f); + where TPixel : struct, IPixel => TestTgaEncoderCore(provider, bitsPerPixel, TgaCompression.None, useExactComparer: false, compareTolerance: 0.03f); [Theory] [WithFile(Bit32, PixelTypes.Rgba32)] public void Encode_Bit16_Works(TestImageProvider provider, TgaBitsPerPixel bitsPerPixel = TgaBitsPerPixel.Pixel16) - where TPixel : struct, IPixel => TestTgaEncoderCore(provider, bitsPerPixel, useCompression: false, useExactComparer: false); + where TPixel : struct, IPixel => TestTgaEncoderCore(provider, bitsPerPixel, TgaCompression.None, useExactComparer: false); [Theory] [WithFile(Bit32, PixelTypes.Rgba32)] public void Encode_Bit24_Works(TestImageProvider provider, TgaBitsPerPixel bitsPerPixel = TgaBitsPerPixel.Pixel24) - where TPixel : struct, IPixel => TestTgaEncoderCore(provider, bitsPerPixel); + where TPixel : struct, IPixel => TestTgaEncoderCore(provider, bitsPerPixel, TgaCompression.None); [Theory] [WithFile(Bit32, PixelTypes.Rgba32)] public void Encode_Bit32_Works(TestImageProvider provider, TgaBitsPerPixel bitsPerPixel = TgaBitsPerPixel.Pixel32) - where TPixel : struct, IPixel => TestTgaEncoderCore(provider, bitsPerPixel); + where TPixel : struct, IPixel => TestTgaEncoderCore(provider, bitsPerPixel, TgaCompression.None); [Theory] [WithFile(Bit32, PixelTypes.Rgba32)] public void Encode_Bit8_WithRunLengthEncoding_Works(TestImageProvider provider, TgaBitsPerPixel bitsPerPixel = TgaBitsPerPixel.Pixel8) // using tolerant comparer here. The results from magick differ slightly. Maybe a different ToGrey method is used. The image looks otherwise ok. - where TPixel : struct, IPixel => TestTgaEncoderCore(provider, bitsPerPixel, useCompression: true, useExactComparer: false, compareTolerance: 0.03f); + where TPixel : struct, IPixel => TestTgaEncoderCore(provider, bitsPerPixel, TgaCompression.RunLength, useExactComparer: false, compareTolerance: 0.03f); [Theory] [WithFile(Bit32, PixelTypes.Rgba32)] public void Encode_Bit16_WithRunLengthEncoding_Works(TestImageProvider provider, TgaBitsPerPixel bitsPerPixel = TgaBitsPerPixel.Pixel16) - where TPixel : struct, IPixel => TestTgaEncoderCore(provider, bitsPerPixel, useCompression: true, useExactComparer: false); + where TPixel : struct, IPixel => TestTgaEncoderCore(provider, bitsPerPixel, TgaCompression.RunLength, useExactComparer: false); [Theory] [WithFile(Bit32, PixelTypes.Rgba32)] public void Encode_Bit24_WithRunLengthEncoding_Works(TestImageProvider provider, TgaBitsPerPixel bitsPerPixel = TgaBitsPerPixel.Pixel24) - where TPixel : struct, IPixel => TestTgaEncoderCore(provider, bitsPerPixel, true); + where TPixel : struct, IPixel => TestTgaEncoderCore(provider, bitsPerPixel, TgaCompression.RunLength); [Theory] [WithFile(Bit32, PixelTypes.Rgba32)] public void Encode_Bit32_WithRunLengthEncoding_Works(TestImageProvider provider, TgaBitsPerPixel bitsPerPixel = TgaBitsPerPixel.Pixel32) - where TPixel : struct, IPixel => TestTgaEncoderCore(provider, bitsPerPixel, true); + where TPixel : struct, IPixel => TestTgaEncoderCore(provider, bitsPerPixel, TgaCompression.RunLength); private static void TestTgaEncoderCore( TestImageProvider provider, TgaBitsPerPixel bitsPerPixel, - bool useCompression = false, + TgaCompression compression = TgaCompression.None, bool useExactComparer = true, float compareTolerance = 0.01f) where TPixel : struct, IPixel { using (Image image = provider.GetImage()) { - var encoder = new TgaEncoder { BitsPerPixel = bitsPerPixel, Compress = useCompression}; + var encoder = new TgaEncoder { BitsPerPixel = bitsPerPixel, Compression = compression}; using (var memStream = new MemoryStream()) { diff --git a/tests/ImageSharp.Tests/ImageSharp.Tests.csproj b/tests/ImageSharp.Tests/ImageSharp.Tests.csproj index 1f6b8b4d95..1ac5f8085a 100644 --- a/tests/ImageSharp.Tests/ImageSharp.Tests.csproj +++ b/tests/ImageSharp.Tests/ImageSharp.Tests.csproj @@ -14,7 +14,7 @@ - + From c964b94e0f5107561df7e1d1902c1c21c057b407 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Fri, 8 Nov 2019 20:49:26 +0100 Subject: [PATCH 35/75] Fix converting pixel to gray in histogram equalization --- src/ImageSharp/Common/Helpers/ImageMaths.cs | 17 ++++++++++++++++- src/ImageSharp/Formats/Tga/TgaEncoderCore.cs | 15 +-------------- ...qualizationSlidingWindowProcessor{TPixel}.cs | 4 ++-- .../HistogramEqualizationProcessor{TPixel}.cs | 11 +---------- 4 files changed, 20 insertions(+), 27 deletions(-) diff --git a/src/ImageSharp/Common/Helpers/ImageMaths.cs b/src/ImageSharp/Common/Helpers/ImageMaths.cs index 7460c9cac1..c51a54a40b 100644 --- a/src/ImageSharp/Common/Helpers/ImageMaths.cs +++ b/src/ImageSharp/Common/Helpers/ImageMaths.cs @@ -1,7 +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.Numerics; using System.Runtime.CompilerServices; using SixLabors.ImageSharp.PixelFormats; @@ -14,6 +15,20 @@ namespace SixLabors.ImageSharp /// internal static class ImageMaths { + /// + /// Vector for converting pixel to gray value as specified by ITU-R Recommendation BT.709. + /// + private static readonly Vector4 Bt709 = new Vector4(.2126f, .7152f, .0722f, 0.0f); + + /// + /// Convert a pixel value to grayscale using ITU-R Recommendation BT.709. + /// + /// The vector to get the luminance from. + /// The number of luminance levels (256 for 8 bit, 65536 for 16 bit grayscale images) + [MethodImpl(InliningOptions.ShortMethod)] + public static int GetBT709Luminance(ref Vector4 vector, int luminanceLevels) + => (int)MathF.Round(Vector4.Dot(vector, Bt709) * (luminanceLevels - 1)); + /// /// Gets the luminance from the rgb components using the formula as specified by ITU-R Recommendation BT.709. /// diff --git a/src/ImageSharp/Formats/Tga/TgaEncoderCore.cs b/src/ImageSharp/Formats/Tga/TgaEncoderCore.cs index 8022a0636c..28b87e9857 100644 --- a/src/ImageSharp/Formats/Tga/TgaEncoderCore.cs +++ b/src/ImageSharp/Formats/Tga/TgaEncoderCore.cs @@ -45,11 +45,6 @@ namespace SixLabors.ImageSharp.Formats.Tga /// private readonly TgaCompression compression; - /// - /// Vector for converting pixel to gray value. - /// - private static readonly Vector4 Bt709 = new Vector4(.2126f, .7152f, .0722f, 0.0f); - /// /// Initializes a new instance of the class. /// @@ -347,15 +342,7 @@ namespace SixLabors.ImageSharp.Formats.Tga where TPixel : struct, IPixel { var vector = sourcePixel.ToVector4(); - return GetLuminance(ref vector); + return ImageMaths.GetBT709Luminance(ref vector, 256); } - - /// - /// Convert the pixel values to grayscale using ITU-R Recommendation BT.709. - /// - /// The vector to get the luminance from. - [MethodImpl(InliningOptions.ShortMethod)] - public static int GetLuminance(ref Vector4 vector) - => (int)MathF.Round(Vector4.Dot(vector, Bt709) * (256 - 1)); } } diff --git a/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistogramEqualizationSlidingWindowProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistogramEqualizationSlidingWindowProcessor{TPixel}.cs index f2f11cbfe5..622c133aeb 100644 --- a/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistogramEqualizationSlidingWindowProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistogramEqualizationSlidingWindowProcessor{TPixel}.cs @@ -349,7 +349,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Normalization { for (int idx = 0; idx < length; idx++) { - int luminance = GetLuminance(ref Unsafe.Add(ref greyValuesBase, idx), luminanceLevels); + int luminance = ImageMaths.GetBT709Luminance(ref Unsafe.Add(ref greyValuesBase, idx), luminanceLevels); Unsafe.Add(ref histogramBase, luminance)++; } } @@ -366,7 +366,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Normalization { for (int idx = 0; idx < length; idx++) { - int luminance = GetLuminance(ref Unsafe.Add(ref greyValuesBase, idx), luminanceLevels); + int luminance = ImageMaths.GetBT709Luminance(ref Unsafe.Add(ref greyValuesBase, idx), luminanceLevels); Unsafe.Add(ref histogramBase, luminance)--; } } diff --git a/src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationProcessor{TPixel}.cs index 6e4c16de76..284b9de1f6 100644 --- a/src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationProcessor{TPixel}.cs @@ -143,16 +143,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Normalization public static int GetLuminance(TPixel sourcePixel, int luminanceLevels) { var vector = sourcePixel.ToVector4(); - return GetLuminance(ref vector, luminanceLevels); + return ImageMaths.GetBT709Luminance(ref vector, luminanceLevels); } - - /// - /// Convert the pixel values to grayscale using ITU-R Recommendation BT.709. - /// - /// The vector to get the luminance from - /// The number of luminance levels (256 for 8 bit, 65536 for 16 bit grayscale images) - [MethodImpl(InliningOptions.ShortMethod)] - public static int GetLuminance(ref Vector4 vector, int luminanceLevels) - => (int)MathF.Round(((.2126F * vector.X) + (.7152F * vector.Y) + (.0722F * vector.Y)) * (luminanceLevels - 1)); } } From 2acaea2ee4cfe27b065905dbe0a8819549681a66 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Fri, 8 Nov 2019 21:16:38 +0100 Subject: [PATCH 36/75] Add benchmarks for tga images --- src/ImageSharp/Formats/Tga/TgaEncoder.cs | 2 +- .../Formats/Tga/TgaImageTypeExtensions.cs | 18 +++---- .../ImageSharp.Benchmarks/Codecs/DecodeTga.cs | 42 +++++++++++++++ .../ImageSharp.Benchmarks/Codecs/EncodeTga.cs | 54 +++++++++++++++++++ .../ImageSharp.Benchmarks.csproj | 1 + 5 files changed, 106 insertions(+), 11 deletions(-) create mode 100644 tests/ImageSharp.Benchmarks/Codecs/DecodeTga.cs create mode 100644 tests/ImageSharp.Benchmarks/Codecs/EncodeTga.cs diff --git a/src/ImageSharp/Formats/Tga/TgaEncoder.cs b/src/ImageSharp/Formats/Tga/TgaEncoder.cs index 52a300f2ed..2fcbb822f5 100644 --- a/src/ImageSharp/Formats/Tga/TgaEncoder.cs +++ b/src/ImageSharp/Formats/Tga/TgaEncoder.cs @@ -21,7 +21,7 @@ namespace SixLabors.ImageSharp.Formats.Tga /// /// Gets or sets a value indicating whether no compression or run length compression should be used. /// - public TgaCompression Compression { get; set; } + public TgaCompression Compression { get; set; } = TgaCompression.RunLength; /// public void Encode(Image image, Stream stream) diff --git a/src/ImageSharp/Formats/Tga/TgaImageTypeExtensions.cs b/src/ImageSharp/Formats/Tga/TgaImageTypeExtensions.cs index 38477f09f5..6a30cdddd7 100644 --- a/src/ImageSharp/Formats/Tga/TgaImageTypeExtensions.cs +++ b/src/ImageSharp/Formats/Tga/TgaImageTypeExtensions.cs @@ -30,17 +30,15 @@ namespace SixLabors.ImageSharp.Formats.Tga /// true, if its a valid tga image type. public static bool IsValid(this TgaImageType imageType) { - byte imageTypeVal = (byte)imageType; - - switch (imageTypeVal) + switch (imageType) { - case 0: - case 1: - case 2: - case 3: - case 9: - case 10: - case 11: + case TgaImageType.NoImageData: + case TgaImageType.ColorMapped: + case TgaImageType.TrueColor: + case TgaImageType.BlackAndWhite: + case TgaImageType.RleColorMapped: + case TgaImageType.RleTrueColor: + case TgaImageType.RleBlackAndWhite: return true; default: diff --git a/tests/ImageSharp.Benchmarks/Codecs/DecodeTga.cs b/tests/ImageSharp.Benchmarks/Codecs/DecodeTga.cs new file mode 100644 index 0000000000..e3c7216102 --- /dev/null +++ b/tests/ImageSharp.Benchmarks/Codecs/DecodeTga.cs @@ -0,0 +1,42 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System.IO; + +using BenchmarkDotNet.Attributes; + +using ImageMagick; + +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Tests; +using SixLabors.Primitives; + +namespace SixLabors.ImageSharp.Benchmarks.Codecs +{ + [Config(typeof(Config.ShortClr))] + public class DecodeTga : BenchmarkBase + { + private string TestImageFullPath => Path.Combine(TestEnvironment.InputImagesDirectoryFullPath, this.TestImage); + + [Params(TestImages.Tga.Bit24)] + public string TestImage { get; set; } + + [Benchmark(Baseline = true, Description = "ImageMagick Tga")] + public Size TgaImageMagick() + { + using (var magickImage = new MagickImage(this.TestImageFullPath)) + { + return new Size(magickImage.Width, magickImage.Height); + } + } + + [Benchmark(Description = "ImageSharp Tga")] + public Size TgaCore() + { + using (var image = Image.Load(this.TestImageFullPath)) + { + return new Size(image.Width, image.Height); + } + } + } +} diff --git a/tests/ImageSharp.Benchmarks/Codecs/EncodeTga.cs b/tests/ImageSharp.Benchmarks/Codecs/EncodeTga.cs new file mode 100644 index 0000000000..ddcbec218e --- /dev/null +++ b/tests/ImageSharp.Benchmarks/Codecs/EncodeTga.cs @@ -0,0 +1,54 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System.IO; + +using BenchmarkDotNet.Attributes; + +using ImageMagick; + +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Tests; + +namespace SixLabors.ImageSharp.Benchmarks.Codecs +{ + [Config(typeof(Config.ShortClr))] + public class EncodeTga : BenchmarkBase + { + private MagickImage tgaMagick; + private Image tgaCore; + + private string TestImageFullPath => Path.Combine(TestEnvironment.InputImagesDirectoryFullPath, this.TestImage); + + [Params(TestImages.Tga.Bit24)] + public string TestImage { get; set; } + + [GlobalSetup] + public void ReadImages() + { + if (this.tgaCore == null) + { + this.tgaCore = Image.Load(TestImageFullPath); + this.tgaMagick = new MagickImage(this.TestImageFullPath); + } + } + + [Benchmark(Baseline = true, Description = "Magick Tga")] + public void BmpSystemDrawing() + { + using (var memoryStream = new MemoryStream()) + { + this.tgaMagick.Write(memoryStream, MagickFormat.Tga); + } + } + + [Benchmark(Description = "ImageSharp Tga")] + public void BmpCore() + { + using (var memoryStream = new MemoryStream()) + { + this.tgaCore.SaveAsBmp(memoryStream); + } + } + } +} diff --git a/tests/ImageSharp.Benchmarks/ImageSharp.Benchmarks.csproj b/tests/ImageSharp.Benchmarks/ImageSharp.Benchmarks.csproj index 14ad5635cd..a57d388a95 100644 --- a/tests/ImageSharp.Benchmarks/ImageSharp.Benchmarks.csproj +++ b/tests/ImageSharp.Benchmarks/ImageSharp.Benchmarks.csproj @@ -16,6 +16,7 @@ + From bd80c72a8212a42063a9e37b4972725dade75143 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Sat, 9 Nov 2019 11:27:09 +0100 Subject: [PATCH 37/75] Add width and height as parameter for makeopaque, change currentPosition to pixelDataStart --- src/ImageSharp/Formats/Tga/TgaDecoderCore.cs | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs b/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs index c5a4df3f96..aa830057e8 100644 --- a/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs +++ b/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs @@ -366,7 +366,7 @@ namespace SixLabors.ImageSharp.Formats.Tga { Span bgraRowSpan = bgraRow.GetSpan(); long currentPosition = this.currentStream.Position; - for (int y = 0; y < this.fileHeader.Height; y++) + for (int y = 0; y < height; y++) { this.currentStream.Read(row); int newY = Invert(y, height, inverted); @@ -379,7 +379,7 @@ namespace SixLabors.ImageSharp.Formats.Tga } // We need to set each alpha component value to fully opaque. - this.MakeOpaque(pixels, currentPosition, row, bgraRowSpan); + this.MakeOpaque(pixels, width, height, currentPosition, row, bgraRowSpan); } } @@ -551,15 +551,17 @@ namespace SixLabors.ImageSharp.Formats.Tga /// /// The pixel type. /// The destination pixel buffer. - /// The start position of pixel data. + /// The width of the image. + /// The height of the image. + /// The start position of pixel data. /// A byte array to store the read pixel data. /// Bgra pixel row span. - private void MakeOpaque(Buffer2D pixels, long currentPosition, IManagedByteBuffer row, Span bgraRowSpan) + private void MakeOpaque(Buffer2D pixels, int width, int height, long pixelDataStart, IManagedByteBuffer row, Span bgraRowSpan) where TPixel : struct, IPixel { // Reset our stream for a second pass. - this.currentStream.Position = currentPosition; - for (int y = 0; y < this.fileHeader.Height; y++) + this.currentStream.Position = pixelDataStart; + for (int y = 0; y < width; y++) { this.currentStream.Read(row); PixelOperations.Instance.FromBgra5551Bytes( @@ -567,9 +569,9 @@ namespace SixLabors.ImageSharp.Formats.Tga row.GetSpan(), bgraRowSpan, this.fileHeader.Width); - Span pixelSpan = pixels.GetRowSpan(this.fileHeader.Height - y - 1); + Span pixelSpan = pixels.GetRowSpan(height - y - 1); - for (int x = 0; x < this.fileHeader.Width; x++) + for (int x = 0; x < width; x++) { Bgra5551 bgra = bgraRowSpan[x]; bgra.PackedValue = (ushort)(bgra.PackedValue | (1 << 15)); From 28570402ddc9ec8182724cd525b798fc42f52f33 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Sat, 9 Nov 2019 18:11:55 +0100 Subject: [PATCH 38/75] Avoid second iteration over the stream in ReadBgra16 to make it opaque --- src/ImageSharp/Formats/Tga/TgaDecoderCore.cs | 53 ++++---------------- 1 file changed, 9 insertions(+), 44 deletions(-) diff --git a/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs b/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs index aa830057e8..d861450e04 100644 --- a/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs +++ b/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs @@ -362,24 +362,26 @@ namespace SixLabors.ImageSharp.Formats.Tga where TPixel : struct, IPixel { using (IManagedByteBuffer row = this.memoryAllocator.AllocatePaddedPixelRowBuffer(width, 2, 0)) - using (IMemoryOwner bgraRow = this.memoryAllocator.Allocate(width)) { - Span bgraRowSpan = bgraRow.GetSpan(); - long currentPosition = this.currentStream.Position; for (int y = 0; y < height; y++) { this.currentStream.Read(row); + Span rowSpan = row.GetSpan(); + + // We need to set each alpha component value to fully opaque. + for (int x = 1; x < rowSpan.Length; x += 2) + { + rowSpan[x] = (byte)(rowSpan[x] | (1 << 7)); + } + int newY = Invert(y, height, inverted); Span pixelSpan = pixels.GetRowSpan(newY); PixelOperations.Instance.FromBgra5551Bytes( this.configuration, - row.GetSpan(), + rowSpan, pixelSpan, width); } - - // We need to set each alpha component value to fully opaque. - this.MakeOpaque(pixels, width, height, currentPosition, row, bgraRowSpan); } } @@ -544,43 +546,6 @@ namespace SixLabors.ImageSharp.Formats.Tga } } - /// - /// Helper method for decoding BGRA5551 images. Makes the pixels opaque, because the high bit does not - /// represent an alpha channel. - /// TODO: maybe there is a better/faster way to achieve this. - /// - /// The pixel type. - /// The destination pixel buffer. - /// The width of the image. - /// The height of the image. - /// The start position of pixel data. - /// A byte array to store the read pixel data. - /// Bgra pixel row span. - private void MakeOpaque(Buffer2D pixels, int width, int height, long pixelDataStart, IManagedByteBuffer row, Span bgraRowSpan) - where TPixel : struct, IPixel - { - // Reset our stream for a second pass. - this.currentStream.Position = pixelDataStart; - for (int y = 0; y < width; y++) - { - this.currentStream.Read(row); - PixelOperations.Instance.FromBgra5551Bytes( - this.configuration, - row.GetSpan(), - bgraRowSpan, - this.fileHeader.Width); - Span pixelSpan = pixels.GetRowSpan(height - y - 1); - - for (int x = 0; x < width; x++) - { - Bgra5551 bgra = bgraRowSpan[x]; - bgra.PackedValue = (ushort)(bgra.PackedValue | (1 << 15)); - ref TPixel pixel = ref pixelSpan[x]; - pixel.FromBgra5551(bgra); - } - } - } - /// /// Returns the y- value based on the given height. /// From 8abc5786b946c855651888f4883965f432c5dff7 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Tue, 5 Nov 2019 20:55:37 +1100 Subject: [PATCH 39/75] Fix color and pixel operations. Touches #967 #1002 --- .../Processing/GradientBrush.cs | 21 +++++++------------ .../Processing/ImageBrush.cs | 4 ++-- .../Processing/PathGradientBrush.cs | 8 +++---- .../Processing/PatternBrush.cs | 8 +++---- src/ImageSharp/ImageSharp.csproj | 4 ++-- 5 files changed, 19 insertions(+), 26 deletions(-) diff --git a/src/ImageSharp.Drawing/Processing/GradientBrush.cs b/src/ImageSharp.Drawing/Processing/GradientBrush.cs index 9826748c46..bc87a26f3b 100644 --- a/src/ImageSharp.Drawing/Processing/GradientBrush.cs +++ b/src/ImageSharp.Drawing/Processing/GradientBrush.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; @@ -92,10 +92,10 @@ namespace SixLabors.ImageSharp.Processing // onLocalGradient = Math.Min(0, Math.Max(1, onLocalGradient)); break; case GradientRepetitionMode.Repeat: - positionOnCompleteGradient = positionOnCompleteGradient % 1; + positionOnCompleteGradient %= 1; break; case GradientRepetitionMode.Reflect: - positionOnCompleteGradient = positionOnCompleteGradient % 2; + positionOnCompleteGradient %= 2; if (positionOnCompleteGradient > 1) { positionOnCompleteGradient = 2 - positionOnCompleteGradient; @@ -121,19 +121,12 @@ namespace SixLabors.ImageSharp.Processing } else { - var fromAsVector = from.Color.ToVector4(); - var toAsVector = to.Color.ToVector4(); float onLocalGradient = (positionOnCompleteGradient - from.Ratio) / (to.Ratio - from.Ratio); // TODO: this should be changeble for different gradienting functions - Vector4 result = PorterDuffFunctions.NormalSrcOver( - fromAsVector, - toAsVector, - onLocalGradient); - - TPixel resultColor = default; - resultColor.FromVector4(result); - return resultColor; + return PixelOperations + .Instance.GetPixelBlender(PixelColorBlendingMode.Normal, PixelAlphaCompositionMode.SrcOver) + .Blend(from.Color.ToPixel(), to.Color.ToPixel(), onLocalGradient); } } } @@ -176,4 +169,4 @@ namespace SixLabors.ImageSharp.Processing } } } -} \ No newline at end of file +} diff --git a/src/ImageSharp.Drawing/Processing/ImageBrush.cs b/src/ImageSharp.Drawing/Processing/ImageBrush.cs index 8485ddfd09..d56b1fccc2 100644 --- a/src/ImageSharp.Drawing/Processing/ImageBrush.cs +++ b/src/ImageSharp.Drawing/Processing/ImageBrush.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; @@ -167,4 +167,4 @@ namespace SixLabors.ImageSharp.Processing } } } -} \ No newline at end of file +} diff --git a/src/ImageSharp.Drawing/Processing/PathGradientBrush.cs b/src/ImageSharp.Drawing/Processing/PathGradientBrush.cs index 7315dc5a3e..d2ad23a0d9 100644 --- a/src/ImageSharp.Drawing/Processing/PathGradientBrush.cs +++ b/src/ImageSharp.Drawing/Processing/PathGradientBrush.cs @@ -105,7 +105,7 @@ namespace SixLabors.ImageSharp.Processing "One or more color is needed to construct a path gradient brush."); } - return new Color(colors.Select(c => c.ToVector4()).Aggregate((p1, p2) => p1 + p2) / colors.Length); + return new Color(colors.Select(c => (Vector4)c).Aggregate((p1, p2) => p1 + p2) / colors.Length); } private static float DistanceBetween(PointF p1, PointF p2) => ((Vector2)(p2 - p1)).Length(); @@ -141,10 +141,10 @@ namespace SixLabors.ImageSharp.Processing Vector2[] points = path.LineSegments.SelectMany(s => s.Flatten()).Select(p => (Vector2)p).ToArray(); this.Start = points.First(); - this.StartColor = startColor.ToVector4(); + this.StartColor = (Vector4)startColor; this.End = points.Last(); - this.EndColor = endColor.ToVector4(); + this.EndColor = (Vector4)endColor; this.length = DistanceBetween(this.End, this.Start); this.buffer = new PointF[this.path.MaxIntersections]; @@ -215,7 +215,7 @@ namespace SixLabors.ImageSharp.Processing PointF[] points = edges.Select(s => s.Start).ToArray(); this.center = points.Aggregate((p1, p2) => p1 + p2) / edges.Count; - this.centerColor = centerColor.ToVector4(); + this.centerColor = (Vector4)centerColor; this.maxDistance = points.Select(p => (Vector2)(p - this.center)).Select(d => d.Length()).Max(); } diff --git a/src/ImageSharp.Drawing/Processing/PatternBrush.cs b/src/ImageSharp.Drawing/Processing/PatternBrush.cs index 1999af8a39..d0db44bb34 100644 --- a/src/ImageSharp.Drawing/Processing/PatternBrush.cs +++ b/src/ImageSharp.Drawing/Processing/PatternBrush.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; @@ -61,8 +61,8 @@ namespace SixLabors.ImageSharp.Processing /// The pattern. internal PatternBrush(Color foreColor, Color backColor, in DenseMatrix pattern) { - var foreColorVector = foreColor.ToVector4(); - var backColorVector = backColor.ToVector4(); + var foreColorVector = (Vector4)foreColor; + var backColorVector = (Vector4)backColor; this.pattern = new DenseMatrix(pattern.Columns, pattern.Rows); this.patternVector = new DenseMatrix(pattern.Columns, pattern.Rows); for (int i = 0; i < pattern.Data.Length; i++) @@ -181,4 +181,4 @@ namespace SixLabors.ImageSharp.Processing } } } -} \ No newline at end of file +} diff --git a/src/ImageSharp/ImageSharp.csproj b/src/ImageSharp/ImageSharp.csproj index 86b0848663..d9e9a8d219 100644 --- a/src/ImageSharp/ImageSharp.csproj +++ b/src/ImageSharp/ImageSharp.csproj @@ -1,4 +1,4 @@ - + @@ -119,7 +119,7 @@ - + From b5957e578ee177bbd3b040c28437686ce2a890f9 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Tue, 5 Nov 2019 21:18:38 +1100 Subject: [PATCH 40/75] Fix span access --- src/ImageSharp.Drawing/Primitives/ShapeRegion.cs | 6 +++--- .../Processing/BrushApplicator.cs | 6 +++--- src/ImageSharp.Drawing/Processing/ImageBrush.cs | 4 ++-- src/ImageSharp.Drawing/Processing/PatternBrush.cs | 4 ++-- .../Processors/Drawing/FillProcessor{TPixel}.cs | 4 ++-- .../Drawing/FillRegionProcessor{TPixel}.cs | 4 ++-- .../Processors/Text/DrawTextProcessor{TPixel}.cs | 4 ++-- src/ImageSharp.Drawing/Processing/RecolorBrush.cs | 8 ++++---- src/ImageSharp.Drawing/Processing/SolidBrush.cs | 14 +++++++------- src/ImageSharp/Primitives/DenseMatrix{T}.cs | 8 ++++---- 10 files changed, 31 insertions(+), 31 deletions(-) diff --git a/src/ImageSharp.Drawing/Primitives/ShapeRegion.cs b/src/ImageSharp.Drawing/Primitives/ShapeRegion.cs index f4a6458206..c008f4419e 100644 --- a/src/ImageSharp.Drawing/Primitives/ShapeRegion.cs +++ b/src/ImageSharp.Drawing/Primitives/ShapeRegion.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; @@ -49,7 +49,7 @@ namespace SixLabors.ImageSharp.Primitives using (IMemoryOwner tempBuffer = configuration.MemoryAllocator.Allocate(buffer.Length)) { - Span innerBuffer = tempBuffer.GetSpan(); + Span innerBuffer = tempBuffer.Memory.Span; int count = this.Shape.FindIntersections(start, end, innerBuffer); for (int i = 0; i < count; i++) @@ -61,4 +61,4 @@ namespace SixLabors.ImageSharp.Primitives } } } -} \ No newline at end of file +} diff --git a/src/ImageSharp.Drawing/Processing/BrushApplicator.cs b/src/ImageSharp.Drawing/Processing/BrushApplicator.cs index 7e75d7effc..ab601bb8b0 100644 --- a/src/ImageSharp.Drawing/Processing/BrushApplicator.cs +++ b/src/ImageSharp.Drawing/Processing/BrushApplicator.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; @@ -71,8 +71,8 @@ namespace SixLabors.ImageSharp.Processing using (IMemoryOwner amountBuffer = memoryAllocator.Allocate(scanline.Length)) using (IMemoryOwner overlay = memoryAllocator.Allocate(scanline.Length)) { - Span amountSpan = amountBuffer.GetSpan(); - Span overlaySpan = overlay.GetSpan(); + Span amountSpan = amountBuffer.Memory.Span; + Span overlaySpan = overlay.Memory.Span; for (int i = 0; i < scanline.Length; i++) { diff --git a/src/ImageSharp.Drawing/Processing/ImageBrush.cs b/src/ImageSharp.Drawing/Processing/ImageBrush.cs index d56b1fccc2..ebcc2c0677 100644 --- a/src/ImageSharp.Drawing/Processing/ImageBrush.cs +++ b/src/ImageSharp.Drawing/Processing/ImageBrush.cs @@ -140,8 +140,8 @@ namespace SixLabors.ImageSharp.Processing using (IMemoryOwner amountBuffer = this.Target.MemoryAllocator.Allocate(scanline.Length)) using (IMemoryOwner overlay = this.Target.MemoryAllocator.Allocate(scanline.Length)) { - Span amountSpan = amountBuffer.GetSpan(); - Span overlaySpan = overlay.GetSpan(); + Span amountSpan = amountBuffer.Memory.Span; + Span overlaySpan = overlay.Memory.Span; int sourceY = (y - this.offsetY) % this.yLength; int offsetX = x - this.offsetX; diff --git a/src/ImageSharp.Drawing/Processing/PatternBrush.cs b/src/ImageSharp.Drawing/Processing/PatternBrush.cs index d0db44bb34..1b7db970a3 100644 --- a/src/ImageSharp.Drawing/Processing/PatternBrush.cs +++ b/src/ImageSharp.Drawing/Processing/PatternBrush.cs @@ -159,8 +159,8 @@ namespace SixLabors.ImageSharp.Processing using (IMemoryOwner amountBuffer = memoryAllocator.Allocate(scanline.Length)) using (IMemoryOwner overlay = memoryAllocator.Allocate(scanline.Length)) { - Span amountSpan = amountBuffer.GetSpan(); - Span overlaySpan = overlay.GetSpan(); + Span amountSpan = amountBuffer.Memory.Span; + Span overlaySpan = overlay.Memory.Span; for (int i = 0; i < scanline.Length; i++) { diff --git a/src/ImageSharp.Drawing/Processing/Processors/Drawing/FillProcessor{TPixel}.cs b/src/ImageSharp.Drawing/Processing/Processors/Drawing/FillProcessor{TPixel}.cs index 4e052818da..1dd4be1354 100644 --- a/src/ImageSharp.Drawing/Processing/Processors/Drawing/FillProcessor{TPixel}.cs +++ b/src/ImageSharp.Drawing/Processing/Processors/Drawing/FillProcessor{TPixel}.cs @@ -88,7 +88,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Drawing sourceRectangle, options)) { - amount.GetSpan().Fill(1f); + amount.Memory.Span.Fill(1f); ParallelHelper.IterateRows( workingRect, @@ -100,7 +100,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Drawing int offsetY = y - startY; int offsetX = minX - startX; - applicator.Apply(amount.GetSpan(), offsetX, offsetY); + applicator.Apply(amount.Memory.Span, offsetX, offsetY); } }); } diff --git a/src/ImageSharp.Drawing/Processing/Processors/Drawing/FillRegionProcessor{TPixel}.cs b/src/ImageSharp.Drawing/Processing/Processors/Drawing/FillRegionProcessor{TPixel}.cs index 45d5015ae0..69f97ce75f 100644 --- a/src/ImageSharp.Drawing/Processing/Processors/Drawing/FillRegionProcessor{TPixel}.cs +++ b/src/ImageSharp.Drawing/Processing/Processors/Drawing/FillRegionProcessor{TPixel}.cs @@ -81,8 +81,8 @@ namespace SixLabors.ImageSharp.Processing.Processors.Drawing float subpixelFraction = 1f / subpixelCount; float subpixelFractionPoint = subpixelFraction / subpixelCount; - Span buffer = bBuffer.GetSpan(); - Span scanline = bScanline.GetSpan(); + Span buffer = bBuffer.Memory.Span; + Span scanline = bScanline.Memory.Span; bool isSolidBrushWithoutBlending = this.IsSolidBrushWithoutBlending(out SolidBrush solidBrush); TPixel solidBrushColor = isSolidBrushWithoutBlending ? solidBrush.Color.ToPixel() : default; diff --git a/src/ImageSharp.Drawing/Processing/Processors/Text/DrawTextProcessor{TPixel}.cs b/src/ImageSharp.Drawing/Processing/Processors/Text/DrawTextProcessor{TPixel}.cs index ea042635dd..c2e38f67be 100644 --- a/src/ImageSharp.Drawing/Processing/Processors/Text/DrawTextProcessor{TPixel}.cs +++ b/src/ImageSharp.Drawing/Processing/Processors/Text/DrawTextProcessor{TPixel}.cs @@ -326,6 +326,8 @@ namespace SixLabors.ImageSharp.Processing.Processors.Text { float subpixelFraction = 1f / subpixelCount; float subpixelFractionPoint = subpixelFraction / subpixelCount; + Span intersectionSpan = rowIntersectionBuffer.Memory.Span; + Span buffer = bufferBacking.Memory.Span; for (int y = 0; y <= size.Height; y++) { @@ -337,8 +339,6 @@ namespace SixLabors.ImageSharp.Processing.Processors.Text { var start = new PointF(path.Bounds.Left - 1, subPixel); var end = new PointF(path.Bounds.Right + 1, subPixel); - Span intersectionSpan = rowIntersectionBuffer.GetSpan(); - Span buffer = bufferBacking.GetSpan(); int pointsFound = path.FindIntersections(start, end, intersectionSpan); if (pointsFound == 0) diff --git a/src/ImageSharp.Drawing/Processing/RecolorBrush.cs b/src/ImageSharp.Drawing/Processing/RecolorBrush.cs index fca95be327..d93110972c 100644 --- a/src/ImageSharp.Drawing/Processing/RecolorBrush.cs +++ b/src/ImageSharp.Drawing/Processing/RecolorBrush.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; @@ -151,8 +151,8 @@ namespace SixLabors.ImageSharp.Processing using (IMemoryOwner amountBuffer = memoryAllocator.Allocate(scanline.Length)) using (IMemoryOwner overlay = memoryAllocator.Allocate(scanline.Length)) { - Span amountSpan = amountBuffer.GetSpan(); - Span overlaySpan = overlay.GetSpan(); + Span amountSpan = amountBuffer.Memory.Span; + Span overlaySpan = overlay.Memory.Span; for (int i = 0; i < scanline.Length; i++) { @@ -176,4 +176,4 @@ namespace SixLabors.ImageSharp.Processing } } } -} \ No newline at end of file +} diff --git a/src/ImageSharp.Drawing/Processing/SolidBrush.cs b/src/ImageSharp.Drawing/Processing/SolidBrush.cs index c62566f6b7..71738de52b 100644 --- a/src/ImageSharp.Drawing/Processing/SolidBrush.cs +++ b/src/ImageSharp.Drawing/Processing/SolidBrush.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; @@ -62,7 +62,7 @@ namespace SixLabors.ImageSharp.Processing : base(source, options) { this.Colors = source.MemoryAllocator.Allocate(source.Width); - this.Colors.GetSpan().Fill(color); + this.Colors.Memory.Span.Fill(color); } /// @@ -78,7 +78,7 @@ namespace SixLabors.ImageSharp.Processing /// /// The color /// - internal override TPixel this[int x, int y] => this.Colors.GetSpan()[x]; + internal override TPixel this[int x, int y] => this.Colors.Memory.Span[x]; /// public override void Dispose() @@ -106,13 +106,13 @@ namespace SixLabors.ImageSharp.Processing if (this.Options.BlendPercentage == 1f) { - this.Blender.Blend(configuration, destinationRow, destinationRow, this.Colors.GetSpan(), scanline); + this.Blender.Blend(configuration, destinationRow, destinationRow, this.Colors.Memory.Span, scanline); } else { using (IMemoryOwner amountBuffer = memoryAllocator.Allocate(scanline.Length)) { - Span amountSpan = amountBuffer.GetSpan(); + Span amountSpan = amountBuffer.Memory.Span; for (int i = 0; i < scanline.Length; i++) { @@ -123,11 +123,11 @@ namespace SixLabors.ImageSharp.Processing configuration, destinationRow, destinationRow, - this.Colors.GetSpan(), + this.Colors.Memory.Span, amountSpan); } } } } } -} \ No newline at end of file +} diff --git a/src/ImageSharp/Primitives/DenseMatrix{T}.cs b/src/ImageSharp/Primitives/DenseMatrix{T}.cs index 170292e29e..cc5e4a90a3 100644 --- a/src/ImageSharp/Primitives/DenseMatrix{T}.cs +++ b/src/ImageSharp/Primitives/DenseMatrix{T}.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; @@ -98,9 +98,9 @@ namespace SixLabors.ImageSharp.Primitives } /// - /// Gets a Span wrapping the Data. + /// Gets a span wrapping the . /// - internal Span Span => new Span(this.Data); + public Span Span => new Span(this.Data); /// /// Gets or sets the item at the specified position. @@ -222,4 +222,4 @@ namespace SixLabors.ImageSharp.Primitives /// public override int GetHashCode() => this.Data.GetHashCode(); } -} \ No newline at end of file +} From 878dec4995278915e1f7cdc3f1ab44ec36865650 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Tue, 5 Nov 2019 21:23:33 +1100 Subject: [PATCH 41/75] Allow bulk Color => TPixel conversion. --- src/ImageSharp/Color/Color.cs | 35 +++++++++++++++++++---------------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/src/ImageSharp/Color/Color.cs b/src/ImageSharp/Color/Color.cs index 76f3995171..5fad7a8e39 100644 --- a/src/ImageSharp/Color/Color.cs +++ b/src/ImageSharp/Color/Color.cs @@ -133,8 +133,7 @@ namespace SixLabors.ImageSharp public override string ToString() => this.ToHex(); /// - /// Converts the color instance to an - /// implementation defined by . + /// Converts the color instance to a specified type. /// /// The pixel type to convert to. /// The pixel value. @@ -147,6 +146,24 @@ namespace SixLabors.ImageSharp return pixel; } + /// + /// Bulk converts a span of to a span of a specified type. + /// + /// The pixel type to convert to. + /// The configuration. + /// The source color span. + /// The destination pixel span. + [MethodImpl(InliningOptions.ShortMethod)] + public static void ToPixel( + Configuration configuration, + ReadOnlySpan source, + Span destination) + where TPixel : struct, IPixel + { + ReadOnlySpan rgba64Span = MemoryMarshal.Cast(source); + PixelOperations.Instance.FromRgba64(configuration, rgba64Span, destination); + } + /// [MethodImpl(InliningOptions.ShortMethod)] public bool Equals(Color other) @@ -166,19 +183,5 @@ namespace SixLabors.ImageSharp { return this.data.PackedValue.GetHashCode(); } - - /// - /// Bulk convert a span of to a span of a specified pixel type. - /// - [MethodImpl(InliningOptions.ShortMethod)] - internal static void ToPixel( - Configuration configuration, - ReadOnlySpan source, - Span destination) - where TPixel : struct, IPixel - { - ReadOnlySpan rgba64Span = MemoryMarshal.Cast(source); - PixelOperations.Instance.FromRgba64(configuration, rgba64Span, destination); - } } } From effa36a7ee4963847a717961faca28eeef26342c Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Tue, 5 Nov 2019 22:07:49 +1100 Subject: [PATCH 42/75] Fix GraphicsOptions --- .../Extensions/GraphicsOptionsExtensions.cs | 53 ++++++++++++++ src/ImageSharp/GraphicsOptions.cs | 72 ++++--------------- 2 files changed, 65 insertions(+), 60 deletions(-) create mode 100644 src/ImageSharp.Drawing/Common/Extensions/GraphicsOptionsExtensions.cs diff --git a/src/ImageSharp.Drawing/Common/Extensions/GraphicsOptionsExtensions.cs b/src/ImageSharp.Drawing/Common/Extensions/GraphicsOptionsExtensions.cs new file mode 100644 index 0000000000..c32d0a46e7 --- /dev/null +++ b/src/ImageSharp.Drawing/Common/Extensions/GraphicsOptionsExtensions.cs @@ -0,0 +1,53 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System.Numerics; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp +{ + /// + /// Extensions methods fpor the class. + /// + internal static class GraphicsOptionsExtensions + { + /// + /// Evaluates if a given SOURCE color can completely replace a BACKDROP color given the current blending and composition settings. + /// + /// The graphics options. + /// The source color. + /// true if the color can be considered opaque + /// + /// Blending and composition is an expensive operation, in some cases, like + /// filling with a solid color, the blending can be avoided by a plain color replacement. + /// This method can be useful for such processors to select the fast path. + /// + public static bool IsOpaqueColorWithoutBlending(this GraphicsOptions options, Color color) + { + if (options.ColorBlendingMode != PixelColorBlendingMode.Normal) + { + return false; + } + + if (options.AlphaCompositionMode != PixelAlphaCompositionMode.SrcOver + && options.AlphaCompositionMode != PixelAlphaCompositionMode.Src) + { + return false; + } + + const float Opaque = 1F; + + if (options.BlendPercentage != Opaque) + { + return false; + } + + if (((Vector4)color).W != Opaque) + { + return false; + } + + return true; + } + } +} diff --git a/src/ImageSharp/GraphicsOptions.cs b/src/ImageSharp/GraphicsOptions.cs index 214b10810a..644bde1cff 100644 --- a/src/ImageSharp/GraphicsOptions.cs +++ b/src/ImageSharp/GraphicsOptions.cs @@ -1,4 +1,4 @@ -// Copyright (c) Six Labors and contributors. +// Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. using SixLabors.ImageSharp.PixelFormats; @@ -21,18 +21,14 @@ namespace SixLabors.ImageSharp private bool? antialias; - private PixelColorBlendingMode colorBlendingMode; - - private PixelAlphaCompositionMode alphaCompositionMode; - /// /// Initializes a new instance of the struct. /// /// If set to true [enable antialiasing]. public GraphicsOptions(bool enableAntialiasing) { - this.colorBlendingMode = PixelColorBlendingMode.Normal; - this.alphaCompositionMode = PixelAlphaCompositionMode.SrcOver; + this.ColorBlendingMode = PixelColorBlendingMode.Normal; + this.AlphaCompositionMode = PixelAlphaCompositionMode.SrcOver; this.blendPercentage = 1; this.antialiasSubpixelDepth = 16; this.antialias = enableAntialiasing; @@ -47,8 +43,8 @@ namespace SixLabors.ImageSharp { Guard.MustBeBetweenOrEqualTo(opacity, 0, 1, nameof(opacity)); - this.colorBlendingMode = PixelColorBlendingMode.Normal; - this.alphaCompositionMode = PixelAlphaCompositionMode.SrcOver; + this.ColorBlendingMode = PixelColorBlendingMode.Normal; + this.AlphaCompositionMode = PixelAlphaCompositionMode.SrcOver; this.blendPercentage = opacity; this.antialiasSubpixelDepth = 16; this.antialias = enableAntialiasing; @@ -64,8 +60,8 @@ namespace SixLabors.ImageSharp { Guard.MustBeBetweenOrEqualTo(opacity, 0, 1, nameof(opacity)); - this.colorBlendingMode = blending; - this.alphaCompositionMode = PixelAlphaCompositionMode.SrcOver; + this.ColorBlendingMode = blending; + this.AlphaCompositionMode = PixelAlphaCompositionMode.SrcOver; this.blendPercentage = opacity; this.antialiasSubpixelDepth = 16; this.antialias = enableAntialiasing; @@ -82,8 +78,8 @@ namespace SixLabors.ImageSharp { Guard.MustBeBetweenOrEqualTo(opacity, 0, 1, nameof(opacity)); - this.colorBlendingMode = blending; - this.alphaCompositionMode = composition; + this.ColorBlendingMode = blending; + this.AlphaCompositionMode = composition; this.blendPercentage = opacity; this.antialiasSubpixelDepth = 16; this.antialias = enableAntialiasing; @@ -123,55 +119,11 @@ namespace SixLabors.ImageSharp /// /// Gets or sets a value indicating the color blending mode to apply to the drawing operation /// - public PixelColorBlendingMode ColorBlendingMode - { - get => this.colorBlendingMode; - set => this.colorBlendingMode = value; - } + public PixelColorBlendingMode ColorBlendingMode { get; set; } /// /// Gets or sets a value indicating the alpha composition mode to apply to the drawing operation /// - public PixelAlphaCompositionMode AlphaCompositionMode - { - get => this.alphaCompositionMode; - set => this.alphaCompositionMode = value; - } - - /// - /// Evaluates if a given SOURCE color can completely replace a BACKDROP color given the current blending and composition settings. - /// - /// the color - /// true if the color can be considered opaque - /// - /// Blending and composition is an expensive operation, in some cases, like - /// filling with a solid color, the blending can be avoided by a plain color replacement. - /// This method can be useful for such processors to select the fast path. - /// - internal bool IsOpaqueColorWithoutBlending(Color color) - { - if (this.ColorBlendingMode != PixelColorBlendingMode.Normal) - { - return false; - } - - if (this.AlphaCompositionMode != PixelAlphaCompositionMode.SrcOver && - this.AlphaCompositionMode != PixelAlphaCompositionMode.Src) - { - return false; - } - - if (this.BlendPercentage != 1f) - { - return false; - } - - if (color.ToVector4().W != 1f) - { - return false; - } - - return true; - } + public PixelAlphaCompositionMode AlphaCompositionMode { get; set; } } -} \ No newline at end of file +} From d7b0a322ab6b81127a3dcbd1e980a9ce94ffc9fb Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Wed, 6 Nov 2019 20:48:05 +1100 Subject: [PATCH 43/75] Fix ImageFrame.Configuration --- .../Processing/BrushApplicator.cs | 53 ++++++++++------ .../Processing/EllipticGradientBrush.cs | 41 ++++++------ .../Processing/GradientBrush.cs | 29 ++++----- src/ImageSharp.Drawing/Processing/IBrush.cs | 10 +-- .../Processing/ImageBrush.cs | 47 +++++++------- .../Processing/LinearGradientBrush.cs | 27 ++++---- .../Processing/PathGradientBrush.cs | 18 +++--- .../Processing/PatternBrush.cs | 36 +++++------ .../Drawing/FillProcessor{TPixel}.cs | 5 +- .../Drawing/FillRegionProcessor{TPixel}.cs | 2 +- .../Text/DrawTextProcessor{TPixel}.cs | 2 +- .../Processing/RadialGradientBrush.cs | 30 ++++----- .../Processing/RecolorBrush.cs | 39 ++++-------- .../Processing/SolidBrush.cs | 62 ++++++++++--------- src/ImageSharp.Drawing/Utils/QuickSort.cs | 2 +- 15 files changed, 202 insertions(+), 201 deletions(-) diff --git a/src/ImageSharp.Drawing/Processing/BrushApplicator.cs b/src/ImageSharp.Drawing/Processing/BrushApplicator.cs index ab601bb8b0..2b02c14fe1 100644 --- a/src/ImageSharp.Drawing/Processing/BrushApplicator.cs +++ b/src/ImageSharp.Drawing/Processing/BrushApplicator.cs @@ -3,66 +3,83 @@ using System; using System.Buffers; - using SixLabors.ImageSharp.Advanced; -using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; using SixLabors.Memory; namespace SixLabors.ImageSharp.Processing { /// - /// primitive that converts a point in to a color for discovering the fill color based on an implementation + /// A primitive that converts a point into a color for discovering the fill color based on an implementation. /// /// The pixel format. - /// - public abstract class BrushApplicator : IDisposable // disposable will be required if/when there is an ImageBrush + /// + public abstract class BrushApplicator : IDisposable where TPixel : struct, IPixel { /// /// Initializes a new instance of the class. /// + /// The configuration instance to use when performing operations. /// The target. /// The options. - internal BrushApplicator(ImageFrame target, GraphicsOptions options) + internal BrushApplicator(Configuration configuration, ImageFrame target, GraphicsOptions options) { + this.Configuration = configuration; this.Target = target; this.Options = options; this.Blender = PixelOperations.Instance.GetPixelBlender(options); } /// - /// Gets the blender + /// Gets the configuration instance to use when performing operations. + /// + protected Configuration Configuration { get; } + + /// + /// Gets the pixel blender. /// internal PixelBlender Blender { get; } /// - /// Gets the destination + /// Gets the target image. /// protected ImageFrame Target { get; } /// - /// Gets the blend percentage + /// Gets thegraphics options /// protected GraphicsOptions Options { get; } /// - /// Gets the color for a single pixel. + /// Gets the overlay pixel at the specified position. /// - /// The x coordinate. - /// The y coordinate. - /// The a that should be applied to the pixel. + /// The x-coordinate. + /// The y-coordinate. + /// The at the specified position. internal abstract TPixel this[int x, int y] { get; } /// - public abstract void Dispose(); + public void Dispose() + { + this.Dispose(true); + GC.SuppressFinalize(this); + } + + /// + /// Disposes the object and frees resources for the Garbage Collector. + /// + /// Whether to dispose managed and unmanaged objects. + protected virtual void Dispose(bool disposing) + { + } /// /// Applies the opacity weighting for each pixel in a scanline to the target based on the pattern contained in the brush. /// - /// The a collection of opacity values between 0 and 1 to be merged with the brushed color value before being applied to the target. - /// The x position in the target pixel space that the start of the scanline data corresponds to. - /// The y position in the target pixel space that whole scanline corresponds to. + /// A collection of opacity values between 0 and 1 to be merged with the brushed color value before being applied to the target. + /// The x-position in the target pixel space that the start of the scanline data corresponds to. + /// The y-position in the target pixel space that whole scanline corresponds to. /// scanlineBuffer will be > scanlineWidth but provide and offset in case we want to share a larger buffer across runs. internal virtual void Apply(Span scanline, int x, int y) { @@ -89,7 +106,7 @@ namespace SixLabors.ImageSharp.Processing } Span destinationRow = this.Target.GetPixelRowSpan(y).Slice(x, scanline.Length); - this.Blender.Blend(this.Target.Configuration, destinationRow, destinationRow, overlaySpan, amountSpan); + this.Blender.Blend(this.Configuration, destinationRow, destinationRow, overlaySpan, amountSpan); } } } diff --git a/src/ImageSharp.Drawing/Processing/EllipticGradientBrush.cs b/src/ImageSharp.Drawing/Processing/EllipticGradientBrush.cs index 91da332a16..7810c3c6d5 100644 --- a/src/ImageSharp.Drawing/Processing/EllipticGradientBrush.cs +++ b/src/ImageSharp.Drawing/Processing/EllipticGradientBrush.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; @@ -47,17 +47,19 @@ namespace SixLabors.ImageSharp.Processing /// public override BrushApplicator CreateApplicator( + Configuration configuration, ImageFrame source, RectangleF region, GraphicsOptions options) => new RadialGradientBrushApplicator( + configuration, source, - options, this.center, this.referenceAxisEnd, this.axisRatio, this.ColorStops, - this.RepetitionMode); + this.RepetitionMode, + options); /// private sealed class RadialGradientBrushApplicator : GradientBrushApplicator @@ -86,24 +88,26 @@ namespace SixLabors.ImageSharp.Processing /// /// Initializes a new instance of the class. /// - /// The target image - /// The options - /// Center of the ellipse + /// The configuration instance to use when performing operations. + /// The target image. + /// Center of the ellipse. /// Point on one angular points of the ellipse. /// /// Ratio of the axis length's. Used to determine the length of the second axis, /// the first is defined by and . - /// Definition of colors + /// Definition of colors. /// Defines how the gradient colors are repeated. + /// The graphics options. public RadialGradientBrushApplicator( + Configuration configuration, ImageFrame target, - GraphicsOptions options, PointF center, PointF referenceAxisEnd, float axisRatio, ColorStop[] colorStops, - GradientRepetitionMode repetitionMode) - : base(target, options, colorStops, repetitionMode) + GradientRepetitionMode repetitionMode, + GraphicsOptions options) + : base(configuration, target, colorStops, repetitionMode, options) { this.center = center; this.referenceAxisEnd = referenceAxisEnd; @@ -122,11 +126,6 @@ namespace SixLabors.ImageSharp.Processing this.cosRotation = (float)Math.Cos(this.rotation); } - /// - public override void Dispose() - { - } - /// protected override float PositionOnGradient(float xt, float yt) { @@ -139,16 +138,13 @@ namespace SixLabors.ImageSharp.Processing float xSquared = x * x; float ySquared = y * y; - var inBoundaryChecker = (xSquared / this.referenceRadiusSquared) - + (ySquared / this.secondRadiusSquared); - - return inBoundaryChecker; + return (xSquared / this.referenceRadiusSquared) + (ySquared / this.secondRadiusSquared); } private float AngleBetween(PointF junction, PointF a, PointF b) { - var vA = a - junction; - var vB = b - junction; + PointF vA = a - junction; + PointF vB = b - junction; return MathF.Atan2(vB.Y, vB.X) - MathF.Atan2(vA.Y, vA.X); } @@ -156,6 +152,7 @@ namespace SixLabors.ImageSharp.Processing PointF p1, PointF p2) { + // TODO: Can we not just use Vector2 distance here? float dX = p1.X - p2.X; float dXsquared = dX * dX; @@ -165,4 +162,4 @@ namespace SixLabors.ImageSharp.Processing } } } -} \ No newline at end of file +} diff --git a/src/ImageSharp.Drawing/Processing/GradientBrush.cs b/src/ImageSharp.Drawing/Processing/GradientBrush.cs index bc87a26f3b..044f4e2ee8 100644 --- a/src/ImageSharp.Drawing/Processing/GradientBrush.cs +++ b/src/ImageSharp.Drawing/Processing/GradientBrush.cs @@ -2,10 +2,8 @@ // Licensed under the Apache License, Version 2.0. using System; -using System.Numerics; using SixLabors.ImageSharp.PixelFormats; -using SixLabors.ImageSharp.PixelFormats.PixelBlenders; using SixLabors.Primitives; namespace SixLabors.ImageSharp.Processing @@ -38,6 +36,7 @@ namespace SixLabors.ImageSharp.Processing /// public abstract BrushApplicator CreateApplicator( + Configuration configuration, ImageFrame source, RectangleF region, GraphicsOptions options) @@ -58,27 +57,24 @@ namespace SixLabors.ImageSharp.Processing /// /// Initializes a new instance of the class. /// - /// The target. - /// The options. + /// The configuration instance to use when performing operations. + /// The target image. /// An array of color stops sorted by their position. /// Defines if and how the gradient should be repeated. + /// The graphics options. protected GradientBrushApplicator( + Configuration configuration, ImageFrame target, - GraphicsOptions options, ColorStop[] colorStops, - GradientRepetitionMode repetitionMode) - : base(target, options) + GradientRepetitionMode repetitionMode, + GraphicsOptions options) + : base(configuration, target, options) { this.colorStops = colorStops; // TODO: requires colorStops to be sorted by position - should that be checked? this.repetitionMode = repetitionMode; } - /// - /// Base implementation of the indexer for gradients - /// (follows the facade pattern, using abstract methods) - /// - /// X coordinate of the Pixel. - /// Y coordinate of the Pixel. + /// internal override TPixel this[int x, int y] { get @@ -123,7 +119,8 @@ namespace SixLabors.ImageSharp.Processing { float onLocalGradient = (positionOnCompleteGradient - from.Ratio) / (to.Ratio - from.Ratio); - // TODO: this should be changeble for different gradienting functions + // TODO: this should be changeble for different gradienting functions. + // TODO: Why not use Blender property? return PixelOperations .Instance.GetPixelBlender(PixelColorBlendingMode.Normal, PixelAlphaCompositionMode.SrcOver) .Blend(from.Color.ToPixel(), to.Color.ToPixel(), onLocalGradient); @@ -135,8 +132,8 @@ namespace SixLabors.ImageSharp.Processing /// calculates the position on the gradient for a given point. /// This method is abstract as it's content depends on the shape of the gradient. /// - /// The x coordinate of the point - /// The y coordinate of the point + /// The x-coordinate of the point. + /// The y-coordinate of the point. /// /// The position the given point has on the gradient. /// The position is not bound to the [0..1] interval. diff --git a/src/ImageSharp.Drawing/Processing/IBrush.cs b/src/ImageSharp.Drawing/Processing/IBrush.cs index 0cd2e20fda..f2fdc32f05 100644 --- a/src/ImageSharp.Drawing/Processing/IBrush.cs +++ b/src/ImageSharp.Drawing/Processing/IBrush.cs @@ -1,4 +1,4 @@ -// Copyright (c) Six Labors and contributors. +// Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. using SixLabors.ImageSharp.PixelFormats; @@ -19,20 +19,22 @@ namespace SixLabors.ImageSharp.Processing /// Creates the applicator for this brush. /// /// The pixel type. + /// The configuration instance to use when performing operations. /// The source image. /// The region the brush will be applied to. /// The graphic options /// - /// The brush applicator for this brush + /// The for this brush. /// /// /// The when being applied to things like shapes would usually be the - /// bounding box of the shape not necessarily the bounds of the whole image + /// bounding box of the shape not necessarily the bounds of the whole image. /// BrushApplicator CreateApplicator( + Configuration configuration, ImageFrame source, RectangleF region, GraphicsOptions options) where TPixel : struct, IPixel; } -} \ No newline at end of file +} diff --git a/src/ImageSharp.Drawing/Processing/ImageBrush.cs b/src/ImageSharp.Drawing/Processing/ImageBrush.cs index ebcc2c0677..f23fb1f18d 100644 --- a/src/ImageSharp.Drawing/Processing/ImageBrush.cs +++ b/src/ImageSharp.Drawing/Processing/ImageBrush.cs @@ -5,7 +5,6 @@ using System; using System.Buffers; using SixLabors.ImageSharp.Advanced; -using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; using SixLabors.Primitives; @@ -32,6 +31,7 @@ namespace SixLabors.ImageSharp.Processing /// public BrushApplicator CreateApplicator( + Configuration configuration, ImageFrame source, RectangleF region, GraphicsOptions options) @@ -39,12 +39,12 @@ namespace SixLabors.ImageSharp.Processing { if (this.image is Image specificImage) { - return new ImageBrushApplicator(source, specificImage, region, options, false); + return new ImageBrushApplicator(configuration, source, specificImage, region, false, options); } specificImage = this.image.CloneAs(); - return new ImageBrushApplicator(source, specificImage, region, options, true); + return new ImageBrushApplicator(configuration, source, specificImage, region, true, options); } /// @@ -79,21 +79,25 @@ namespace SixLabors.ImageSharp.Processing /// private readonly int offsetX; + private bool isDisposed; + /// /// Initializes a new instance of the class. /// + /// The configuration instance to use when performing operations. /// The target image. /// The image. /// The region. - /// The options /// Whether to dispose the image on disposal of the applicator. + /// The graphics options. public ImageBrushApplicator( + Configuration configuration, ImageFrame target, Image image, RectangleF region, - GraphicsOptions options, - bool shouldDisposeImage) - : base(target, options) + bool shouldDisposeImage, + GraphicsOptions options) + : base(configuration, target, options) { this.sourceImage = image; this.sourceFrame = image.Frames.RootFrame; @@ -104,14 +108,7 @@ namespace SixLabors.ImageSharp.Processing this.offsetX = (int)MathF.Max(MathF.Floor(region.Left), 0); } - /// - /// Gets the color for a single pixel. - /// - /// The x. - /// The y. - /// - /// The color - /// + /// internal override TPixel this[int x, int y] { get @@ -123,14 +120,21 @@ namespace SixLabors.ImageSharp.Processing } /// - public override void Dispose() + protected override void Dispose(bool disposing) { - if (this.shouldDisposeImage) + if (this.isDisposed) + { + return; + } + + if (disposing && this.shouldDisposeImage) { this.sourceImage?.Dispose(); - this.sourceImage = null; - this.sourceFrame = null; } + + this.sourceImage = null; + this.sourceFrame = null; + this.isDisposed = true; } /// @@ -152,13 +156,12 @@ namespace SixLabors.ImageSharp.Processing amountSpan[i] = scanline[i] * this.Options.BlendPercentage; int sourceX = (i + offsetX) % this.xLength; - TPixel pixel = sourceRow[sourceX]; - overlaySpan[i] = pixel; + overlaySpan[i] = sourceRow[sourceX]; } Span destinationRow = this.Target.GetPixelRowSpan(y).Slice(x, scanline.Length); this.Blender.Blend( - this.sourceFrame.Configuration, + this.Configuration, destinationRow, destinationRow, overlaySpan, diff --git a/src/ImageSharp.Drawing/Processing/LinearGradientBrush.cs b/src/ImageSharp.Drawing/Processing/LinearGradientBrush.cs index bb99eeb26a..bf6a6356ac 100644 --- a/src/ImageSharp.Drawing/Processing/LinearGradientBrush.cs +++ b/src/ImageSharp.Drawing/Processing/LinearGradientBrush.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; @@ -39,10 +39,12 @@ namespace SixLabors.ImageSharp.Processing /// public override BrushApplicator CreateApplicator( + Configuration configuration, ImageFrame source, RectangleF region, GraphicsOptions options) => new LinearGradientBrushApplicator( + configuration, source, this.p1, this.p2, @@ -93,20 +95,22 @@ namespace SixLabors.ImageSharp.Processing /// /// Initializes a new instance of the class. /// - /// The source - /// start point of the gradient - /// end point of the gradient - /// tuple list of colors and their respective position between 0 and 1 on the line + /// The configuration instance to use when performing operations. + /// The source image. + /// start point of the gradient. + /// end point of the gradient. + /// tuple list of colors and their respective position between 0 and 1 on the line. /// defines how the gradient colors are repeated. - /// the graphics options + /// the graphics options. public LinearGradientBrushApplicator( + Configuration configuration, ImageFrame source, PointF start, PointF end, ColorStop[] colorStops, GradientRepetitionMode repetitionMode, GraphicsOptions options) - : base(source, options, colorStops, repetitionMode) + : base(configuration, source, colorStops, repetitionMode, options) { this.start = start; this.end = end; @@ -148,14 +152,9 @@ namespace SixLabors.ImageSharp.Processing float distance = MathF.Sqrt(MathF.Pow(x4 - this.start.X, 2) + MathF.Pow(y4 - this.start.Y, 2)); // get and return ratio - float ratio = distance / this.length; - return ratio; + return distance / this.length; } } - - public override void Dispose() - { - } } } -} \ No newline at end of file +} diff --git a/src/ImageSharp.Drawing/Processing/PathGradientBrush.cs b/src/ImageSharp.Drawing/Processing/PathGradientBrush.cs index d2ad23a0d9..be1af50118 100644 --- a/src/ImageSharp.Drawing/Processing/PathGradientBrush.cs +++ b/src/ImageSharp.Drawing/Processing/PathGradientBrush.cs @@ -83,12 +83,13 @@ namespace SixLabors.ImageSharp.Processing /// public BrushApplicator CreateApplicator( + Configuration configuration, ImageFrame source, RectangleF region, GraphicsOptions options) where TPixel : struct, IPixel { - return new PathGradientBrushApplicator(source, this.edges, this.centerColor, options); + return new PathGradientBrushApplicator(configuration, source, this.edges, this.centerColor, options); } private static Color CalculateCenterColor(Color[] colors) @@ -199,16 +200,18 @@ namespace SixLabors.ImageSharp.Processing /// /// Initializes a new instance of the class. /// + /// The configuration instance to use when performing operations. /// The source image. /// Edges of the polygon. /// Color at the center of the gradient area to which the other colors converge. - /// The options. + /// The graphics options. public PathGradientBrushApplicator( + Configuration configuration, ImageFrame source, IList edges, Color centerColor, GraphicsOptions options) - : base(source, options) + : base(configuration, source, options) { this.edges = edges; @@ -232,7 +235,7 @@ namespace SixLabors.ImageSharp.Processing return new Color(this.centerColor).ToPixel(); } - Vector2 direction = Vector2.Normalize(point - this.center); + var direction = Vector2.Normalize(point - this.center); PointF end = point + (PointF)(direction * this.maxDistance); @@ -250,7 +253,7 @@ namespace SixLabors.ImageSharp.Processing float length = DistanceBetween(intersection, this.center); float ratio = length > 0 ? DistanceBetween(intersection, point) / length : 0; - Vector4 color = Vector4.Lerp(edgeColor, this.centerColor, ratio); + var color = Vector4.Lerp(edgeColor, this.centerColor, ratio); return new Color(color).ToPixel(); } @@ -277,11 +280,6 @@ namespace SixLabors.ImageSharp.Processing return closest; } - - /// - public override void Dispose() - { - } } } } diff --git a/src/ImageSharp.Drawing/Processing/PatternBrush.cs b/src/ImageSharp.Drawing/Processing/PatternBrush.cs index 1b7db970a3..37bb9b0fc0 100644 --- a/src/ImageSharp.Drawing/Processing/PatternBrush.cs +++ b/src/ImageSharp.Drawing/Processing/PatternBrush.cs @@ -92,13 +92,15 @@ namespace SixLabors.ImageSharp.Processing /// public BrushApplicator CreateApplicator( + Configuration configuration, ImageFrame source, RectangleF region, GraphicsOptions options) where TPixel : struct, IPixel => new PatternBrushApplicator( + configuration, source, - this.pattern.ToPixelMatrix(source.Configuration), + this.pattern.ToPixelMatrix(configuration), options); /// @@ -115,41 +117,33 @@ namespace SixLabors.ImageSharp.Processing /// /// Initializes a new instance of the class. /// + /// The configuration instance to use when performing operations. /// The source image. /// The pattern. - /// The options - public PatternBrushApplicator(ImageFrame source, in DenseMatrix pattern, GraphicsOptions options) - : base(source, options) + /// The graphics options. + public PatternBrushApplicator( + Configuration configuration, + ImageFrame source, + in DenseMatrix pattern, + GraphicsOptions options) + : base(configuration, source, options) { this.pattern = pattern; } - /// - /// Gets the color for a single pixel. - /// # - /// The x. - /// The y. - /// - /// The Color. - /// + /// internal override TPixel this[int x, int y] { get { - x = x % this.pattern.Columns; - y = y % this.pattern.Rows; + x %= this.pattern.Columns; + y %= this.pattern.Rows; // 2d array index at row/column return this.pattern[y, x]; } } - /// - public override void Dispose() - { - // noop - } - /// internal override void Apply(Span scanline, int x, int y) { @@ -172,7 +166,7 @@ namespace SixLabors.ImageSharp.Processing Span destinationRow = this.Target.GetPixelRowSpan(y).Slice(x, scanline.Length); this.Blender.Blend( - this.Target.Configuration, + this.Configuration, destinationRow, destinationRow, overlaySpan, diff --git a/src/ImageSharp.Drawing/Processing/Processors/Drawing/FillProcessor{TPixel}.cs b/src/ImageSharp.Drawing/Processing/Processors/Drawing/FillProcessor{TPixel}.cs index 1dd4be1354..0309c561aa 100644 --- a/src/ImageSharp.Drawing/Processing/Processors/Drawing/FillProcessor{TPixel}.cs +++ b/src/ImageSharp.Drawing/Processing/Processors/Drawing/FillProcessor{TPixel}.cs @@ -45,7 +45,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Drawing int width = maxX - minX; - Rectangle workingRect = Rectangle.FromLTRB(minX, minY, maxX, maxY); + var workingRect = Rectangle.FromLTRB(minX, minY, maxX, maxY); IBrush brush = this.definition.Brush; GraphicsOptions options = this.definition.Options; @@ -56,7 +56,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Drawing ParallelExecutionSettings parallelSettings = ParallelExecutionSettings.FromConfiguration(configuration) .MultiplyMinimumPixelsPerTask(4); - var colorPixel = solidBrush.Color.ToPixel(); + TPixel colorPixel = solidBrush.Color.ToPixel(); ParallelHelper.IterateRows( workingRect, @@ -84,6 +84,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Drawing using (IMemoryOwner amount = source.MemoryAllocator.Allocate(width)) using (BrushApplicator applicator = brush.CreateApplicator( + configuration, source, sourceRectangle, options)) diff --git a/src/ImageSharp.Drawing/Processing/Processors/Drawing/FillRegionProcessor{TPixel}.cs b/src/ImageSharp.Drawing/Processing/Processors/Drawing/FillRegionProcessor{TPixel}.cs index 69f97ce75f..8c665826fe 100644 --- a/src/ImageSharp.Drawing/Processing/Processors/Drawing/FillRegionProcessor{TPixel}.cs +++ b/src/ImageSharp.Drawing/Processing/Processors/Drawing/FillRegionProcessor{TPixel}.cs @@ -71,7 +71,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Drawing } } - using (BrushApplicator applicator = brush.CreateApplicator(source, rect, options)) + using (BrushApplicator applicator = brush.CreateApplicator(configuration, source, rect, options)) { int scanlineWidth = maxX - minX; using (IMemoryOwner bBuffer = source.MemoryAllocator.Allocate(maxIntersections)) diff --git a/src/ImageSharp.Drawing/Processing/Processors/Text/DrawTextProcessor{TPixel}.cs b/src/ImageSharp.Drawing/Processing/Processors/Text/DrawTextProcessor{TPixel}.cs index c2e38f67be..8ae6ee4916 100644 --- a/src/ImageSharp.Drawing/Processing/Processors/Text/DrawTextProcessor{TPixel}.cs +++ b/src/ImageSharp.Drawing/Processing/Processors/Text/DrawTextProcessor{TPixel}.cs @@ -83,7 +83,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Text { if (operations?.Count > 0) { - using (BrushApplicator app = brush.CreateApplicator(source, this.SourceRectangle, this.textRenderer.Options)) + using (BrushApplicator app = brush.CreateApplicator(this.Configuration, source, this.SourceRectangle, this.textRenderer.Options)) { foreach (DrawingOperation operation in operations) { diff --git a/src/ImageSharp.Drawing/Processing/RadialGradientBrush.cs b/src/ImageSharp.Drawing/Processing/RadialGradientBrush.cs index f4d2dd81f4..7f1fa818eb 100644 --- a/src/ImageSharp.Drawing/Processing/RadialGradientBrush.cs +++ b/src/ImageSharp.Drawing/Processing/RadialGradientBrush.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,7 +9,7 @@ using SixLabors.Primitives; namespace SixLabors.ImageSharp.Processing { /// - /// A Circular Gradient Brush, defined by center point and radius. + /// A radial gradient brush, defined by center point and radius. /// public sealed class RadialGradientBrush : GradientBrush { @@ -35,16 +35,18 @@ namespace SixLabors.ImageSharp.Processing /// public override BrushApplicator CreateApplicator( + Configuration configuration, ImageFrame source, RectangleF region, GraphicsOptions options) => new RadialGradientBrushApplicator( + configuration, source, - options, this.center, this.radius, this.ColorStops, - this.RepetitionMode); + this.RepetitionMode, + options); /// private sealed class RadialGradientBrushApplicator : GradientBrushApplicator @@ -57,30 +59,27 @@ namespace SixLabors.ImageSharp.Processing /// /// Initializes a new instance of the class. /// - /// The target image - /// The options. + /// The configuration instance to use when performing operations. + /// The target image. /// Center point of the gradient. /// Radius of the gradient. /// Definition of colors. /// How the colors are repeated beyond the first gradient. + /// The graphics options. public RadialGradientBrushApplicator( + Configuration configuration, ImageFrame target, - GraphicsOptions options, PointF center, float radius, ColorStop[] colorStops, - GradientRepetitionMode repetitionMode) - : base(target, options, colorStops, repetitionMode) + GradientRepetitionMode repetitionMode, + GraphicsOptions options) + : base(configuration, target, colorStops, repetitionMode, options) { this.center = center; this.radius = radius; } - /// - public override void Dispose() - { - } - /// /// As this is a circular gradient, the position on the gradient is based on /// the distance of the point to the center. @@ -90,6 +89,7 @@ namespace SixLabors.ImageSharp.Processing /// the position on the color gradient. protected override float PositionOnGradient(float x, float y) { + // TODO: Can this not use Vector2 distance? float distance = MathF.Sqrt(MathF.Pow(this.center.X - x, 2) + MathF.Pow(this.center.Y - y, 2)); return distance / this.radius; } @@ -101,4 +101,4 @@ namespace SixLabors.ImageSharp.Processing } } } -} \ No newline at end of file +} diff --git a/src/ImageSharp.Drawing/Processing/RecolorBrush.cs b/src/ImageSharp.Drawing/Processing/RecolorBrush.cs index d93110972c..1ad613bf82 100644 --- a/src/ImageSharp.Drawing/Processing/RecolorBrush.cs +++ b/src/ImageSharp.Drawing/Processing/RecolorBrush.cs @@ -5,7 +5,6 @@ using System; using System.Buffers; using System.Numerics; using SixLabors.ImageSharp.Advanced; -using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; using SixLabors.Memory; using SixLabors.Primitives; @@ -38,9 +37,6 @@ namespace SixLabors.ImageSharp.Processing /// /// Gets the source color. /// - /// - /// The color of the source. - /// public Color SourceColor { get; } /// @@ -50,12 +46,14 @@ namespace SixLabors.ImageSharp.Processing /// public BrushApplicator CreateApplicator( + Configuration configuration, ImageFrame source, RectangleF region, GraphicsOptions options) where TPixel : struct, IPixel { return new RecolorBrushApplicator( + configuration, source, this.SourceColor.ToPixel(), this.TargetColor.ToPixel(), @@ -74,11 +72,6 @@ namespace SixLabors.ImageSharp.Processing /// private readonly Vector4 sourceColor; - /// - /// The target color. - /// - private readonly Vector4 targetColor; - /// /// The threshold. /// @@ -89,16 +82,22 @@ namespace SixLabors.ImageSharp.Processing /// /// Initializes a new instance of the class. /// + /// The configuration instance to use when performing operations. /// The source image. /// Color of the source. /// Color of the target. /// The threshold . /// The options - public RecolorBrushApplicator(ImageFrame source, TPixel sourceColor, TPixel targetColor, float threshold, GraphicsOptions options) - : base(source, options) + public RecolorBrushApplicator( + Configuration configuration, + ImageFrame source, + TPixel sourceColor, + TPixel targetColor, + float threshold, + GraphicsOptions options) + : base(configuration, source, options) { this.sourceColor = sourceColor.ToVector4(); - this.targetColor = targetColor.ToVector4(); this.targetColorPixel = targetColor; // Lets hack a min max extremes for a color space by letting the IPackedPixel clamp our values to something in the correct spaces :) @@ -109,14 +108,7 @@ namespace SixLabors.ImageSharp.Processing this.threshold = Vector4.DistanceSquared(maxColor.ToVector4(), minColor.ToVector4()) * threshold; } - /// - /// Gets the color for a single pixel. - /// - /// The x. - /// The y. - /// - /// The color - /// + /// internal override TPixel this[int x, int y] { get @@ -138,11 +130,6 @@ namespace SixLabors.ImageSharp.Processing } } - /// - public override void Dispose() - { - } - /// internal override void Apply(Span scanline, int x, int y) { @@ -167,7 +154,7 @@ namespace SixLabors.ImageSharp.Processing Span destinationRow = this.Target.GetPixelRowSpan(y).Slice(x, scanline.Length); this.Blender.Blend( - this.Target.Configuration, + this.Configuration, destinationRow, destinationRow, overlaySpan, diff --git a/src/ImageSharp.Drawing/Processing/SolidBrush.cs b/src/ImageSharp.Drawing/Processing/SolidBrush.cs index 71738de52b..a2be775daa 100644 --- a/src/ImageSharp.Drawing/Processing/SolidBrush.cs +++ b/src/ImageSharp.Drawing/Processing/SolidBrush.cs @@ -5,7 +5,6 @@ using System; using System.Buffers; using SixLabors.ImageSharp.Advanced; -using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; using SixLabors.Memory; using SixLabors.Primitives; @@ -17,33 +16,29 @@ namespace SixLabors.ImageSharp.Processing /// public class SolidBrush : IBrush { - /// - /// The color to paint. - /// - private readonly Color color; - /// /// Initializes a new instance of the class. /// /// The color. public SolidBrush(Color color) { - this.color = color; + this.Color = color; } /// /// Gets the color. /// - /// - /// The color. - /// - public Color Color => this.color; + public Color Color { get; } /// - public BrushApplicator CreateApplicator(ImageFrame source, RectangleF region, GraphicsOptions options) + public BrushApplicator CreateApplicator( + Configuration configuration, + ImageFrame source, + RectangleF region, + GraphicsOptions options) where TPixel : struct, IPixel { - return new SolidBrushApplicator(source, this.color.ToPixel(), options); + return new SolidBrushApplicator(configuration, source, this.Color.ToPixel(), options); } /// @@ -52,14 +47,21 @@ namespace SixLabors.ImageSharp.Processing private class SolidBrushApplicator : BrushApplicator where TPixel : struct, IPixel { + private bool isDisposed; + /// /// Initializes a new instance of the class. /// + /// The configuration instance to use when performing operations. /// The source image. /// The color. - /// The options - public SolidBrushApplicator(ImageFrame source, TPixel color, GraphicsOptions options) - : base(source, options) + /// The graphics options. + public SolidBrushApplicator( + Configuration configuration, + ImageFrame source, + TPixel color, + GraphicsOptions options) + : base(configuration, source, options) { this.Colors = source.MemoryAllocator.Allocate(source.Width); this.Colors.Memory.Span.Fill(color); @@ -68,22 +70,26 @@ namespace SixLabors.ImageSharp.Processing /// /// Gets the colors. /// - protected IMemoryOwner Colors { get; } + protected IMemoryOwner Colors { get; private set; } - /// - /// Gets the color for a single pixel. - /// - /// The x. - /// The y. - /// - /// The color - /// + /// internal override TPixel this[int x, int y] => this.Colors.Memory.Span[x]; /// - public override void Dispose() + protected override void Dispose(bool disposing) { - this.Colors.Dispose(); + if (this.isDisposed) + { + return; + } + + if (disposing) + { + this.Colors.Dispose(); + } + + this.Colors = null; + this.isDisposed = true; } /// @@ -102,7 +108,7 @@ namespace SixLabors.ImageSharp.Processing } MemoryAllocator memoryAllocator = this.Target.MemoryAllocator; - Configuration configuration = this.Target.Configuration; + Configuration configuration = this.Configuration; if (this.Options.BlendPercentage == 1f) { diff --git a/src/ImageSharp.Drawing/Utils/QuickSort.cs b/src/ImageSharp.Drawing/Utils/QuickSort.cs index ca1da5505a..14e3146a0b 100644 --- a/src/ImageSharp.Drawing/Utils/QuickSort.cs +++ b/src/ImageSharp.Drawing/Utils/QuickSort.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; From c8a2c079e229f725acf6fc85f161d47b3756757f Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Wed, 6 Nov 2019 21:35:57 +1100 Subject: [PATCH 44/75] Fix float clamp accessibility. --- .../Processing/PatternBrush.cs | 2 +- .../Processing/TextGraphicsOptions.cs | 12 ++++---- src/ImageSharp.Drawing/Utils/NumberUtils.cs | 29 +++++++++++++++++++ 3 files changed, 36 insertions(+), 7 deletions(-) create mode 100644 src/ImageSharp.Drawing/Utils/NumberUtils.cs diff --git a/src/ImageSharp.Drawing/Processing/PatternBrush.cs b/src/ImageSharp.Drawing/Processing/PatternBrush.cs index 37bb9b0fc0..9024036d02 100644 --- a/src/ImageSharp.Drawing/Processing/PatternBrush.cs +++ b/src/ImageSharp.Drawing/Processing/PatternBrush.cs @@ -158,7 +158,7 @@ namespace SixLabors.ImageSharp.Processing for (int i = 0; i < scanline.Length; i++) { - amountSpan[i] = (scanline[i] * this.Options.BlendPercentage).Clamp(0, 1); + amountSpan[i] = NumberUtils.ClampFloat(scanline[i] * this.Options.BlendPercentage, 0, 1F); int patternX = (x + i) % this.pattern.Columns; overlaySpan[i] = this.pattern[patternY, patternX]; diff --git a/src/ImageSharp.Drawing/Processing/TextGraphicsOptions.cs b/src/ImageSharp.Drawing/Processing/TextGraphicsOptions.cs index 6c140be72e..13c7f4bdf9 100644 --- a/src/ImageSharp.Drawing/Processing/TextGraphicsOptions.cs +++ b/src/ImageSharp.Drawing/Processing/TextGraphicsOptions.cs @@ -1,4 +1,4 @@ -// Copyright (c) Six Labors and contributors. +// Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. using SixLabors.Fonts; @@ -11,13 +11,13 @@ namespace SixLabors.ImageSharp.Processing /// public struct TextGraphicsOptions { - private const int DefaultTextDpi = 72; - /// /// Represents the default . /// public static readonly TextGraphicsOptions Default = new TextGraphicsOptions(true); + private const int DefaultTextDpi = 72; + private float? blendPercentage; private int? antialiasSubpixelDepth; @@ -51,7 +51,7 @@ namespace SixLabors.ImageSharp.Processing this.antialiasSubpixelDepth = 16; this.ColorBlendingMode = PixelColorBlendingMode.Normal; this.AlphaCompositionMode = PixelAlphaCompositionMode.SrcOver; - this.blendPercentage = 1; + this.blendPercentage = 1F; this.antialias = enableAntialiasing; this.dpiX = DefaultTextDpi; this.dpiY = DefaultTextDpi; @@ -70,7 +70,7 @@ namespace SixLabors.ImageSharp.Processing /// /// Gets or sets a value indicating the blending percentage to apply to the drawing operation /// - public float BlendPercentage { get => (this.blendPercentage ?? 1).Clamp(0, 1); set => this.blendPercentage = value; } + public float BlendPercentage { get => this.blendPercentage ?? 1F; set => this.blendPercentage = NumberUtils.ClampFloat(value, 0, 1F); } // In the future we could expose a PixelBlender directly on here // or some forms of PixelBlender factory for each pixel type. Will need @@ -160,4 +160,4 @@ namespace SixLabors.ImageSharp.Processing }; } } -} \ No newline at end of file +} diff --git a/src/ImageSharp.Drawing/Utils/NumberUtils.cs b/src/ImageSharp.Drawing/Utils/NumberUtils.cs new file mode 100644 index 0000000000..d034c5d7ed --- /dev/null +++ b/src/ImageSharp.Drawing/Utils/NumberUtils.cs @@ -0,0 +1,29 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System.Runtime.CompilerServices; + +namespace SixLabors.ImageSharp +{ + /// + /// Utility methods for numeric primitives. + /// + internal static class NumberUtils + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static float ClampFloat(float value, float min, float max) + { + if (value >= max) + { + return max; + } + + if (value <= min) + { + return min; + } + + return value; + } + } +} From d91bda2946ada6b3a2fbdc3656270efaa0fd240e Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Fri, 8 Nov 2019 17:10:31 +1100 Subject: [PATCH 45/75] Fix #1044 --- .../Processing/GradientBrush.cs | 8 ++-- src/ImageSharp/ImageSharp.csproj | 4 +- .../Drawing/FillLinearGradientBrushTests.cs | 44 ++++++++++++++++++- tests/Images/External | 2 +- 4 files changed, 48 insertions(+), 10 deletions(-) diff --git a/src/ImageSharp.Drawing/Processing/GradientBrush.cs b/src/ImageSharp.Drawing/Processing/GradientBrush.cs index 044f4e2ee8..69659431d0 100644 --- a/src/ImageSharp.Drawing/Processing/GradientBrush.cs +++ b/src/ImageSharp.Drawing/Processing/GradientBrush.cs @@ -2,7 +2,7 @@ // Licensed under the Apache License, Version 2.0. using System; - +using System.Numerics; using SixLabors.ImageSharp.PixelFormats; using SixLabors.Primitives; @@ -120,10 +120,8 @@ namespace SixLabors.ImageSharp.Processing float onLocalGradient = (positionOnCompleteGradient - from.Ratio) / (to.Ratio - from.Ratio); // TODO: this should be changeble for different gradienting functions. - // TODO: Why not use Blender property? - return PixelOperations - .Instance.GetPixelBlender(PixelColorBlendingMode.Normal, PixelAlphaCompositionMode.SrcOver) - .Blend(from.Color.ToPixel(), to.Color.ToPixel(), onLocalGradient); + // TODO: Is the comment above still relevent? + return new Color(Vector4.Lerp((Vector4)from.Color, (Vector4)to.Color, onLocalGradient)).ToPixel(); } } } diff --git a/src/ImageSharp/ImageSharp.csproj b/src/ImageSharp/ImageSharp.csproj index d9e9a8d219..86b0848663 100644 --- a/src/ImageSharp/ImageSharp.csproj +++ b/src/ImageSharp/ImageSharp.csproj @@ -1,4 +1,4 @@ - + @@ -119,7 +119,7 @@ - + diff --git a/tests/ImageSharp.Tests/Drawing/FillLinearGradientBrushTests.cs b/tests/ImageSharp.Tests/Drawing/FillLinearGradientBrushTests.cs index 361e7e70d1..8aae342cba 100644 --- a/tests/ImageSharp.Tests/Drawing/FillLinearGradientBrushTests.cs +++ b/tests/ImageSharp.Tests/Drawing/FillLinearGradientBrushTests.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; @@ -16,6 +16,8 @@ namespace SixLabors.ImageSharp.Tests.Drawing { using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison; + using SixLabors.Primitives; + using SixLabors.Shapes; [GroupOutput("Drawing/GradientBrushes")] public class FillLinearGradientBrushTests @@ -392,5 +394,43 @@ namespace SixLabors.ImageSharp.Tests.Drawing false, false); } + + [Theory] + [WithBlankImages(200, 200, PixelTypes.Rgba32)] + public void GradientsWithTransparencyOnExistingBackground(TestImageProvider provider) + where TPixel : struct, IPixel + { + provider.VerifyOperation( + image => + { + image.Mutate(i => i.Fill(Color.Red)); + image.Mutate(ApplyGloss); + + }); + + void ApplyGloss(IImageProcessingContext ctx) + { + Size size = ctx.GetCurrentSize(); + IPathCollection glossPath = BuildGloss(size.Width, size.Height); + var graphicsOptions = new GraphicsOptions(true) + { + ColorBlendingMode = PixelColorBlendingMode.Normal, + AlphaCompositionMode = PixelAlphaCompositionMode.SrcAtop + }; + var linearGradientBrush = new LinearGradientBrush(new Point(0, 0), new Point(0, size.Height / 2), GradientRepetitionMode.Repeat, new ColorStop(0, Color.White.WithAlpha(0.5f)), new ColorStop(1, Color.White.WithAlpha(0.25f))); + ctx.Fill(graphicsOptions, linearGradientBrush, glossPath); + } + + IPathCollection BuildGloss(int imageWidth, int imageHeight) + { + var pathBuilder = new PathBuilder(); + pathBuilder.AddLine(new PointF(0, 0), new PointF(imageWidth, 0)); + pathBuilder.AddLine(new PointF(imageWidth, 0), new PointF(imageWidth, imageHeight * 0.4f)); + pathBuilder.AddBezier(new PointF(imageWidth, imageHeight * 0.4f), new PointF(imageWidth / 2, imageHeight * 0.6f), new PointF(0, imageHeight * 0.4f)); + pathBuilder.CloseFigure(); + return new PathCollection(pathBuilder.Build()); + } + } + } -} \ No newline at end of file +} diff --git a/tests/Images/External b/tests/Images/External index 563ec6f777..f0c4033667 160000 --- a/tests/Images/External +++ b/tests/Images/External @@ -1 +1 @@ -Subproject commit 563ec6f7774734ba39924174c8961705a1ea6fa2 +Subproject commit f0c4033667bd23ad9dde82ccb625c232d402ee05 From e59f2206f3f6b3e92cfe7db53b3018a34e5871d9 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Sat, 9 Nov 2019 21:26:48 +1100 Subject: [PATCH 46/75] Convert options into classes. --- .../Processing/GradientBrush.cs | 3 - .../Processing/TextGraphicsOptions.cs | 140 ++++++++++++------ src/ImageSharp/GraphicsOptions.cs | 120 ++++++++------- .../Drawing/Paths/DrawPathCollection.cs | 2 +- .../Drawing/Paths/FillPath.cs | 2 +- .../Drawing/Paths/FillPathCollection.cs | 2 +- .../Drawing/Paths/FillPolygon.cs | 2 +- .../Drawing/Paths/FillRectangle.cs | 4 +- .../PorterDuffCompositorTests.cs | 11 +- 9 files changed, 174 insertions(+), 112 deletions(-) diff --git a/src/ImageSharp.Drawing/Processing/GradientBrush.cs b/src/ImageSharp.Drawing/Processing/GradientBrush.cs index 69659431d0..f17adf6335 100644 --- a/src/ImageSharp.Drawing/Processing/GradientBrush.cs +++ b/src/ImageSharp.Drawing/Processing/GradientBrush.cs @@ -118,9 +118,6 @@ namespace SixLabors.ImageSharp.Processing else { float onLocalGradient = (positionOnCompleteGradient - from.Ratio) / (to.Ratio - from.Ratio); - - // TODO: this should be changeble for different gradienting functions. - // TODO: Is the comment above still relevent? return new Color(Vector4.Lerp((Vector4)from.Color, (Vector4)to.Color, onLocalGradient)).ToPixel(); } } diff --git a/src/ImageSharp.Drawing/Processing/TextGraphicsOptions.cs b/src/ImageSharp.Drawing/Processing/TextGraphicsOptions.cs index 13c7f4bdf9..23d7aebf58 100644 --- a/src/ImageSharp.Drawing/Processing/TextGraphicsOptions.cs +++ b/src/ImageSharp.Drawing/Processing/TextGraphicsOptions.cs @@ -1,6 +1,7 @@ // Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. +using System; using SixLabors.Fonts; using SixLabors.ImageSharp.PixelFormats; @@ -9,68 +10,87 @@ namespace SixLabors.ImageSharp.Processing /// /// Options for influencing the drawing functions. /// - public struct TextGraphicsOptions + public class TextGraphicsOptions { + private static readonly Lazy Lazy = new Lazy(); + private int antialiasSubpixelDepth; + private float blendPercentage; + private float tabWidth; + private float dpiX; + private float dpiY; + /// - /// Represents the default . + /// Initializes a new instance of the class. /// - public static readonly TextGraphicsOptions Default = new TextGraphicsOptions(true); - - private const int DefaultTextDpi = 72; - - private float? blendPercentage; - - private int? antialiasSubpixelDepth; - - private bool? antialias; - - private bool? applyKerning; - - private float? tabWidth; - - private float? dpiX; - - private float? dpiY; - - private HorizontalAlignment? horizontalAlignment; - - private VerticalAlignment? verticalAlignment; + public TextGraphicsOptions() + : this(true) + { + } /// - /// Initializes a new instance of the struct. + /// Initializes a new instance of the class. /// /// If set to true [enable antialiasing]. public TextGraphicsOptions(bool enableAntialiasing) { - this.applyKerning = true; - this.tabWidth = 4; + this.ApplyKerning = true; + this.TabWidth = 4F; this.WrapTextWidth = 0; - this.horizontalAlignment = HorizontalAlignment.Left; - this.verticalAlignment = VerticalAlignment.Top; + this.HorizontalAlignment = HorizontalAlignment.Left; + this.VerticalAlignment = VerticalAlignment.Top; this.antialiasSubpixelDepth = 16; this.ColorBlendingMode = PixelColorBlendingMode.Normal; this.AlphaCompositionMode = PixelAlphaCompositionMode.SrcOver; this.blendPercentage = 1F; - this.antialias = enableAntialiasing; - this.dpiX = DefaultTextDpi; - this.dpiY = DefaultTextDpi; + this.Antialias = enableAntialiasing; + this.dpiX = 72F; + this.dpiY = 72F; } + /// + /// Gets the default instance. + /// + public static TextGraphicsOptions Default { get; } = Lazy.Value; + /// /// Gets or sets a value indicating whether antialiasing should be applied. /// - public bool Antialias { get => this.antialias ?? true; set => this.antialias = value; } + public bool Antialias { get; set; } /// /// Gets or sets a value indicating the number of subpixels to use while rendering with antialiasing enabled. /// - public int AntialiasSubpixelDepth { get => this.antialiasSubpixelDepth ?? 16; set => this.antialiasSubpixelDepth = value; } + public int AntialiasSubpixelDepth + { + get + { + return this.antialiasSubpixelDepth; + } + + set + { + Guard.MustBeGreaterThanOrEqualTo(value, 0, nameof(this.AntialiasSubpixelDepth)); + this.antialiasSubpixelDepth = value; + } + } /// - /// Gets or sets a value indicating the blending percentage to apply to the drawing operation + /// Gets or sets a value indicating the blending percentage to apply to the drawing operation. /// - public float BlendPercentage { get => this.blendPercentage ?? 1F; set => this.blendPercentage = NumberUtils.ClampFloat(value, 0, 1F); } + public float BlendPercentage + { + get + { + return this.blendPercentage; + } + + set + { + Guard.MustBeBetweenOrEqualTo(value, 0, 1F, nameof(this.BlendPercentage)); + this.blendPercentage = value; + } + } // In the future we could expose a PixelBlender directly on here // or some forms of PixelBlender factory for each pixel type. Will need @@ -89,27 +109,63 @@ namespace SixLabors.ImageSharp.Processing /// /// Gets or sets a value indicating whether the text should be drawing with kerning enabled. /// - public bool ApplyKerning { get => this.applyKerning ?? true; set => this.applyKerning = value; } + public bool ApplyKerning { get; set; } /// /// Gets or sets a value indicating the number of space widths a tab should lock to. /// - public float TabWidth { get => this.tabWidth ?? 4; set => this.tabWidth = value; } + public float TabWidth + { + get + { + return this.tabWidth; + } + + set + { + Guard.MustBeGreaterThanOrEqualTo(value, 0, nameof(this.TabWidth)); + this.tabWidth = value; + } + } /// - /// Gets or sets a value indicating if greater than zero determine the width at which text should wrap. + /// Gets or sets a value, if greater than 0, indicating the width at which text should wrap. /// public float WrapTextWidth { get; set; } /// /// Gets or sets a value indicating the DPI to render text along the X axis. /// - public float DpiX { get => this.dpiX ?? DefaultTextDpi; set => this.dpiX = value; } + public float DpiX + { + get + { + return this.dpiX; + } + + set + { + Guard.MustBeGreaterThanOrEqualTo(value, 0, nameof(this.DpiX)); + this.dpiX = value; + } + } /// /// Gets or sets a value indicating the DPI to render text along the Y axis. /// - public float DpiY { get => this.dpiY ?? DefaultTextDpi; set => this.dpiY = value; } + public float DpiY + { + get + { + return this.dpiY; + } + + set + { + Guard.MustBeGreaterThanOrEqualTo(value, 0, nameof(this.DpiY)); + this.dpiY = value; + } + } /// /// Gets or sets a value indicating how to align the text relative to the rendering space. @@ -117,12 +173,12 @@ namespace SixLabors.ImageSharp.Processing /// defined by the location and width, if equals zero, and thus /// wrapping disabled, then the alignment is relative to the drawing location. /// - public HorizontalAlignment HorizontalAlignment { get => this.horizontalAlignment ?? HorizontalAlignment.Left; set => this.horizontalAlignment = value; } + public HorizontalAlignment HorizontalAlignment { get; set; } /// /// Gets or sets a value indicating how to align the text relative to the rendering space. /// - public VerticalAlignment VerticalAlignment { get => this.verticalAlignment ?? VerticalAlignment.Top; set => this.verticalAlignment = value; } + public VerticalAlignment VerticalAlignment { get; set; } /// /// Performs an implicit conversion from to . diff --git a/src/ImageSharp/GraphicsOptions.cs b/src/ImageSharp/GraphicsOptions.cs index 644bde1cff..1e18edb4ae 100644 --- a/src/ImageSharp/GraphicsOptions.cs +++ b/src/ImageSharp/GraphicsOptions.cs @@ -1,6 +1,7 @@ // Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. +using System; using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp @@ -8,108 +9,115 @@ namespace SixLabors.ImageSharp /// /// Options for influencing the drawing functions. /// - public struct GraphicsOptions + public class GraphicsOptions { + private static readonly Lazy Lazy = new Lazy(); + private int antialiasSubpixelDepth; + private float blendPercentage; + /// - /// Represents the default . + /// Initializes a new instance of the class. /// - public static readonly GraphicsOptions Default = new GraphicsOptions(true); - - private float? blendPercentage; - - private int? antialiasSubpixelDepth; - - private bool? antialias; + public GraphicsOptions() + : this(true) + { + } /// - /// Initializes a new instance of the struct. + /// Initializes a new instance of the class. /// /// If set to true [enable antialiasing]. public GraphicsOptions(bool enableAntialiasing) + : this(enableAntialiasing, 1F) { - this.ColorBlendingMode = PixelColorBlendingMode.Normal; - this.AlphaCompositionMode = PixelAlphaCompositionMode.SrcOver; - this.blendPercentage = 1; - this.antialiasSubpixelDepth = 16; - this.antialias = enableAntialiasing; } /// - /// Initializes a new instance of the struct. + /// Initializes a new instance of the class. /// /// If set to true [enable antialiasing]. - /// blending percentage to apply to the drawing operation - public GraphicsOptions(bool enableAntialiasing, float opacity) + /// The blending percentage to apply to the drawing operation + public GraphicsOptions(bool enableAntialiasing, float blendPercentage) + : this(enableAntialiasing, PixelColorBlendingMode.Normal, blendPercentage) { - Guard.MustBeBetweenOrEqualTo(opacity, 0, 1, nameof(opacity)); - - this.ColorBlendingMode = PixelColorBlendingMode.Normal; - this.AlphaCompositionMode = PixelAlphaCompositionMode.SrcOver; - this.blendPercentage = opacity; - this.antialiasSubpixelDepth = 16; - this.antialias = enableAntialiasing; } /// - /// Initializes a new instance of the struct. + /// Initializes a new instance of the class. /// /// If set to true [enable antialiasing]. - /// blending percentage to apply to the drawing operation - /// color blending mode to apply to the drawing operation - public GraphicsOptions(bool enableAntialiasing, PixelColorBlendingMode blending, float opacity) + /// The color blending mode to apply to the drawing operation + /// The blending percentage to apply to the drawing operation + public GraphicsOptions( + bool enableAntialiasing, + PixelColorBlendingMode blending, + float blendPercentage) + : this(enableAntialiasing, blending, PixelAlphaCompositionMode.SrcOver, blendPercentage) { - Guard.MustBeBetweenOrEqualTo(opacity, 0, 1, nameof(opacity)); - - this.ColorBlendingMode = blending; - this.AlphaCompositionMode = PixelAlphaCompositionMode.SrcOver; - this.blendPercentage = opacity; - this.antialiasSubpixelDepth = 16; - this.antialias = enableAntialiasing; } /// - /// Initializes a new instance of the struct. + /// Initializes a new instance of the class. /// /// If set to true [enable antialiasing]. - /// blending percentage to apply to the drawing operation - /// color blending mode to apply to the drawing operation - /// alpha composition mode to apply to the drawing operation - public GraphicsOptions(bool enableAntialiasing, PixelColorBlendingMode blending, PixelAlphaCompositionMode composition, float opacity) + /// The color blending mode to apply to the drawing operation + /// The alpha composition mode to apply to the drawing operation + /// The blending percentage to apply to the drawing operation + public GraphicsOptions( + bool enableAntialiasing, + PixelColorBlendingMode blending, + PixelAlphaCompositionMode composition, + float blendPercentage) { - Guard.MustBeBetweenOrEqualTo(opacity, 0, 1, nameof(opacity)); - this.ColorBlendingMode = blending; this.AlphaCompositionMode = composition; - this.blendPercentage = opacity; - this.antialiasSubpixelDepth = 16; - this.antialias = enableAntialiasing; + this.BlendPercentage = blendPercentage; + this.AntialiasSubpixelDepth = 16; + this.Antialias = enableAntialiasing; } + /// + /// Gets the default instance. + /// + public static GraphicsOptions Default { get; } = Lazy.Value; + /// /// Gets or sets a value indicating whether antialiasing should be applied. /// - public bool Antialias - { - get => this.antialias ?? true; - set => this.antialias = value; - } + public bool Antialias { get; set; } /// /// Gets or sets a value indicating the number of subpixels to use while rendering with antialiasing enabled. /// public int AntialiasSubpixelDepth { - get => this.antialiasSubpixelDepth ?? 16; - set => this.antialiasSubpixelDepth = value; + get + { + return this.antialiasSubpixelDepth; + } + + set + { + Guard.MustBeGreaterThanOrEqualTo(value, 0, nameof(this.AntialiasSubpixelDepth)); + this.antialiasSubpixelDepth = value; + } } /// - /// Gets or sets a value indicating the blending percentage to apply to the drawing operation + /// Gets or sets a value indicating the blending percentage to apply to the drawing operation. /// public float BlendPercentage { - get => (this.blendPercentage ?? 1).Clamp(0, 1); - set => this.blendPercentage = value; + get + { + return this.blendPercentage; + } + + set + { + Guard.MustBeBetweenOrEqualTo(value, 0, 1F, nameof(this.BlendPercentage)); + this.blendPercentage = value; + } } // In the future we could expose a PixelBlender directly on here diff --git a/tests/ImageSharp.Tests/Drawing/Paths/DrawPathCollection.cs b/tests/ImageSharp.Tests/Drawing/Paths/DrawPathCollection.cs index 3691b54ce3..5d309d2995 100644 --- a/tests/ImageSharp.Tests/Drawing/Paths/DrawPathCollection.cs +++ b/tests/ImageSharp.Tests/Drawing/Paths/DrawPathCollection.cs @@ -14,7 +14,7 @@ namespace SixLabors.ImageSharp.Tests.Drawing.Paths { public class DrawPathCollection : BaseImageOperationsExtensionTest { - GraphicsOptions noneDefault = new GraphicsOptions(); + GraphicsOptions noneDefault = new GraphicsOptions(false); Color color = Color.HotPink; Pen pen = Pens.Solid(Rgba32.HotPink, 1); IPath path1 = new Path(new LinearLineSegment(new SixLabors.Primitives.PointF[] { diff --git a/tests/ImageSharp.Tests/Drawing/Paths/FillPath.cs b/tests/ImageSharp.Tests/Drawing/Paths/FillPath.cs index 160ff22a3e..cb247ac039 100644 --- a/tests/ImageSharp.Tests/Drawing/Paths/FillPath.cs +++ b/tests/ImageSharp.Tests/Drawing/Paths/FillPath.cs @@ -14,7 +14,7 @@ namespace SixLabors.ImageSharp.Tests.Drawing.Paths { public class FillPath : BaseImageOperationsExtensionTest { - GraphicsOptions noneDefault = new GraphicsOptions(); + GraphicsOptions noneDefault = new GraphicsOptions(false); Color color = Color.HotPink; SolidBrush brush = Brushes.Solid(Rgba32.HotPink); IPath path = new Path(new LinearLineSegment(new SixLabors.Primitives.PointF[] { diff --git a/tests/ImageSharp.Tests/Drawing/Paths/FillPathCollection.cs b/tests/ImageSharp.Tests/Drawing/Paths/FillPathCollection.cs index b76ee8ffcd..0c4d6ca3ca 100644 --- a/tests/ImageSharp.Tests/Drawing/Paths/FillPathCollection.cs +++ b/tests/ImageSharp.Tests/Drawing/Paths/FillPathCollection.cs @@ -14,7 +14,7 @@ namespace SixLabors.ImageSharp.Tests.Drawing.Paths { public class FillPathCollection : BaseImageOperationsExtensionTest { - GraphicsOptions noneDefault = new GraphicsOptions(); + GraphicsOptions noneDefault = new GraphicsOptions(false); Color color = Color.HotPink; SolidBrush brush = Brushes.Solid(Rgba32.HotPink); IPath path1 = new Path(new LinearLineSegment(new SixLabors.Primitives.PointF[] { diff --git a/tests/ImageSharp.Tests/Drawing/Paths/FillPolygon.cs b/tests/ImageSharp.Tests/Drawing/Paths/FillPolygon.cs index c62a871481..06a7348427 100644 --- a/tests/ImageSharp.Tests/Drawing/Paths/FillPolygon.cs +++ b/tests/ImageSharp.Tests/Drawing/Paths/FillPolygon.cs @@ -14,7 +14,7 @@ namespace SixLabors.ImageSharp.Tests.Drawing.Paths { public class FillPolygon : BaseImageOperationsExtensionTest { - GraphicsOptions noneDefault = new GraphicsOptions(); + GraphicsOptions noneDefault = new GraphicsOptions(false); Color color = Color.HotPink; SolidBrush brush = Brushes.Solid(Rgba32.HotPink); SixLabors.Primitives.PointF[] path = { diff --git a/tests/ImageSharp.Tests/Drawing/Paths/FillRectangle.cs b/tests/ImageSharp.Tests/Drawing/Paths/FillRectangle.cs index 17a2b87c0e..c1ad059a85 100644 --- a/tests/ImageSharp.Tests/Drawing/Paths/FillRectangle.cs +++ b/tests/ImageSharp.Tests/Drawing/Paths/FillRectangle.cs @@ -1,4 +1,4 @@ -// Copyright (c) Six Labors and contributors. +// Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. using SixLabors.ImageSharp.PixelFormats; @@ -13,7 +13,7 @@ namespace SixLabors.ImageSharp.Tests.Drawing.Paths { public class FillRectangle : BaseImageOperationsExtensionTest { - GraphicsOptions noneDefault = new GraphicsOptions(); + GraphicsOptions noneDefault = new GraphicsOptions(false); Color color = Color.HotPink; SolidBrush brush = Brushes.Solid(Rgba32.HotPink); SixLabors.Primitives.Rectangle rectangle = new SixLabors.Primitives.Rectangle(10, 10, 77, 76); diff --git a/tests/ImageSharp.Tests/PixelFormats/PixelBlenders/PorterDuffCompositorTests.cs b/tests/ImageSharp.Tests/PixelFormats/PixelBlenders/PorterDuffCompositorTests.cs index 09a78a6aa1..693dd6bd81 100644 --- a/tests/ImageSharp.Tests/PixelFormats/PixelBlenders/PorterDuffCompositorTests.cs +++ b/tests/ImageSharp.Tests/PixelFormats/PixelBlenders/PorterDuffCompositorTests.cs @@ -1,4 +1,4 @@ -// Copyright (c) Six Labors and contributors. +// Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. namespace SixLabors.ImageSharp.Tests.PixelFormats.PixelBlenders @@ -36,9 +36,10 @@ namespace SixLabors.ImageSharp.Tests.PixelFormats.PixelBlenders using (Image src = srcFile.CreateRgba32Image()) using (Image dest = provider.GetImage()) { - GraphicsOptions options = new GraphicsOptions - { - AlphaCompositionMode = mode + var options = new GraphicsOptions + { + Antialias = false, + AlphaCompositionMode = mode }; using (Image res = dest.Clone(x => x.DrawImage(src, options))) @@ -53,4 +54,4 @@ namespace SixLabors.ImageSharp.Tests.PixelFormats.PixelBlenders } } } -} \ No newline at end of file +} From 50b4d6a669a6d08bf49dc184c7907732e7e9f454 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Sat, 9 Nov 2019 21:54:48 +1100 Subject: [PATCH 47/75] Fix MemoryAllocator access. --- .../Processing/Processors/Text/DrawTextProcessor{TPixel}.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ImageSharp.Drawing/Processing/Processors/Text/DrawTextProcessor{TPixel}.cs b/src/ImageSharp.Drawing/Processing/Processors/Text/DrawTextProcessor{TPixel}.cs index 8ae6ee4916..244e18b81c 100644 --- a/src/ImageSharp.Drawing/Processing/Processors/Text/DrawTextProcessor{TPixel}.cs +++ b/src/ImageSharp.Drawing/Processing/Processors/Text/DrawTextProcessor{TPixel}.cs @@ -59,7 +59,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Text VerticalAlignment = this.Options.VerticalAlignment }; - this.textRenderer = new CachingGlyphRenderer(this.Source.GetMemoryAllocator(), this.Text.Length, this.Pen, this.Brush != null); + this.textRenderer = new CachingGlyphRenderer(this.Configuration.MemoryAllocator, this.Text.Length, this.Pen, this.Brush != null); this.textRenderer.Options = (GraphicsOptions)this.Options; var renderer = new TextRenderer(this.textRenderer); renderer.RenderText(this.Text, style); From e20d8206ea87fd862cdd0d3022a4e0815c628498 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Sat, 9 Nov 2019 22:39:23 +1100 Subject: [PATCH 48/75] Expose Allocate2D --- .../Memory/MemoryAllocatorExtensions.cs | 27 ++++++++++++++++--- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/src/ImageSharp/Memory/MemoryAllocatorExtensions.cs b/src/ImageSharp/Memory/MemoryAllocatorExtensions.cs index b596351b5f..a3fa0e1ff1 100644 --- a/src/ImageSharp/Memory/MemoryAllocatorExtensions.cs +++ b/src/ImageSharp/Memory/MemoryAllocatorExtensions.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.Buffers; @@ -11,8 +11,18 @@ namespace SixLabors.ImageSharp.Memory /// /// Extension methods for . /// - internal static class MemoryAllocatorExtensions + public static class MemoryAllocatorExtensions { + /// + /// Allocates a buffer of value type objects interpreted as a 2D region + /// of x elements. + /// + /// The type of buffer items to allocate. + /// The memory allocator. + /// The buffer width. + /// The buffer heght. + /// The allocation options. + /// The . public static Buffer2D Allocate2D( this MemoryAllocator memoryAllocator, int width, @@ -26,6 +36,15 @@ namespace SixLabors.ImageSharp.Memory return new Buffer2D(memorySource, width, height); } + /// + /// Allocates a buffer of value type objects interpreted as a 2D region + /// of width x height elements. + /// + /// The type of buffer items to allocate. + /// The memory allocator. + /// The buffer size. + /// The allocation options. + /// The . public static Buffer2D Allocate2D( this MemoryAllocator memoryAllocator, Size size, @@ -41,7 +60,7 @@ namespace SixLabors.ImageSharp.Memory /// The pixel size in bytes, eg. 3 for RGB /// The padding /// A - public static IManagedByteBuffer AllocatePaddedPixelRowBuffer( + internal static IManagedByteBuffer AllocatePaddedPixelRowBuffer( this MemoryAllocator memoryAllocator, int width, int pixelSizeInBytes, @@ -51,4 +70,4 @@ namespace SixLabors.ImageSharp.Memory return memoryAllocator.AllocateManagedByteBuffer(length); } } -} \ No newline at end of file +} From 4a4379ef800d806fd86f01005ccff2815657b1c6 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Sun, 10 Nov 2019 20:28:55 +1100 Subject: [PATCH 49/75] Simplify options and add clone + tests --- .../Processing/TextGraphicsOptions.cs | 103 +++++++------- .../ImageSharp.Benchmarks/Drawing/DrawText.cs | 10 +- .../Drawing/DrawTextOutline.cs | 14 +- .../Drawing/DrawLinesTests.cs | 22 +-- .../Drawing/DrawPolygonTests.cs | 2 +- .../Drawing/FillLinearGradientBrushTests.cs | 3 +- .../Drawing/FillPolygonTests.cs | 2 +- .../Drawing/FillRegionProcessorTests.cs | 16 +-- .../Drawing/FillSolidBrushTests.cs | 24 ++-- .../Drawing/Paths/DrawPathCollection.cs | 2 +- .../Drawing/Paths/FillPath.cs | 2 +- .../Drawing/Paths/FillPathCollection.cs | 2 +- .../Drawing/Paths/FillPolygon.cs | 2 +- .../Drawing/Paths/FillRectangle.cs | 2 +- .../Drawing/SolidFillBlendedShapesTests.cs | 29 ++-- .../ImageSharp.Tests/Drawing/Text/DrawText.cs | 12 +- .../Drawing/Text/DrawTextOnImageTests.cs | 9 +- .../Drawing/Text/TextGraphicsOptionsTests.cs | 128 +++++++++++++++++- .../ImageSharp.Tests/GraphicsOptionsTests.cs | 65 ++++++++- tests/ImageSharp.Tests/Issues/Issue412.cs | 4 +- .../BaseImageOperationsExtensionTest.cs | 2 +- 21 files changed, 316 insertions(+), 139 deletions(-) diff --git a/src/ImageSharp.Drawing/Processing/TextGraphicsOptions.cs b/src/ImageSharp.Drawing/Processing/TextGraphicsOptions.cs index 23d7aebf58..60a48a31fa 100644 --- a/src/ImageSharp.Drawing/Processing/TextGraphicsOptions.cs +++ b/src/ImageSharp.Drawing/Processing/TextGraphicsOptions.cs @@ -12,51 +12,22 @@ namespace SixLabors.ImageSharp.Processing /// public class TextGraphicsOptions { - private static readonly Lazy Lazy = new Lazy(); - private int antialiasSubpixelDepth; - private float blendPercentage; - private float tabWidth; - private float dpiX; - private float dpiY; - - /// - /// Initializes a new instance of the class. - /// - public TextGraphicsOptions() - : this(true) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// If set to true [enable antialiasing]. - public TextGraphicsOptions(bool enableAntialiasing) - { - this.ApplyKerning = true; - this.TabWidth = 4F; - this.WrapTextWidth = 0; - this.HorizontalAlignment = HorizontalAlignment.Left; - this.VerticalAlignment = VerticalAlignment.Top; - - this.antialiasSubpixelDepth = 16; - this.ColorBlendingMode = PixelColorBlendingMode.Normal; - this.AlphaCompositionMode = PixelAlphaCompositionMode.SrcOver; - this.blendPercentage = 1F; - this.Antialias = enableAntialiasing; - this.dpiX = 72F; - this.dpiY = 72F; - } + private int antialiasSubpixelDepth = 16; + private float blendPercentage = 1F; + private float tabWidth = 4F; + private float dpiX = 72F; + private float dpiY = 72F; /// /// Gets the default instance. /// - public static TextGraphicsOptions Default { get; } = Lazy.Value; + public static TextGraphicsOptions Default { get; } = new TextGraphicsOptions(); /// /// Gets or sets a value indicating whether antialiasing should be applied. + /// Defaults to true. /// - public bool Antialias { get; set; } + public bool Antialias { get; set; } = true; /// /// Gets or sets a value indicating the number of subpixels to use while rendering with antialiasing enabled. @@ -92,27 +63,27 @@ namespace SixLabors.ImageSharp.Processing } } - // In the future we could expose a PixelBlender directly on here - // or some forms of PixelBlender factory for each pixel type. Will need - // some API thought post V1. - /// - /// Gets or sets a value indicating the color blending percentage to apply to the drawing operation + /// Gets or sets a value indicating the color blending percentage to apply to the drawing operation. + /// Defaults to . /// - public PixelColorBlendingMode ColorBlendingMode { get; set; } + public PixelColorBlendingMode ColorBlendingMode { get; set; } = PixelColorBlendingMode.Normal; /// /// Gets or sets a value indicating the color blending percentage to apply to the drawing operation + /// Defaults to . /// - public PixelAlphaCompositionMode AlphaCompositionMode { get; set; } + public PixelAlphaCompositionMode AlphaCompositionMode { get; set; } = PixelAlphaCompositionMode.SrcOver; /// /// Gets or sets a value indicating whether the text should be drawing with kerning enabled. + /// Defaults to true; /// - public bool ApplyKerning { get; set; } + public bool ApplyKerning { get; set; } = true; /// /// Gets or sets a value indicating the number of space widths a tab should lock to. + /// Defaults to 4. /// public float TabWidth { @@ -130,11 +101,13 @@ namespace SixLabors.ImageSharp.Processing /// /// Gets or sets a value, if greater than 0, indicating the width at which text should wrap. + /// Defaults to 0. /// public float WrapTextWidth { get; set; } /// - /// Gets or sets a value indicating the DPI to render text along the X axis. + /// Gets or sets a value indicating the DPI (Dots Per Inch) to render text along the X axis. + /// Defaults to 72. /// public float DpiX { @@ -151,7 +124,8 @@ namespace SixLabors.ImageSharp.Processing } /// - /// Gets or sets a value indicating the DPI to render text along the Y axis. + /// Gets or sets a value indicating the DPI (Dots Per Inch) to render text along the Y axis. + /// Defaults to 72. /// public float DpiY { @@ -172,13 +146,15 @@ namespace SixLabors.ImageSharp.Processing /// If is greater than zero it will align relative to the space /// defined by the location and width, if equals zero, and thus /// wrapping disabled, then the alignment is relative to the drawing location. + /// Defaults to . /// - public HorizontalAlignment HorizontalAlignment { get; set; } + public HorizontalAlignment HorizontalAlignment { get; set; } = HorizontalAlignment.Left; /// /// Gets or sets a value indicating how to align the text relative to the rendering space. + /// Defaults to . /// - public VerticalAlignment VerticalAlignment { get; set; } + public VerticalAlignment VerticalAlignment { get; set; } = VerticalAlignment.Top; /// /// Performs an implicit conversion from to . @@ -189,8 +165,9 @@ namespace SixLabors.ImageSharp.Processing /// public static implicit operator TextGraphicsOptions(GraphicsOptions options) { - return new TextGraphicsOptions(options.Antialias) + return new TextGraphicsOptions() { + Antialias = options.Antialias, AntialiasSubpixelDepth = options.AntialiasSubpixelDepth, blendPercentage = options.BlendPercentage, ColorBlendingMode = options.ColorBlendingMode, @@ -207,13 +184,37 @@ namespace SixLabors.ImageSharp.Processing /// public static explicit operator GraphicsOptions(TextGraphicsOptions options) { - return new GraphicsOptions(options.Antialias) + return new GraphicsOptions() { + Antialias = options.Antialias, AntialiasSubpixelDepth = options.AntialiasSubpixelDepth, ColorBlendingMode = options.ColorBlendingMode, AlphaCompositionMode = options.AlphaCompositionMode, BlendPercentage = options.BlendPercentage }; } + + /// + /// Creates a shallow copy of the . + /// + /// A new options instance. + public TextGraphicsOptions Clone() + { + return new TextGraphicsOptions + { + AlphaCompositionMode = this.AlphaCompositionMode, + Antialias = this.Antialias, + AntialiasSubpixelDepth = this.AntialiasSubpixelDepth, + ApplyKerning = this.ApplyKerning, + BlendPercentage = this.BlendPercentage, + ColorBlendingMode = this.ColorBlendingMode, + DpiX = this.DpiX, + DpiY = this.DpiY, + HorizontalAlignment = this.HorizontalAlignment, + TabWidth = this.TabWidth, + WrapTextWidth = this.WrapTextWidth, + VerticalAlignment = this.VerticalAlignment + }; + } } } diff --git a/tests/ImageSharp.Benchmarks/Drawing/DrawText.cs b/tests/ImageSharp.Benchmarks/Drawing/DrawText.cs index 0982db3340..c199613900 100644 --- a/tests/ImageSharp.Benchmarks/Drawing/DrawText.cs +++ b/tests/ImageSharp.Benchmarks/Drawing/DrawText.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.Drawing; @@ -31,7 +31,7 @@ namespace SixLabors.ImageSharp.Benchmarks graphics.SmoothingMode = SmoothingMode.AntiAlias; using (var font = new Font("Arial", 12, GraphicsUnit.Point)) { - graphics.DrawString(TextToRender, font, System.Drawing.Brushes.HotPink, new RectangleF(10, 10, 780, 780)); + graphics.DrawString(this.TextToRender, font, System.Drawing.Brushes.HotPink, new RectangleF(10, 10, 780, 780)); } } } @@ -42,7 +42,7 @@ namespace SixLabors.ImageSharp.Benchmarks using (var image = new Image(800, 800)) { var font = SixLabors.Fonts.SystemFonts.CreateFont("Arial", 12); - image.Mutate(x => x.ApplyProcessor(new DrawTextProcessor(new TextGraphicsOptions(true) { WrapTextWidth = 780 }, TextToRender, font, Processing.Brushes.Solid(Rgba32.HotPink), null, new SixLabors.Primitives.PointF(10, 10)))); + image.Mutate(x => x.ApplyProcessor(new DrawTextProcessor(new TextGraphicsOptions { Antialias = true, WrapTextWidth = 780 }, this.TextToRender, font, Processing.Brushes.Solid(Rgba32.HotPink), null, new SixLabors.Primitives.PointF(10, 10)))); } } @@ -52,7 +52,7 @@ namespace SixLabors.ImageSharp.Benchmarks using (var image = new Image(800, 800)) { var font = SixLabors.Fonts.SystemFonts.CreateFont("Arial", 12); - image.Mutate(x => DrawTextOldVersion(x, new TextGraphicsOptions(true) { WrapTextWidth = 780 }, TextToRender, font, Processing.Brushes.Solid(Rgba32.HotPink), null, new SixLabors.Primitives.PointF(10, 10))); + image.Mutate(x => DrawTextOldVersion(x, new TextGraphicsOptions { Antialias = true, WrapTextWidth = 780 }, this.TextToRender, font, Processing.Brushes.Solid(Rgba32.HotPink), null, new SixLabors.Primitives.PointF(10, 10))); } IImageProcessingContext DrawTextOldVersion( @@ -93,4 +93,4 @@ namespace SixLabors.ImageSharp.Benchmarks } } } -} \ No newline at end of file +} diff --git a/tests/ImageSharp.Benchmarks/Drawing/DrawTextOutline.cs b/tests/ImageSharp.Benchmarks/Drawing/DrawTextOutline.cs index c5c1ba5ac1..7d8b776598 100644 --- a/tests/ImageSharp.Benchmarks/Drawing/DrawTextOutline.cs +++ b/tests/ImageSharp.Benchmarks/Drawing/DrawTextOutline.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.Drawing; @@ -17,7 +17,7 @@ namespace SixLabors.ImageSharp.Benchmarks [Params(10, 100)] public int TextIterations { get; set; } public string TextPhrase { get; set; } = "Hello World"; - public string TextToRender => string.Join(" ", Enumerable.Repeat(TextPhrase, TextIterations)); + public string TextToRender => string.Join(" ", Enumerable.Repeat(this.TextPhrase, this.TextIterations)); [Benchmark(Baseline = true, Description = "System.Drawing Draw Text Outline")] public void DrawTextSystemDrawing() @@ -31,7 +31,7 @@ namespace SixLabors.ImageSharp.Benchmarks using (var font = new Font("Arial", 12, GraphicsUnit.Point)) using (var gp = new GraphicsPath()) { - gp.AddString(TextToRender, font.FontFamily, (int)font.Style, font.Size, new RectangleF(10, 10, 780, 780), new StringFormat()); + gp.AddString(this.TextToRender, font.FontFamily, (int)font.Style, font.Size, new RectangleF(10, 10, 780, 780), new StringFormat()); graphics.DrawPath(pen, gp); } } @@ -43,7 +43,7 @@ namespace SixLabors.ImageSharp.Benchmarks using (var image = new Image(800, 800)) { var font = SixLabors.Fonts.SystemFonts.CreateFont("Arial", 12); - image.Mutate(x => x.ApplyProcessor(new DrawTextProcessor(new TextGraphicsOptions(true) { WrapTextWidth = 780 }, TextToRender, font, null, Processing.Pens.Solid(Rgba32.HotPink, 10), new SixLabors.Primitives.PointF(10, 10)))); + image.Mutate(x => x.ApplyProcessor(new DrawTextProcessor(new TextGraphicsOptions { Antialias = true, WrapTextWidth = 780 }, this.TextToRender, font, null, Processing.Pens.Solid(Rgba32.HotPink, 10), new SixLabors.Primitives.PointF(10, 10)))); } } @@ -56,8 +56,8 @@ namespace SixLabors.ImageSharp.Benchmarks image.Mutate( x => DrawTextOldVersion( x, - new TextGraphicsOptions(true) { WrapTextWidth = 780 }, - TextToRender, + new TextGraphicsOptions { Antialias = true, WrapTextWidth = 780 }, + this.TextToRender, font, null, Processing.Pens.Solid(Rgba32.HotPink, 10), @@ -99,4 +99,4 @@ namespace SixLabors.ImageSharp.Benchmarks } } } -} \ No newline at end of file +} diff --git a/tests/ImageSharp.Tests/Drawing/DrawLinesTests.cs b/tests/ImageSharp.Tests/Drawing/DrawLinesTests.cs index 2836f8a38d..b45fc620b2 100644 --- a/tests/ImageSharp.Tests/Drawing/DrawLinesTests.cs +++ b/tests/ImageSharp.Tests/Drawing/DrawLinesTests.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; @@ -24,10 +24,10 @@ namespace SixLabors.ImageSharp.Tests.Drawing { Color color = TestUtils.GetColorByName(colorName).WithAlpha(alpha); Pen pen = new Pen(color, thickness); - + DrawLinesImpl(provider, colorName, alpha, thickness, antialias, pen); } - + [Theory] [WithBasicTestPatternImages(250, 350, PixelTypes.Rgba32, "White", 1f, 5, false)] public void DrawLines_Dash(TestImageProvider provider, string colorName, float alpha, float thickness, bool antialias) @@ -35,10 +35,10 @@ namespace SixLabors.ImageSharp.Tests.Drawing { Color color = TestUtils.GetColorByName(colorName).WithAlpha(alpha); Pen pen = Pens.Dash(color, thickness); - + DrawLinesImpl(provider, colorName, alpha, thickness, antialias, pen); } - + [Theory] [WithBasicTestPatternImages(250, 350, PixelTypes.Rgba32, "LightGreen", 1f, 5, false)] public void DrawLines_Dot(TestImageProvider provider, string colorName, float alpha, float thickness, bool antialias) @@ -46,10 +46,10 @@ namespace SixLabors.ImageSharp.Tests.Drawing { Color color = TestUtils.GetColorByName(colorName).WithAlpha(alpha); Pen pen = Pens.Dot(color, thickness); - + DrawLinesImpl(provider, colorName, alpha, thickness, antialias, pen); } - + [Theory] [WithBasicTestPatternImages(250, 350, PixelTypes.Rgba32, "Yellow", 1f, 5, false)] public void DrawLines_DashDot(TestImageProvider provider, string colorName, float alpha, float thickness, bool antialias) @@ -57,7 +57,7 @@ namespace SixLabors.ImageSharp.Tests.Drawing { Color color = TestUtils.GetColorByName(colorName).WithAlpha(alpha); Pen pen = Pens.DashDot(color, thickness); - + DrawLinesImpl(provider, colorName, alpha, thickness, antialias, pen); } @@ -68,11 +68,11 @@ namespace SixLabors.ImageSharp.Tests.Drawing { Color color = TestUtils.GetColorByName(colorName).WithAlpha(alpha); Pen pen = Pens.DashDotDot(color, thickness); - + DrawLinesImpl(provider, colorName, alpha, thickness, antialias, pen); } - + private static void DrawLinesImpl( TestImageProvider provider, string colorName, @@ -84,7 +84,7 @@ namespace SixLabors.ImageSharp.Tests.Drawing { SixLabors.Primitives.PointF[] simplePath = { new Vector2(10, 10), new Vector2(200, 150), new Vector2(50, 300) }; - GraphicsOptions options = new GraphicsOptions(antialias); + GraphicsOptions options = new GraphicsOptions { Antialias = antialias }; string aa = antialias ? "" : "_NoAntialias"; FormattableString outputDetails = $"{colorName}_A({alpha})_T({thickness}){aa}"; diff --git a/tests/ImageSharp.Tests/Drawing/DrawPolygonTests.cs b/tests/ImageSharp.Tests/Drawing/DrawPolygonTests.cs index 18fde6ad8f..4a6cb430a8 100644 --- a/tests/ImageSharp.Tests/Drawing/DrawPolygonTests.cs +++ b/tests/ImageSharp.Tests/Drawing/DrawPolygonTests.cs @@ -27,7 +27,7 @@ namespace SixLabors.ImageSharp.Tests.Drawing }; Color color = TestUtils.GetColorByName(colorName).WithAlpha(alpha); - GraphicsOptions options = new GraphicsOptions(antialias); + GraphicsOptions options = new GraphicsOptions { Antialias = antialias }; string aa = antialias ? "" : "_NoAntialias"; FormattableString outputDetails = $"{colorName}_A({alpha})_T({thickness}){aa}"; diff --git a/tests/ImageSharp.Tests/Drawing/FillLinearGradientBrushTests.cs b/tests/ImageSharp.Tests/Drawing/FillLinearGradientBrushTests.cs index 8aae342cba..031e732eaa 100644 --- a/tests/ImageSharp.Tests/Drawing/FillLinearGradientBrushTests.cs +++ b/tests/ImageSharp.Tests/Drawing/FillLinearGradientBrushTests.cs @@ -412,8 +412,9 @@ namespace SixLabors.ImageSharp.Tests.Drawing { Size size = ctx.GetCurrentSize(); IPathCollection glossPath = BuildGloss(size.Width, size.Height); - var graphicsOptions = new GraphicsOptions(true) + var graphicsOptions = new GraphicsOptions { + Antialias = true, ColorBlendingMode = PixelColorBlendingMode.Normal, AlphaCompositionMode = PixelAlphaCompositionMode.SrcAtop }; diff --git a/tests/ImageSharp.Tests/Drawing/FillPolygonTests.cs b/tests/ImageSharp.Tests/Drawing/FillPolygonTests.cs index 104237ec3e..22294e76df 100644 --- a/tests/ImageSharp.Tests/Drawing/FillPolygonTests.cs +++ b/tests/ImageSharp.Tests/Drawing/FillPolygonTests.cs @@ -29,7 +29,7 @@ namespace SixLabors.ImageSharp.Tests.Drawing }; Color color = TestUtils.GetColorByName(colorName).WithAlpha(alpha); - var options = new GraphicsOptions(antialias); + var options = new GraphicsOptions { Antialias = antialias }; string aa = antialias ? "" : "_NoAntialias"; FormattableString outputDetails = $"{colorName}_A{alpha}{aa}"; diff --git a/tests/ImageSharp.Tests/Drawing/FillRegionProcessorTests.cs b/tests/ImageSharp.Tests/Drawing/FillRegionProcessorTests.cs index c0388ea2d4..6230d52a17 100644 --- a/tests/ImageSharp.Tests/Drawing/FillRegionProcessorTests.cs +++ b/tests/ImageSharp.Tests/Drawing/FillRegionProcessorTests.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.Numerics; @@ -16,8 +16,6 @@ using SixLabors.Shapes; namespace SixLabors.ImageSharp.Tests.Drawing { - - public class FillRegionProcessorTests { @@ -35,8 +33,9 @@ namespace SixLabors.ImageSharp.Tests.Drawing var brush = new Mock(); var region = new MockRegion2(bounds); - var options = new GraphicsOptions(antialias) + var options = new GraphicsOptions { + Antialias = antialias, AntialiasSubpixelDepth = 1 }; var processor = new FillRegionProcessor(brush.Object, region, options); @@ -51,7 +50,7 @@ namespace SixLabors.ImageSharp.Tests.Drawing { var bounds = new Rectangle(-100, -10, 10, 10); var brush = new Mock(); - var options = new GraphicsOptions(true); + var options = new GraphicsOptions { Antialias = true }; var processor = new FillRegionProcessor(brush.Object, new MockRegion1(), options); var img = new Image(10, 10); processor.Execute(img, bounds); @@ -77,7 +76,8 @@ namespace SixLabors.ImageSharp.Tests.Drawing { img.Mutate(x => x.Fill(Rgba32.Transparent)); - img.Mutate(ctx => { + img.Mutate(ctx => + { ctx.DrawLines( Rgba32.Red, 0.984252f, @@ -90,7 +90,7 @@ namespace SixLabors.ImageSharp.Tests.Drawing new PointF(104.782608f, 1075.13245f), new PointF(104.782608f, 1075.13245f) ); - } + } ); } } @@ -98,7 +98,7 @@ namespace SixLabors.ImageSharp.Tests.Drawing [Fact] public void DoesNotThrowFillingTriangle() { - using(var image = new Image(28, 28)) + using (var image = new Image(28, 28)) { var path = new Polygon( new LinearLineSegment(new PointF(17.11f, 13.99659f), new PointF(14.01433f, 27.06201f)), diff --git a/tests/ImageSharp.Tests/Drawing/FillSolidBrushTests.cs b/tests/ImageSharp.Tests/Drawing/FillSolidBrushTests.cs index a5e7450839..1e3688fead 100644 --- a/tests/ImageSharp.Tests/Drawing/FillSolidBrushTests.cs +++ b/tests/ImageSharp.Tests/Drawing/FillSolidBrushTests.cs @@ -156,10 +156,12 @@ namespace SixLabors.ImageSharp.Tests.Drawing { TPixel bgColor = image[0, 0]; - var options = new GraphicsOptions(false) - { - ColorBlendingMode = blenderMode, BlendPercentage = blendPercentage - }; + var options = new GraphicsOptions + { + Antialias = false, + ColorBlendingMode = blenderMode, + BlendPercentage = blendPercentage + }; if (triggerFillRegion) { @@ -173,13 +175,13 @@ namespace SixLabors.ImageSharp.Tests.Drawing } var testOutputDetails = new - { - triggerFillRegion = triggerFillRegion, - newColorName = newColorName, - alpha = alpha, - blenderMode = blenderMode, - blendPercentage = blendPercentage - }; + { + triggerFillRegion = triggerFillRegion, + newColorName = newColorName, + alpha = alpha, + blenderMode = blenderMode, + blendPercentage = blendPercentage + }; image.DebugSave( provider, diff --git a/tests/ImageSharp.Tests/Drawing/Paths/DrawPathCollection.cs b/tests/ImageSharp.Tests/Drawing/Paths/DrawPathCollection.cs index 5d309d2995..f6d9c7fe5c 100644 --- a/tests/ImageSharp.Tests/Drawing/Paths/DrawPathCollection.cs +++ b/tests/ImageSharp.Tests/Drawing/Paths/DrawPathCollection.cs @@ -14,7 +14,7 @@ namespace SixLabors.ImageSharp.Tests.Drawing.Paths { public class DrawPathCollection : BaseImageOperationsExtensionTest { - GraphicsOptions noneDefault = new GraphicsOptions(false); + GraphicsOptions noneDefault = new GraphicsOptions { Antialias = false }; Color color = Color.HotPink; Pen pen = Pens.Solid(Rgba32.HotPink, 1); IPath path1 = new Path(new LinearLineSegment(new SixLabors.Primitives.PointF[] { diff --git a/tests/ImageSharp.Tests/Drawing/Paths/FillPath.cs b/tests/ImageSharp.Tests/Drawing/Paths/FillPath.cs index cb247ac039..fa1949e673 100644 --- a/tests/ImageSharp.Tests/Drawing/Paths/FillPath.cs +++ b/tests/ImageSharp.Tests/Drawing/Paths/FillPath.cs @@ -14,7 +14,7 @@ namespace SixLabors.ImageSharp.Tests.Drawing.Paths { public class FillPath : BaseImageOperationsExtensionTest { - GraphicsOptions noneDefault = new GraphicsOptions(false); + GraphicsOptions noneDefault = new GraphicsOptions { Antialias = false }; Color color = Color.HotPink; SolidBrush brush = Brushes.Solid(Rgba32.HotPink); IPath path = new Path(new LinearLineSegment(new SixLabors.Primitives.PointF[] { diff --git a/tests/ImageSharp.Tests/Drawing/Paths/FillPathCollection.cs b/tests/ImageSharp.Tests/Drawing/Paths/FillPathCollection.cs index 0c4d6ca3ca..39e2fc2f97 100644 --- a/tests/ImageSharp.Tests/Drawing/Paths/FillPathCollection.cs +++ b/tests/ImageSharp.Tests/Drawing/Paths/FillPathCollection.cs @@ -14,7 +14,7 @@ namespace SixLabors.ImageSharp.Tests.Drawing.Paths { public class FillPathCollection : BaseImageOperationsExtensionTest { - GraphicsOptions noneDefault = new GraphicsOptions(false); + GraphicsOptions noneDefault = new GraphicsOptions { Antialias = false }; Color color = Color.HotPink; SolidBrush brush = Brushes.Solid(Rgba32.HotPink); IPath path1 = new Path(new LinearLineSegment(new SixLabors.Primitives.PointF[] { diff --git a/tests/ImageSharp.Tests/Drawing/Paths/FillPolygon.cs b/tests/ImageSharp.Tests/Drawing/Paths/FillPolygon.cs index 06a7348427..03a827a6a9 100644 --- a/tests/ImageSharp.Tests/Drawing/Paths/FillPolygon.cs +++ b/tests/ImageSharp.Tests/Drawing/Paths/FillPolygon.cs @@ -14,7 +14,7 @@ namespace SixLabors.ImageSharp.Tests.Drawing.Paths { public class FillPolygon : BaseImageOperationsExtensionTest { - GraphicsOptions noneDefault = new GraphicsOptions(false); + GraphicsOptions noneDefault = new GraphicsOptions { Antialias = false }; Color color = Color.HotPink; SolidBrush brush = Brushes.Solid(Rgba32.HotPink); SixLabors.Primitives.PointF[] path = { diff --git a/tests/ImageSharp.Tests/Drawing/Paths/FillRectangle.cs b/tests/ImageSharp.Tests/Drawing/Paths/FillRectangle.cs index c1ad059a85..cc108fb54e 100644 --- a/tests/ImageSharp.Tests/Drawing/Paths/FillRectangle.cs +++ b/tests/ImageSharp.Tests/Drawing/Paths/FillRectangle.cs @@ -13,7 +13,7 @@ namespace SixLabors.ImageSharp.Tests.Drawing.Paths { public class FillRectangle : BaseImageOperationsExtensionTest { - GraphicsOptions noneDefault = new GraphicsOptions(false); + GraphicsOptions noneDefault = new GraphicsOptions { Antialias = false }; Color color = Color.HotPink; SolidBrush brush = Brushes.Solid(Rgba32.HotPink); SixLabors.Primitives.Rectangle rectangle = new SixLabors.Primitives.Rectangle(10, 10, 77, 76); diff --git a/tests/ImageSharp.Tests/Drawing/SolidFillBlendedShapesTests.cs b/tests/ImageSharp.Tests/Drawing/SolidFillBlendedShapesTests.cs index da7c865b96..f1a62cf292 100644 --- a/tests/ImageSharp.Tests/Drawing/SolidFillBlendedShapesTests.cs +++ b/tests/ImageSharp.Tests/Drawing/SolidFillBlendedShapesTests.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; using System.Collections.Generic; @@ -27,7 +27,7 @@ namespace SixLabors.ImageSharp.Tests.Drawing } } } - + [Theory] [WithBlankImages(nameof(modes), 250, 250, PixelTypes.Rgba32)] @@ -46,7 +46,8 @@ namespace SixLabors.ImageSharp.Tests.Drawing Color.DarkBlue, new Rectangle(0 * scaleX, 40 * scaleY, 100 * scaleX, 20 * scaleY) ) - .Fill(new GraphicsOptions(true) { ColorBlendingMode = blending, AlphaCompositionMode=composition }, + .Fill( + new GraphicsOptions { Antialias = true, ColorBlendingMode = blending, AlphaCompositionMode = composition }, Color.HotPink, new Rectangle(20 * scaleX, 0 * scaleY, 30 * scaleX, 100 * scaleY)) ); @@ -73,12 +74,12 @@ namespace SixLabors.ImageSharp.Tests.Drawing new Rectangle(0 * scaleX, 40 * scaleY, 100 * scaleX, 20 * scaleY))); img.Mutate( x => x.Fill( - new GraphicsOptions(true) { ColorBlendingMode = blending, AlphaCompositionMode = composition }, + new GraphicsOptions { Antialias = true, ColorBlendingMode = blending, AlphaCompositionMode = composition }, Color.HotPink, new Rectangle(20 * scaleX, 0 * scaleY, 30 * scaleX, 100 * scaleY))); img.Mutate( x => x.Fill( - new GraphicsOptions(true) { ColorBlendingMode = blending, AlphaCompositionMode = composition }, + new GraphicsOptions { Antialias = true, ColorBlendingMode = blending, AlphaCompositionMode = composition }, Color.Transparent, new Shapes.EllipsePolygon(40 * scaleX, 50 * scaleY, 50 * scaleX, 50 * scaleY)) ); @@ -105,7 +106,7 @@ namespace SixLabors.ImageSharp.Tests.Drawing new Rectangle(0 * scaleX, 40, 100 * scaleX, 20 * scaleY))); img.Mutate( x => x.Fill( - new GraphicsOptions(true) { ColorBlendingMode = blending, AlphaCompositionMode = composition }, + new GraphicsOptions { Antialias = true, ColorBlendingMode = blending, AlphaCompositionMode = composition }, Color.HotPink, new Rectangle(20 * scaleX, 0, 30 * scaleX, 100 * scaleY))); @@ -113,7 +114,7 @@ namespace SixLabors.ImageSharp.Tests.Drawing img.Mutate( x => x.Fill( - new GraphicsOptions(true) { ColorBlendingMode = blending, AlphaCompositionMode = composition }, + new GraphicsOptions { Antialias = true, ColorBlendingMode = blending, AlphaCompositionMode = composition }, transparentRed, new Shapes.EllipsePolygon(40 * scaleX, 50 * scaleY, 50 * scaleX, 50 * scaleY)) ); @@ -130,7 +131,7 @@ namespace SixLabors.ImageSharp.Tests.Drawing PixelAlphaCompositionMode composition) where TPixel : struct, IPixel { - using(Image dstImg = provider.GetImage(), srcImg = provider.GetImage()) + using (Image dstImg = provider.GetImage(), srcImg = provider.GetImage()) { int scaleX = (dstImg.Width / 100); int scaleY = (dstImg.Height / 100); @@ -146,13 +147,13 @@ namespace SixLabors.ImageSharp.Tests.Drawing new Shapes.EllipsePolygon(40 * scaleX, 50 * scaleY, 50 * scaleX, 50 * scaleY))); dstImg.Mutate( - x => x.DrawImage(srcImg, new GraphicsOptions(true) { ColorBlendingMode = blending, AlphaCompositionMode = composition }) - ); + x => x.DrawImage(srcImg, new GraphicsOptions { Antialias = true, ColorBlendingMode = blending, AlphaCompositionMode = composition }) + ); VerifyImage(provider, blending, composition, dstImg); } } - + private static void VerifyImage( TestImageProvider provider, PixelColorBlendingMode blending, @@ -165,13 +166,13 @@ namespace SixLabors.ImageSharp.Tests.Drawing new { composition, blending }, appendPixelTypeToFileName: false, appendSourceFileOrDescription: false); - + var comparer = ImageComparer.TolerantPercentage(0.01f, 3); img.CompareFirstFrameToReferenceOutput(comparer, provider, new { composition, blending }, appendPixelTypeToFileName: false, - appendSourceFileOrDescription: false); + appendSourceFileOrDescription: false); } } -} \ No newline at end of file +} diff --git a/tests/ImageSharp.Tests/Drawing/Text/DrawText.cs b/tests/ImageSharp.Tests/Drawing/Text/DrawText.cs index e6866c6579..2a39e18cb6 100644 --- a/tests/ImageSharp.Tests/Drawing/Text/DrawText.cs +++ b/tests/ImageSharp.Tests/Drawing/Text/DrawText.cs @@ -27,7 +27,7 @@ namespace SixLabors.ImageSharp.Tests.Drawing.Text public void FillsForEachACharacterWhenBrushSetAndNotPen() { this.operations.DrawText( - new TextGraphicsOptions(true), + new TextGraphicsOptions { Antialias = true }, "123", this.Font, Brushes.Solid(Color.Red), @@ -48,7 +48,7 @@ namespace SixLabors.ImageSharp.Tests.Drawing.Text [Fact] public void FillsForEachACharacterWhenBrushSet() { - this.operations.DrawText(new TextGraphicsOptions(true), "123", this.Font, Brushes.Solid(Color.Red), Vector2.Zero); + this.operations.DrawText(new TextGraphicsOptions { Antialias = true }, "123", this.Font, Brushes.Solid(Color.Red), Vector2.Zero); this.Verify(0); } @@ -64,7 +64,7 @@ namespace SixLabors.ImageSharp.Tests.Drawing.Text [Fact] public void FillsForEachACharacterWhenColorSet() { - this.operations.DrawText(new TextGraphicsOptions(true), "123", this.Font, Color.Red, Vector2.Zero); + this.operations.DrawText(new TextGraphicsOptions { Antialias = true }, "123", this.Font, Color.Red, Vector2.Zero); var processor = this.Verify(0); @@ -87,7 +87,7 @@ namespace SixLabors.ImageSharp.Tests.Drawing.Text public void DrawForEachACharacterWhenPenSetAndNotBrush() { this.operations.DrawText( - new TextGraphicsOptions(true), + new TextGraphicsOptions { Antialias = true }, "123", this.Font, null, @@ -108,7 +108,7 @@ namespace SixLabors.ImageSharp.Tests.Drawing.Text [Fact] public void DrawForEachACharacterWhenPenSet() { - this.operations.DrawText(new TextGraphicsOptions(true), "123", this.Font, Pens.Dash(Color.Red, 1), Vector2.Zero); + this.operations.DrawText(new TextGraphicsOptions { Antialias = true }, "123", this.Font, Pens.Dash(Color.Red, 1), Vector2.Zero); this.Verify(0); } @@ -132,7 +132,7 @@ namespace SixLabors.ImageSharp.Tests.Drawing.Text public void DrawForEachACharacterWhenPenSetAndFillFroEachWhenBrushSet() { this.operations.DrawText( - new TextGraphicsOptions(true), + new TextGraphicsOptions { Antialias = true }, "123", this.Font, Brushes.Solid(Color.Red), diff --git a/tests/ImageSharp.Tests/Drawing/Text/DrawTextOnImageTests.cs b/tests/ImageSharp.Tests/Drawing/Text/DrawTextOnImageTests.cs index a767a686ed..281a516509 100644 --- a/tests/ImageSharp.Tests/Drawing/Text/DrawTextOnImageTests.cs +++ b/tests/ImageSharp.Tests/Drawing/Text/DrawTextOnImageTests.cs @@ -55,8 +55,9 @@ namespace SixLabors.ImageSharp.Tests.Drawing.Text var scaledFont = new Font(font, scalingFactor * font.Size); var center = new PointF(img.Width / 2, img.Height / 2); - var textGraphicOptions = new TextGraphicsOptions(true) + var textGraphicOptions = new TextGraphicsOptions { + Antialias = true, HorizontalAlignment = HorizontalAlignment.Center, VerticalAlignment = VerticalAlignment.Center }; @@ -222,7 +223,11 @@ namespace SixLabors.ImageSharp.Tests.Drawing.Text string text = Repeat("Beware the Jabberwock, my son! The jaws that bite, the claws that catch! Beware the Jubjub bird, and shun The frumious Bandersnatch!\n", 20); - var textOptions = new TextGraphicsOptions(true) { WrapTextWidth = 1000 }; + var textOptions = new TextGraphicsOptions + { + Antialias = true, + WrapTextWidth = 1000 + }; string details = fontName.Replace(" ", ""); diff --git a/tests/ImageSharp.Tests/Drawing/Text/TextGraphicsOptionsTests.cs b/tests/ImageSharp.Tests/Drawing/Text/TextGraphicsOptionsTests.cs index 0885611c67..d15b717bb6 100644 --- a/tests/ImageSharp.Tests/Drawing/Text/TextGraphicsOptionsTests.cs +++ b/tests/ImageSharp.Tests/Drawing/Text/TextGraphicsOptionsTests.cs @@ -1,6 +1,8 @@ -// Copyright (c) Six Labors and contributors. +// Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. +using SixLabors.Fonts; +using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Processing; using Xunit; @@ -9,16 +11,129 @@ namespace SixLabors.ImageSharp.Tests.Drawing.Text { public class TextGraphicsOptionsTests { + private readonly TextGraphicsOptions newTextGraphicsOptions = new TextGraphicsOptions(); + private readonly TextGraphicsOptions defaultTextGraphicsOptions = TextGraphicsOptions.Default; + private readonly TextGraphicsOptions cloneTextGraphicsOptions = TextGraphicsOptions.Default.Clone(); + + [Fact] + public void DefaultTextGraphicsOptionsIsNotNull() => Assert.True(this.defaultTextGraphicsOptions != null); + + [Fact] + public void DefaultTextGraphicsOptionsAntialias() + { + Assert.True(this.newTextGraphicsOptions.Antialias); + Assert.True(this.defaultTextGraphicsOptions.Antialias); + Assert.True(this.cloneTextGraphicsOptions.Antialias); + } + + [Fact] + public void DefaultTextGraphicsOptionsAntialiasSuppixelDepth() + { + const int Expected = 16; + Assert.Equal(Expected, this.newTextGraphicsOptions.AntialiasSubpixelDepth); + Assert.Equal(Expected, this.defaultTextGraphicsOptions.AntialiasSubpixelDepth); + Assert.Equal(Expected, this.cloneTextGraphicsOptions.AntialiasSubpixelDepth); + } + + [Fact] + public void DefaultTextGraphicsOptionsBlendPercentage() + { + const float Expected = 1F; + Assert.Equal(Expected, this.newTextGraphicsOptions.BlendPercentage); + Assert.Equal(Expected, this.defaultTextGraphicsOptions.BlendPercentage); + Assert.Equal(Expected, this.cloneTextGraphicsOptions.BlendPercentage); + } + + [Fact] + public void DefaultTextGraphicsOptionsColorBlendingMode() + { + const PixelColorBlendingMode Expected = PixelColorBlendingMode.Normal; + Assert.Equal(Expected, this.newTextGraphicsOptions.ColorBlendingMode); + Assert.Equal(Expected, this.defaultTextGraphicsOptions.ColorBlendingMode); + Assert.Equal(Expected, this.cloneTextGraphicsOptions.ColorBlendingMode); + } + + [Fact] + public void DefaultTextGraphicsOptionsAlphaCompositionMode() + { + const PixelAlphaCompositionMode Expected = PixelAlphaCompositionMode.SrcOver; + Assert.Equal(Expected, this.newTextGraphicsOptions.AlphaCompositionMode); + Assert.Equal(Expected, this.defaultTextGraphicsOptions.AlphaCompositionMode); + Assert.Equal(Expected, this.cloneTextGraphicsOptions.AlphaCompositionMode); + } + + [Fact] + public void DefaultTextGraphicsOptionsApplyKerning() + { + const bool Expected = true; + Assert.Equal(Expected, this.newTextGraphicsOptions.ApplyKerning); + Assert.Equal(Expected, this.defaultTextGraphicsOptions.ApplyKerning); + Assert.Equal(Expected, this.cloneTextGraphicsOptions.ApplyKerning); + } + + [Fact] + public void DefaultTextGraphicsOptionsHorizontalAlignment() + { + const HorizontalAlignment Expected = HorizontalAlignment.Left; + Assert.Equal(Expected, this.newTextGraphicsOptions.HorizontalAlignment); + Assert.Equal(Expected, this.defaultTextGraphicsOptions.HorizontalAlignment); + Assert.Equal(Expected, this.cloneTextGraphicsOptions.HorizontalAlignment); + } + + [Fact] + public void DefaultTextGraphicsOptionsVerticalAlignment() + { + const VerticalAlignment Expected = VerticalAlignment.Top; + Assert.Equal(Expected, this.newTextGraphicsOptions.VerticalAlignment); + Assert.Equal(Expected, this.defaultTextGraphicsOptions.VerticalAlignment); + Assert.Equal(Expected, this.cloneTextGraphicsOptions.VerticalAlignment); + } + + [Fact] + public void DefaultTextGraphicsOptionsDpiX() + { + const float Expected = 72F; + Assert.Equal(Expected, this.newTextGraphicsOptions.DpiX); + Assert.Equal(Expected, this.defaultTextGraphicsOptions.DpiX); + Assert.Equal(Expected, this.cloneTextGraphicsOptions.DpiX); + } + + [Fact] + public void DefaultTextGraphicsOptionsDpiY() + { + const float Expected = 72F; + Assert.Equal(Expected, this.newTextGraphicsOptions.DpiY); + Assert.Equal(Expected, this.defaultTextGraphicsOptions.DpiY); + Assert.Equal(Expected, this.cloneTextGraphicsOptions.DpiY); + } + + [Fact] + public void DefaultTextGraphicsOptionsTabWidth() + { + const float Expected = 4F; + Assert.Equal(Expected, this.newTextGraphicsOptions.TabWidth); + Assert.Equal(Expected, this.defaultTextGraphicsOptions.TabWidth); + Assert.Equal(Expected, this.cloneTextGraphicsOptions.TabWidth); + } + + [Fact] + public void DefaultTextGraphicsOptionsWrapTextWidth() + { + const float Expected = 0F; + Assert.Equal(Expected, this.newTextGraphicsOptions.WrapTextWidth); + Assert.Equal(Expected, this.defaultTextGraphicsOptions.WrapTextWidth); + Assert.Equal(Expected, this.cloneTextGraphicsOptions.WrapTextWidth); + } + [Fact] public void ExplicitCastOfGraphicsOptions() { - var opt = new GraphicsOptions(false) + TextGraphicsOptions textOptions = new GraphicsOptions { + Antialias = false, AntialiasSubpixelDepth = 99 }; - TextGraphicsOptions textOptions = opt; - Assert.False(textOptions.Antialias); Assert.Equal(99, textOptions.AntialiasSubpixelDepth); } @@ -26,8 +141,9 @@ namespace SixLabors.ImageSharp.Tests.Drawing.Text [Fact] public void ImplicitCastToGraphicsOptions() { - var textOptions = new TextGraphicsOptions(false) + var textOptions = new TextGraphicsOptions { + Antialias = false, AntialiasSubpixelDepth = 99 }; @@ -37,4 +153,4 @@ namespace SixLabors.ImageSharp.Tests.Drawing.Text Assert.Equal(99, opt.AntialiasSubpixelDepth); } } -} \ No newline at end of file +} diff --git a/tests/ImageSharp.Tests/GraphicsOptionsTests.cs b/tests/ImageSharp.Tests/GraphicsOptionsTests.cs index 6ff38626d6..4e23b17663 100644 --- a/tests/ImageSharp.Tests/GraphicsOptionsTests.cs +++ b/tests/ImageSharp.Tests/GraphicsOptionsTests.cs @@ -1,4 +1,4 @@ -// Copyright (c) Six Labors and contributors. +// Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. using SixLabors.ImageSharp.PixelFormats; @@ -8,14 +8,65 @@ namespace SixLabors.ImageSharp.Tests { public class GraphicsOptionsTests { + private readonly GraphicsOptions newGraphicsOptions = new GraphicsOptions(); + private readonly GraphicsOptions defaultGraphicsOptions = GraphicsOptions.Default; + private readonly GraphicsOptions cloneGraphicsOptions = GraphicsOptions.Default.Clone(); + + [Fact] + public void DefaultGraphicsOptionsIsNotNull() => Assert.True(this.defaultGraphicsOptions != null); + + [Fact] + public void DefaultGraphicsOptionsAntialias() + { + Assert.True(this.newGraphicsOptions.Antialias); + Assert.True(this.defaultGraphicsOptions.Antialias); + Assert.True(this.cloneGraphicsOptions.Antialias); + } + + [Fact] + public void DefaultGraphicsOptionsAntialiasSuppixelDepth() + { + const int Expected = 16; + Assert.Equal(Expected, this.newGraphicsOptions.AntialiasSubpixelDepth); + Assert.Equal(Expected, this.defaultGraphicsOptions.AntialiasSubpixelDepth); + Assert.Equal(Expected, this.cloneGraphicsOptions.AntialiasSubpixelDepth); + } + + [Fact] + public void DefaultGraphicsOptionsBlendPercentage() + { + const float Expected = 1F; + Assert.Equal(Expected, this.newGraphicsOptions.BlendPercentage); + Assert.Equal(Expected, this.defaultGraphicsOptions.BlendPercentage); + Assert.Equal(Expected, this.cloneGraphicsOptions.BlendPercentage); + } + + [Fact] + public void DefaultGraphicsOptionsColorBlendingMode() + { + const PixelColorBlendingMode Expected = PixelColorBlendingMode.Normal; + Assert.Equal(Expected, this.newGraphicsOptions.ColorBlendingMode); + Assert.Equal(Expected, this.defaultGraphicsOptions.ColorBlendingMode); + Assert.Equal(Expected, this.cloneGraphicsOptions.ColorBlendingMode); + } + + [Fact] + public void DefaultGraphicsOptionsAlphaCompositionMode() + { + const PixelAlphaCompositionMode Expected = PixelAlphaCompositionMode.SrcOver; + Assert.Equal(Expected, this.newGraphicsOptions.AlphaCompositionMode); + Assert.Equal(Expected, this.defaultGraphicsOptions.AlphaCompositionMode); + Assert.Equal(Expected, this.cloneGraphicsOptions.AlphaCompositionMode); + } + [Fact] public void IsOpaqueColor() { - Assert.True(new GraphicsOptions(true).IsOpaqueColorWithoutBlending(Rgba32.Red)); - Assert.False(new GraphicsOptions(true, 0.5f).IsOpaqueColorWithoutBlending(Rgba32.Red)); - Assert.False(new GraphicsOptions(true).IsOpaqueColorWithoutBlending(Rgba32.Transparent)); - Assert.False(new GraphicsOptions(true, PixelColorBlendingMode.Lighten, 1).IsOpaqueColorWithoutBlending(Rgba32.Red)); - Assert.False(new GraphicsOptions(true, PixelColorBlendingMode.Normal, PixelAlphaCompositionMode.DestOver, 1).IsOpaqueColorWithoutBlending(Rgba32.Red)); + Assert.True(new GraphicsOptions().IsOpaqueColorWithoutBlending(Rgba32.Red)); + Assert.False(new GraphicsOptions { BlendPercentage = .5F }.IsOpaqueColorWithoutBlending(Rgba32.Red)); + Assert.False(new GraphicsOptions().IsOpaqueColorWithoutBlending(Rgba32.Transparent)); + Assert.False(new GraphicsOptions { ColorBlendingMode = PixelColorBlendingMode.Lighten, BlendPercentage = 1F }.IsOpaqueColorWithoutBlending(Rgba32.Red)); + Assert.False(new GraphicsOptions { ColorBlendingMode = PixelColorBlendingMode.Normal, AlphaCompositionMode = PixelAlphaCompositionMode.DestOver, BlendPercentage = 1f }.IsOpaqueColorWithoutBlending(Rgba32.Red)); } } -} \ No newline at end of file +} diff --git a/tests/ImageSharp.Tests/Issues/Issue412.cs b/tests/ImageSharp.Tests/Issues/Issue412.cs index 5a590018e5..53c65b643a 100644 --- a/tests/ImageSharp.Tests/Issues/Issue412.cs +++ b/tests/ImageSharp.Tests/Issues/Issue412.cs @@ -20,14 +20,14 @@ namespace SixLabors.ImageSharp.Tests.Issues for (var i = 0; i < 40; ++i) { context.DrawLines( - new GraphicsOptions(false), + new GraphicsOptions { Antialias = false }, Color.Black, 1, new PointF(i, 0.1066f), new PointF(i, 10.1066f)); context.DrawLines( - new GraphicsOptions(false), + new GraphicsOptions { Antialias = false }, Color.Red, 1, new PointF(i, 15.1066f), diff --git a/tests/ImageSharp.Tests/Processing/BaseImageOperationsExtensionTest.cs b/tests/ImageSharp.Tests/Processing/BaseImageOperationsExtensionTest.cs index 140af9563a..cfac8645ff 100644 --- a/tests/ImageSharp.Tests/Processing/BaseImageOperationsExtensionTest.cs +++ b/tests/ImageSharp.Tests/Processing/BaseImageOperationsExtensionTest.cs @@ -21,7 +21,7 @@ namespace SixLabors.ImageSharp.Tests.Processing public BaseImageOperationsExtensionTest() { - this.options = new GraphicsOptions(false); + this.options = new GraphicsOptions { Antialias = false }; this.source = new Image(91 + 324, 123 + 56); this.rect = new Rectangle(91, 123, 324, 56); // make this random? this.internalOperations = new FakeImageOperationsProvider.FakeImageOperations(this.source, false); From c657747382df87aea7ec02bec45ed2b114965553 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Sun, 10 Nov 2019 20:29:03 +1100 Subject: [PATCH 50/75] Update GraphicsOptions.cs --- src/ImageSharp/GraphicsOptions.cs | 103 +++++++++--------------------- 1 file changed, 29 insertions(+), 74 deletions(-) diff --git a/src/ImageSharp/GraphicsOptions.cs b/src/ImageSharp/GraphicsOptions.cs index 1e18edb4ae..1673057b86 100644 --- a/src/ImageSharp/GraphicsOptions.cs +++ b/src/ImageSharp/GraphicsOptions.cs @@ -11,83 +11,23 @@ namespace SixLabors.ImageSharp /// public class GraphicsOptions { - private static readonly Lazy Lazy = new Lazy(); - private int antialiasSubpixelDepth; - private float blendPercentage; - - /// - /// Initializes a new instance of the class. - /// - public GraphicsOptions() - : this(true) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// If set to true [enable antialiasing]. - public GraphicsOptions(bool enableAntialiasing) - : this(enableAntialiasing, 1F) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// If set to true [enable antialiasing]. - /// The blending percentage to apply to the drawing operation - public GraphicsOptions(bool enableAntialiasing, float blendPercentage) - : this(enableAntialiasing, PixelColorBlendingMode.Normal, blendPercentage) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// If set to true [enable antialiasing]. - /// The color blending mode to apply to the drawing operation - /// The blending percentage to apply to the drawing operation - public GraphicsOptions( - bool enableAntialiasing, - PixelColorBlendingMode blending, - float blendPercentage) - : this(enableAntialiasing, blending, PixelAlphaCompositionMode.SrcOver, blendPercentage) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// If set to true [enable antialiasing]. - /// The color blending mode to apply to the drawing operation - /// The alpha composition mode to apply to the drawing operation - /// The blending percentage to apply to the drawing operation - public GraphicsOptions( - bool enableAntialiasing, - PixelColorBlendingMode blending, - PixelAlphaCompositionMode composition, - float blendPercentage) - { - this.ColorBlendingMode = blending; - this.AlphaCompositionMode = composition; - this.BlendPercentage = blendPercentage; - this.AntialiasSubpixelDepth = 16; - this.Antialias = enableAntialiasing; - } + private int antialiasSubpixelDepth = 16; + private float blendPercentage = 1F; /// /// Gets the default instance. /// - public static GraphicsOptions Default { get; } = Lazy.Value; + public static GraphicsOptions Default { get; } = new GraphicsOptions(); /// /// Gets or sets a value indicating whether antialiasing should be applied. + /// Defaults to true. /// - public bool Antialias { get; set; } + public bool Antialias { get; set; } = true; /// /// Gets or sets a value indicating the number of subpixels to use while rendering with antialiasing enabled. + /// Defaults to 16. /// public int AntialiasSubpixelDepth { @@ -104,7 +44,8 @@ namespace SixLabors.ImageSharp } /// - /// Gets or sets a value indicating the blending percentage to apply to the drawing operation. + /// Gets or sets a value between indicating the blending percentage to apply to the drawing operation. + /// Range 0..1; Defaults to 1. /// public float BlendPercentage { @@ -120,18 +61,32 @@ namespace SixLabors.ImageSharp } } - // In the future we could expose a PixelBlender directly on here - // or some forms of PixelBlender factory for each pixel type. Will need - // some API thought post V1. - /// - /// Gets or sets a value indicating the color blending mode to apply to the drawing operation + /// Gets or sets a value indicating the color blending mode to apply to the drawing operation. + /// Defaults to . /// - public PixelColorBlendingMode ColorBlendingMode { get; set; } + public PixelColorBlendingMode ColorBlendingMode { get; set; } = PixelColorBlendingMode.Normal; /// /// Gets or sets a value indicating the alpha composition mode to apply to the drawing operation + /// Defaults to . + /// + public PixelAlphaCompositionMode AlphaCompositionMode { get; set; } = PixelAlphaCompositionMode.SrcOver; + + /// + /// Creates a shallow copy of the . /// - public PixelAlphaCompositionMode AlphaCompositionMode { get; set; } + /// A new options instance. + public GraphicsOptions Clone() + { + return new GraphicsOptions + { + AlphaCompositionMode = this.AlphaCompositionMode, + Antialias = this.Antialias, + AntialiasSubpixelDepth = this.AntialiasSubpixelDepth, + BlendPercentage = this.BlendPercentage, + ColorBlendingMode = this.ColorBlendingMode + }; + } } } From 241c50a79e2747f68c50801113c96e9bfdc026a7 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Mon, 11 Nov 2019 12:12:18 +1100 Subject: [PATCH 51/75] Drop Default, use DeepClone + tests --- .../Extensions/DrawImageExtensions.cs | 12 +-- .../DrawPathCollectionExtensions.cs | 2 +- .../Extensions/DrawPathExtensions.cs | 2 +- .../Extensions/DrawPolygonExtensions.cs | 2 +- .../Extensions/DrawRectangleExtensions.cs | 2 +- .../Extensions/DrawTextExtensions.cs | 8 +- .../Extensions/FillPathBuilderExtensions.cs | 2 +- .../FillPathCollectionExtensions.cs | 2 +- .../Extensions/FillPathExtensions.cs | 2 +- .../Extensions/FillRegionExtensions.cs | 4 +- .../Processing/TextGraphicsOptions.cs | 50 +++++------ src/ImageSharp/GraphicsOptions.cs | 36 ++++---- .../Extensions/BackgroundColorExtensions.cs | 8 +- .../Processing/Extensions/GlowExtensions.cs | 10 +-- .../Extensions/VignetteExtensions.cs | 10 +-- .../Processors/Overlays/GlowProcessor.cs | 2 +- .../Processors/Overlays/VignetteProcessor.cs | 2 +- .../Drawing/DrawImageTests.cs | 2 +- .../Drawing/Paths/DrawPathCollection.cs | 17 ++-- .../Drawing/Paths/FillPath.cs | 17 ++-- .../Drawing/Paths/FillPathCollection.cs | 17 ++-- .../Drawing/Paths/FillPolygon.cs | 17 ++-- .../Drawing/Paths/FillRectangle.cs | 24 +++--- .../Drawing/Text/TextGraphicsOptionsTests.cs | 85 +++++++++++++++---- .../ImageSharp.Tests/GraphicsOptionsTests.cs | 44 ++++++++-- .../Processing/Effects/BackgroundColorTest.cs | 24 +++--- .../Processing/Overlays/GlowTest.cs | 23 ++--- .../Processing/Overlays/VignetteTest.cs | 23 ++--- .../TestUtilities/GraphicsOptionsComparer.cs | 21 +++++ 29 files changed, 300 insertions(+), 170 deletions(-) create mode 100644 tests/ImageSharp.Tests/TestUtilities/GraphicsOptionsComparer.cs diff --git a/src/ImageSharp.Drawing/Processing/Extensions/DrawImageExtensions.cs b/src/ImageSharp.Drawing/Processing/Extensions/DrawImageExtensions.cs index 981cf1bef4..6c694ab73b 100644 --- a/src/ImageSharp.Drawing/Processing/Extensions/DrawImageExtensions.cs +++ b/src/ImageSharp.Drawing/Processing/Extensions/DrawImageExtensions.cs @@ -27,8 +27,8 @@ namespace SixLabors.ImageSharp.Processing new DrawImageProcessor( image, Point.Empty, - GraphicsOptions.Default.ColorBlendingMode, - GraphicsOptions.Default.AlphaCompositionMode, + new GraphicsOptions().ColorBlendingMode, + new GraphicsOptions().AlphaCompositionMode, opacity)); /// @@ -49,7 +49,7 @@ namespace SixLabors.ImageSharp.Processing image, Point.Empty, colorBlending, - GraphicsOptions.Default.AlphaCompositionMode, + new GraphicsOptions().AlphaCompositionMode, opacity)); /// @@ -105,8 +105,8 @@ namespace SixLabors.ImageSharp.Processing new DrawImageProcessor( image, location, - GraphicsOptions.Default.ColorBlendingMode, - GraphicsOptions.Default.AlphaCompositionMode, + new GraphicsOptions().ColorBlendingMode, + new GraphicsOptions().AlphaCompositionMode, opacity)); /// @@ -129,7 +129,7 @@ namespace SixLabors.ImageSharp.Processing image, location, colorBlending, - GraphicsOptions.Default.AlphaCompositionMode, + new GraphicsOptions().AlphaCompositionMode, opacity)); /// diff --git a/src/ImageSharp.Drawing/Processing/Extensions/DrawPathCollectionExtensions.cs b/src/ImageSharp.Drawing/Processing/Extensions/DrawPathCollectionExtensions.cs index a68b69a444..90b8c68ac2 100644 --- a/src/ImageSharp.Drawing/Processing/Extensions/DrawPathCollectionExtensions.cs +++ b/src/ImageSharp.Drawing/Processing/Extensions/DrawPathCollectionExtensions.cs @@ -41,7 +41,7 @@ namespace SixLabors.ImageSharp.Processing /// The . public static IImageProcessingContext Draw(this IImageProcessingContext source, IPen pen, IPathCollection paths) => - source.Draw(GraphicsOptions.Default, pen, paths); + source.Draw(new GraphicsOptions(), pen, paths); /// /// Draws the outline of the polygon with the provided brush at the provided thickness. diff --git a/src/ImageSharp.Drawing/Processing/Extensions/DrawPathExtensions.cs b/src/ImageSharp.Drawing/Processing/Extensions/DrawPathExtensions.cs index dfe30f6a3c..822375ca97 100644 --- a/src/ImageSharp.Drawing/Processing/Extensions/DrawPathExtensions.cs +++ b/src/ImageSharp.Drawing/Processing/Extensions/DrawPathExtensions.cs @@ -34,7 +34,7 @@ namespace SixLabors.ImageSharp.Processing /// The path. /// The . public static IImageProcessingContext Draw(this IImageProcessingContext source, IPen pen, IPath path) => - source.Draw(GraphicsOptions.Default, pen, path); + source.Draw(new GraphicsOptions(), pen, path); /// /// Draws the outline of the polygon with the provided brush at the provided thickness. diff --git a/src/ImageSharp.Drawing/Processing/Extensions/DrawPolygonExtensions.cs b/src/ImageSharp.Drawing/Processing/Extensions/DrawPolygonExtensions.cs index 86d8e9e2e2..d51e586452 100644 --- a/src/ImageSharp.Drawing/Processing/Extensions/DrawPolygonExtensions.cs +++ b/src/ImageSharp.Drawing/Processing/Extensions/DrawPolygonExtensions.cs @@ -86,7 +86,7 @@ namespace SixLabors.ImageSharp.Processing this IImageProcessingContext source, IPen pen, params PointF[] points) => - source.Draw(GraphicsOptions.Default, pen, new Polygon(new LinearLineSegment(points))); + source.Draw(new GraphicsOptions(), pen, new Polygon(new LinearLineSegment(points))); /// /// Draws the provided Points as a closed Linear Polygon with the provided Pen. diff --git a/src/ImageSharp.Drawing/Processing/Extensions/DrawRectangleExtensions.cs b/src/ImageSharp.Drawing/Processing/Extensions/DrawRectangleExtensions.cs index da78ab2ecc..b3b5dd76a5 100644 --- a/src/ImageSharp.Drawing/Processing/Extensions/DrawRectangleExtensions.cs +++ b/src/ImageSharp.Drawing/Processing/Extensions/DrawRectangleExtensions.cs @@ -34,7 +34,7 @@ namespace SixLabors.ImageSharp.Processing /// The shape. /// The . public static IImageProcessingContext Draw(this IImageProcessingContext source, IPen pen, RectangleF shape) => - source.Draw(GraphicsOptions.Default, pen, shape); + source.Draw(new GraphicsOptions(), pen, shape); /// /// Draws the outline of the rectangle with the provided brush at the provided thickness. diff --git a/src/ImageSharp.Drawing/Processing/Extensions/DrawTextExtensions.cs b/src/ImageSharp.Drawing/Processing/Extensions/DrawTextExtensions.cs index 05cd3a1ae6..82dbb8d97e 100644 --- a/src/ImageSharp.Drawing/Processing/Extensions/DrawTextExtensions.cs +++ b/src/ImageSharp.Drawing/Processing/Extensions/DrawTextExtensions.cs @@ -29,7 +29,7 @@ namespace SixLabors.ImageSharp.Processing Font font, Color color, PointF location) => - source.DrawText(TextGraphicsOptions.Default, text, font, color, location); + source.DrawText(new TextGraphicsOptions(), text, font, color, location); /// /// Draws the text onto the the image filled via the brush. @@ -69,7 +69,7 @@ namespace SixLabors.ImageSharp.Processing Font font, IBrush brush, PointF location) => - source.DrawText(TextGraphicsOptions.Default, text, font, brush, location); + source.DrawText(new TextGraphicsOptions(), text, font, brush, location); /// /// Draws the text onto the the image filled via the brush. @@ -109,7 +109,7 @@ namespace SixLabors.ImageSharp.Processing Font font, IPen pen, PointF location) => - source.DrawText(TextGraphicsOptions.Default, text, font, pen, location); + source.DrawText(new TextGraphicsOptions(), text, font, pen, location); /// /// Draws the text onto the the image outlined via the pen. @@ -151,7 +151,7 @@ namespace SixLabors.ImageSharp.Processing IBrush brush, IPen pen, PointF location) => - source.DrawText(TextGraphicsOptions.Default, text, font, brush, pen, location); + source.DrawText(new TextGraphicsOptions(), text, font, brush, pen, location); /// /// Draws the text using the default resolution of 72dpi onto the the image filled via the brush then outlined via the pen. diff --git a/src/ImageSharp.Drawing/Processing/Extensions/FillPathBuilderExtensions.cs b/src/ImageSharp.Drawing/Processing/Extensions/FillPathBuilderExtensions.cs index 5de9c6d4ed..030fe6ff1f 100644 --- a/src/ImageSharp.Drawing/Processing/Extensions/FillPathBuilderExtensions.cs +++ b/src/ImageSharp.Drawing/Processing/Extensions/FillPathBuilderExtensions.cs @@ -43,7 +43,7 @@ namespace SixLabors.ImageSharp.Processing this IImageProcessingContext source, IBrush brush, Action path) => - source.Fill(GraphicsOptions.Default, brush, path); + source.Fill(new GraphicsOptions(), brush, path); /// /// Flood fills the image in the shape of the provided polygon with the specified brush. diff --git a/src/ImageSharp.Drawing/Processing/Extensions/FillPathCollectionExtensions.cs b/src/ImageSharp.Drawing/Processing/Extensions/FillPathCollectionExtensions.cs index 776e1f7e4e..5d8aaf3071 100644 --- a/src/ImageSharp.Drawing/Processing/Extensions/FillPathCollectionExtensions.cs +++ b/src/ImageSharp.Drawing/Processing/Extensions/FillPathCollectionExtensions.cs @@ -43,7 +43,7 @@ namespace SixLabors.ImageSharp.Processing this IImageProcessingContext source, IBrush brush, IPathCollection paths) => - source.Fill(GraphicsOptions.Default, brush, paths); + source.Fill(new GraphicsOptions(), brush, paths); /// /// Flood fills the image in the shape of the provided polygon with the specified brush. diff --git a/src/ImageSharp.Drawing/Processing/Extensions/FillPathExtensions.cs b/src/ImageSharp.Drawing/Processing/Extensions/FillPathExtensions.cs index 718016a9e6..4d262aa5fb 100644 --- a/src/ImageSharp.Drawing/Processing/Extensions/FillPathExtensions.cs +++ b/src/ImageSharp.Drawing/Processing/Extensions/FillPathExtensions.cs @@ -34,7 +34,7 @@ namespace SixLabors.ImageSharp.Processing /// The path. /// The . public static IImageProcessingContext Fill(this IImageProcessingContext source, IBrush brush, IPath path) => - source.Fill(GraphicsOptions.Default, brush, new ShapeRegion(path)); + source.Fill(new GraphicsOptions(), brush, new ShapeRegion(path)); /// /// Flood fills the image in the shape of the provided polygon with the specified brush.. diff --git a/src/ImageSharp.Drawing/Processing/Extensions/FillRegionExtensions.cs b/src/ImageSharp.Drawing/Processing/Extensions/FillRegionExtensions.cs index 294e575140..f5dd76318c 100644 --- a/src/ImageSharp.Drawing/Processing/Extensions/FillRegionExtensions.cs +++ b/src/ImageSharp.Drawing/Processing/Extensions/FillRegionExtensions.cs @@ -18,7 +18,7 @@ namespace SixLabors.ImageSharp.Processing /// The details how to fill the region of interest. /// The . public static IImageProcessingContext Fill(this IImageProcessingContext source, IBrush brush) => - source.Fill(GraphicsOptions.Default, brush); + source.Fill(new GraphicsOptions(), brush); /// /// Flood fills the image with the specified color. @@ -37,7 +37,7 @@ namespace SixLabors.ImageSharp.Processing /// The region. /// The . public static IImageProcessingContext Fill(this IImageProcessingContext source, IBrush brush, Region region) => - source.Fill(GraphicsOptions.Default, brush, region); + source.Fill(new GraphicsOptions(), brush, region); /// /// Flood fills the image with in the region with the specified color. diff --git a/src/ImageSharp.Drawing/Processing/TextGraphicsOptions.cs b/src/ImageSharp.Drawing/Processing/TextGraphicsOptions.cs index 60a48a31fa..185fa1f74c 100644 --- a/src/ImageSharp.Drawing/Processing/TextGraphicsOptions.cs +++ b/src/ImageSharp.Drawing/Processing/TextGraphicsOptions.cs @@ -1,7 +1,6 @@ // Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. -using System; using SixLabors.Fonts; using SixLabors.ImageSharp.PixelFormats; @@ -10,7 +9,7 @@ namespace SixLabors.ImageSharp.Processing /// /// Options for influencing the drawing functions. /// - public class TextGraphicsOptions + public class TextGraphicsOptions : IDeepCloneable { private int antialiasSubpixelDepth = 16; private float blendPercentage = 1F; @@ -18,6 +17,29 @@ namespace SixLabors.ImageSharp.Processing private float dpiX = 72F; private float dpiY = 72F; + /// + /// Initializes a new instance of the class. + /// + public TextGraphicsOptions() + { + } + + private TextGraphicsOptions(TextGraphicsOptions source) + { + this.AlphaCompositionMode = source.AlphaCompositionMode; + this.Antialias = source.Antialias; + this.AntialiasSubpixelDepth = source.AntialiasSubpixelDepth; + this.ApplyKerning = source.ApplyKerning; + this.BlendPercentage = source.BlendPercentage; + this.ColorBlendingMode = source.ColorBlendingMode; + this.DpiX = source.DpiX; + this.DpiY = source.DpiY; + this.HorizontalAlignment = source.HorizontalAlignment; + this.TabWidth = source.TabWidth; + this.WrapTextWidth = source.WrapTextWidth; + this.VerticalAlignment = source.VerticalAlignment; + } + /// /// Gets the default instance. /// @@ -194,27 +216,7 @@ namespace SixLabors.ImageSharp.Processing }; } - /// - /// Creates a shallow copy of the . - /// - /// A new options instance. - public TextGraphicsOptions Clone() - { - return new TextGraphicsOptions - { - AlphaCompositionMode = this.AlphaCompositionMode, - Antialias = this.Antialias, - AntialiasSubpixelDepth = this.AntialiasSubpixelDepth, - ApplyKerning = this.ApplyKerning, - BlendPercentage = this.BlendPercentage, - ColorBlendingMode = this.ColorBlendingMode, - DpiX = this.DpiX, - DpiY = this.DpiY, - HorizontalAlignment = this.HorizontalAlignment, - TabWidth = this.TabWidth, - WrapTextWidth = this.WrapTextWidth, - VerticalAlignment = this.VerticalAlignment - }; - } + /// + public TextGraphicsOptions DeepClone() => new TextGraphicsOptions(this); } } diff --git a/src/ImageSharp/GraphicsOptions.cs b/src/ImageSharp/GraphicsOptions.cs index 1673057b86..9c62d23371 100644 --- a/src/ImageSharp/GraphicsOptions.cs +++ b/src/ImageSharp/GraphicsOptions.cs @@ -1,7 +1,6 @@ // Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. -using System; using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp @@ -9,11 +8,27 @@ namespace SixLabors.ImageSharp /// /// Options for influencing the drawing functions. /// - public class GraphicsOptions + public class GraphicsOptions : IDeepCloneable { private int antialiasSubpixelDepth = 16; private float blendPercentage = 1F; + /// + /// Initializes a new instance of the class. + /// + public GraphicsOptions() + { + } + + private GraphicsOptions(GraphicsOptions source) + { + this.AlphaCompositionMode = source.AlphaCompositionMode; + this.Antialias = source.Antialias; + this.AntialiasSubpixelDepth = source.AntialiasSubpixelDepth; + this.BlendPercentage = source.BlendPercentage; + this.ColorBlendingMode = source.ColorBlendingMode; + } + /// /// Gets the default instance. /// @@ -73,20 +88,7 @@ namespace SixLabors.ImageSharp /// public PixelAlphaCompositionMode AlphaCompositionMode { get; set; } = PixelAlphaCompositionMode.SrcOver; - /// - /// Creates a shallow copy of the . - /// - /// A new options instance. - public GraphicsOptions Clone() - { - return new GraphicsOptions - { - AlphaCompositionMode = this.AlphaCompositionMode, - Antialias = this.Antialias, - AntialiasSubpixelDepth = this.AntialiasSubpixelDepth, - BlendPercentage = this.BlendPercentage, - ColorBlendingMode = this.ColorBlendingMode - }; - } + /// + public GraphicsOptions DeepClone() => new GraphicsOptions(this); } } diff --git a/src/ImageSharp/Processing/Extensions/BackgroundColorExtensions.cs b/src/ImageSharp/Processing/Extensions/BackgroundColorExtensions.cs index dd1cc1ed24..cc2fb25120 100644 --- a/src/ImageSharp/Processing/Extensions/BackgroundColorExtensions.cs +++ b/src/ImageSharp/Processing/Extensions/BackgroundColorExtensions.cs @@ -1,4 +1,4 @@ -// Copyright (c) Six Labors and contributors. +// Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. using SixLabors.ImageSharp.Processing.Processors.Overlays; @@ -19,7 +19,7 @@ namespace SixLabors.ImageSharp.Processing /// The color to set as the background. /// The to allow chaining of operations. public static IImageProcessingContext BackgroundColor(this IImageProcessingContext source, Color color) => - BackgroundColor(source, GraphicsOptions.Default, color); + BackgroundColor(source, new GraphicsOptions(), color); /// /// Replaces the background color of image with the given one. @@ -34,7 +34,7 @@ namespace SixLabors.ImageSharp.Processing this IImageProcessingContext source, Color color, Rectangle rectangle) => - BackgroundColor(source, GraphicsOptions.Default, color, rectangle); + BackgroundColor(source, new GraphicsOptions(), color, rectangle); /// /// Replaces the background color of image with the given one. @@ -66,4 +66,4 @@ namespace SixLabors.ImageSharp.Processing Rectangle rectangle) => source.ApplyProcessor(new BackgroundColorProcessor(color, options), rectangle); } -} \ No newline at end of file +} diff --git a/src/ImageSharp/Processing/Extensions/GlowExtensions.cs b/src/ImageSharp/Processing/Extensions/GlowExtensions.cs index 39734882b0..90b73794ba 100644 --- a/src/ImageSharp/Processing/Extensions/GlowExtensions.cs +++ b/src/ImageSharp/Processing/Extensions/GlowExtensions.cs @@ -19,7 +19,7 @@ namespace SixLabors.ImageSharp.Processing /// The image this method extends. /// The to allow chaining of operations. public static IImageProcessingContext Glow(this IImageProcessingContext source) => - Glow(source, GraphicsOptions.Default); + Glow(source, new GraphicsOptions()); /// /// Applies a radial glow effect to an image. @@ -29,7 +29,7 @@ namespace SixLabors.ImageSharp.Processing /// The to allow chaining of operations. public static IImageProcessingContext Glow(this IImageProcessingContext source, Color color) { - return Glow(source, GraphicsOptions.Default, color); + return Glow(source, new GraphicsOptions(), color); } /// @@ -39,7 +39,7 @@ namespace SixLabors.ImageSharp.Processing /// The the radius. /// The to allow chaining of operations. public static IImageProcessingContext Glow(this IImageProcessingContext source, float radius) => - Glow(source, GraphicsOptions.Default, radius); + Glow(source, new GraphicsOptions(), radius); /// /// Applies a radial glow effect to an image. @@ -50,7 +50,7 @@ namespace SixLabors.ImageSharp.Processing /// /// The to allow chaining of operations. public static IImageProcessingContext Glow(this IImageProcessingContext source, Rectangle rectangle) => - source.Glow(GraphicsOptions.Default, rectangle); + source.Glow(new GraphicsOptions(), rectangle); /// /// Applies a radial glow effect to an image. @@ -67,7 +67,7 @@ namespace SixLabors.ImageSharp.Processing Color color, float radius, Rectangle rectangle) => - source.Glow(GraphicsOptions.Default, color, ValueSize.Absolute(radius), rectangle); + source.Glow(new GraphicsOptions(), color, ValueSize.Absolute(radius), rectangle); /// /// Applies a radial glow effect to an image. diff --git a/src/ImageSharp/Processing/Extensions/VignetteExtensions.cs b/src/ImageSharp/Processing/Extensions/VignetteExtensions.cs index 74a59d3e13..ffab8e2bf9 100644 --- a/src/ImageSharp/Processing/Extensions/VignetteExtensions.cs +++ b/src/ImageSharp/Processing/Extensions/VignetteExtensions.cs @@ -19,7 +19,7 @@ namespace SixLabors.ImageSharp.Processing /// The image this method extends. /// The to allow chaining of operations. public static IImageProcessingContext Vignette(this IImageProcessingContext source) => - Vignette(source, GraphicsOptions.Default); + Vignette(source, new GraphicsOptions()); /// /// Applies a radial vignette effect to an image. @@ -28,7 +28,7 @@ namespace SixLabors.ImageSharp.Processing /// The color to set as the vignette. /// The to allow chaining of operations. public static IImageProcessingContext Vignette(this IImageProcessingContext source, Color color) => - Vignette(source, GraphicsOptions.Default, color); + Vignette(source, new GraphicsOptions(), color); /// /// Applies a radial vignette effect to an image. @@ -41,7 +41,7 @@ namespace SixLabors.ImageSharp.Processing this IImageProcessingContext source, float radiusX, float radiusY) => - Vignette(source, GraphicsOptions.Default, radiusX, radiusY); + Vignette(source, new GraphicsOptions(), radiusX, radiusY); /// /// Applies a radial vignette effect to an image. @@ -52,7 +52,7 @@ namespace SixLabors.ImageSharp.Processing /// /// The to allow chaining of operations. public static IImageProcessingContext Vignette(this IImageProcessingContext source, Rectangle rectangle) => - Vignette(source, GraphicsOptions.Default, rectangle); + Vignette(source, new GraphicsOptions(), rectangle); /// /// Applies a radial vignette effect to an image. @@ -71,7 +71,7 @@ namespace SixLabors.ImageSharp.Processing float radiusX, float radiusY, Rectangle rectangle) => - source.Vignette(GraphicsOptions.Default, color, radiusX, radiusY, rectangle); + source.Vignette(new GraphicsOptions(), color, radiusX, radiusY, rectangle); /// /// Applies a radial vignette effect to an image. diff --git a/src/ImageSharp/Processing/Processors/Overlays/GlowProcessor.cs b/src/ImageSharp/Processing/Processors/Overlays/GlowProcessor.cs index 0958e3aa9e..1a60b79aca 100644 --- a/src/ImageSharp/Processing/Processors/Overlays/GlowProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Overlays/GlowProcessor.cs @@ -37,7 +37,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Overlays /// The color or the glow. /// The radius of the glow. internal GlowProcessor(Color color, ValueSize radius) - : this(color, radius, GraphicsOptions.Default) + : this(color, radius, new GraphicsOptions()) { } diff --git a/src/ImageSharp/Processing/Processors/Overlays/VignetteProcessor.cs b/src/ImageSharp/Processing/Processors/Overlays/VignetteProcessor.cs index 2365318f3d..e1c836ff56 100644 --- a/src/ImageSharp/Processing/Processors/Overlays/VignetteProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Overlays/VignetteProcessor.cs @@ -17,7 +17,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Overlays /// /// The color of the vignette. public VignetteProcessor(Color color) - : this(color, GraphicsOptions.Default) + : this(color, new GraphicsOptions()) { } diff --git a/tests/ImageSharp.Tests/Drawing/DrawImageTests.cs b/tests/ImageSharp.Tests/Drawing/DrawImageTests.cs index 86c1c28504..61b45729d3 100644 --- a/tests/ImageSharp.Tests/Drawing/DrawImageTests.cs +++ b/tests/ImageSharp.Tests/Drawing/DrawImageTests.cs @@ -190,7 +190,7 @@ namespace SixLabors.ImageSharp.Tests.Drawing void Test() { - background.Mutate(context => context.DrawImage(overlay, new Point(x, y), GraphicsOptions.Default)); + background.Mutate(context => context.DrawImage(overlay, new Point(x, y), new GraphicsOptions())); } } } diff --git a/tests/ImageSharp.Tests/Drawing/Paths/DrawPathCollection.cs b/tests/ImageSharp.Tests/Drawing/Paths/DrawPathCollection.cs index f6d9c7fe5c..36c11035c6 100644 --- a/tests/ImageSharp.Tests/Drawing/Paths/DrawPathCollection.cs +++ b/tests/ImageSharp.Tests/Drawing/Paths/DrawPathCollection.cs @@ -7,6 +7,7 @@ using SixLabors.ImageSharp.Primitives; using SixLabors.ImageSharp.Processing; using SixLabors.ImageSharp.Processing.Processors.Drawing; using SixLabors.ImageSharp.Tests.Processing; +using SixLabors.ImageSharp.Tests.TestUtilities; using SixLabors.Shapes; using Xunit; @@ -14,7 +15,9 @@ namespace SixLabors.ImageSharp.Tests.Drawing.Paths { public class DrawPathCollection : BaseImageOperationsExtensionTest { - GraphicsOptions noneDefault = new GraphicsOptions { Antialias = false }; + private static readonly GraphicsOptionsComparer graphicsOptionsComparer = new GraphicsOptionsComparer(); + + GraphicsOptions nonDefault = new GraphicsOptions { Antialias = false }; Color color = Color.HotPink; Pen pen = Pens.Solid(Rgba32.HotPink, 1); IPath path1 = new Path(new LinearLineSegment(new SixLabors.Primitives.PointF[] { @@ -46,7 +49,7 @@ namespace SixLabors.ImageSharp.Tests.Drawing.Paths { FillRegionProcessor processor = this.Verify(i); - Assert.Equal(GraphicsOptions.Default, processor.Options); + Assert.Equal(new GraphicsOptions(), processor.Options, graphicsOptionsComparer); ShapePath region = Assert.IsType(processor.Region); @@ -60,13 +63,13 @@ namespace SixLabors.ImageSharp.Tests.Drawing.Paths [Fact] public void CorrectlySetsBrushPathOptions() { - this.operations.Draw(this.noneDefault, this.pen, this.pathCollection); + this.operations.Draw(this.nonDefault, this.pen, this.pathCollection); for (int i = 0; i < 2; i++) { FillRegionProcessor processor = this.Verify(i); - Assert.Equal(this.noneDefault, processor.Options); + Assert.Equal(this.nonDefault, processor.Options, graphicsOptionsComparer); ShapePath region = Assert.IsType(processor.Region); Assert.IsType(region.Shape); @@ -84,7 +87,7 @@ namespace SixLabors.ImageSharp.Tests.Drawing.Paths { FillRegionProcessor processor = this.Verify(i); - Assert.Equal(GraphicsOptions.Default, processor.Options); + Assert.Equal(new GraphicsOptions(), processor.Options, graphicsOptionsComparer); ShapePath region = Assert.IsType(processor.Region); Assert.IsType(region.Shape); @@ -97,13 +100,13 @@ namespace SixLabors.ImageSharp.Tests.Drawing.Paths [Fact] public void CorrectlySetsColorPathAndOptions() { - this.operations.Draw(this.noneDefault, this.color, 1, this.pathCollection); + this.operations.Draw(this.nonDefault, this.color, 1, this.pathCollection); for (int i = 0; i < 2; i++) { FillRegionProcessor processor = this.Verify(i); - Assert.Equal(this.noneDefault, processor.Options); + Assert.Equal(this.nonDefault, processor.Options, graphicsOptionsComparer); ShapePath region = Assert.IsType(processor.Region); Assert.IsType(region.Shape); diff --git a/tests/ImageSharp.Tests/Drawing/Paths/FillPath.cs b/tests/ImageSharp.Tests/Drawing/Paths/FillPath.cs index fa1949e673..cea59e15e5 100644 --- a/tests/ImageSharp.Tests/Drawing/Paths/FillPath.cs +++ b/tests/ImageSharp.Tests/Drawing/Paths/FillPath.cs @@ -7,6 +7,7 @@ using SixLabors.ImageSharp.Primitives; using SixLabors.ImageSharp.Processing; using SixLabors.ImageSharp.Processing.Processors.Drawing; using SixLabors.ImageSharp.Tests.Processing; +using SixLabors.ImageSharp.Tests.TestUtilities; using SixLabors.Shapes; using Xunit; @@ -14,7 +15,9 @@ namespace SixLabors.ImageSharp.Tests.Drawing.Paths { public class FillPath : BaseImageOperationsExtensionTest { - GraphicsOptions noneDefault = new GraphicsOptions { Antialias = false }; + private static readonly GraphicsOptionsComparer graphicsOptionsComparer = new GraphicsOptionsComparer(); + + GraphicsOptions nonDefault = new GraphicsOptions { Antialias = false }; Color color = Color.HotPink; SolidBrush brush = Brushes.Solid(Rgba32.HotPink); IPath path = new Path(new LinearLineSegment(new SixLabors.Primitives.PointF[] { @@ -30,7 +33,7 @@ namespace SixLabors.ImageSharp.Tests.Drawing.Paths this.operations.Fill(this.brush, this.path); var processor = this.Verify(); - Assert.Equal(GraphicsOptions.Default, processor.Options); + Assert.Equal(new GraphicsOptions(), processor.Options, graphicsOptionsComparer); ShapeRegion region = Assert.IsType(processor.Region); @@ -44,10 +47,10 @@ namespace SixLabors.ImageSharp.Tests.Drawing.Paths [Fact] public void CorrectlySetsBrushPathOptions() { - this.operations.Fill(this.noneDefault, this.brush, this.path); + this.operations.Fill(this.nonDefault, this.brush, this.path); var processor = this.Verify(); - Assert.Equal(this.noneDefault, processor.Options); + Assert.Equal(this.nonDefault, processor.Options, graphicsOptionsComparer); ShapeRegion region = Assert.IsType(processor.Region); Polygon polygon = Assert.IsType(region.Shape); @@ -62,7 +65,7 @@ namespace SixLabors.ImageSharp.Tests.Drawing.Paths this.operations.Fill(this.color, this.path); var processor = this.Verify(); - Assert.Equal(GraphicsOptions.Default, processor.Options); + Assert.Equal(new GraphicsOptions(), processor.Options, graphicsOptionsComparer); ShapeRegion region = Assert.IsType(processor.Region); Polygon polygon = Assert.IsType(region.Shape); @@ -75,10 +78,10 @@ namespace SixLabors.ImageSharp.Tests.Drawing.Paths [Fact] public void CorrectlySetsColorPathAndOptions() { - this.operations.Fill(this.noneDefault, this.color, this.path); + this.operations.Fill(this.nonDefault, this.color, this.path); var processor = this.Verify(); - Assert.Equal(this.noneDefault, processor.Options); + Assert.Equal(this.nonDefault, processor.Options); ShapeRegion region = Assert.IsType(processor.Region); Polygon polygon = Assert.IsType(region.Shape); diff --git a/tests/ImageSharp.Tests/Drawing/Paths/FillPathCollection.cs b/tests/ImageSharp.Tests/Drawing/Paths/FillPathCollection.cs index 39e2fc2f97..2a9c04a89f 100644 --- a/tests/ImageSharp.Tests/Drawing/Paths/FillPathCollection.cs +++ b/tests/ImageSharp.Tests/Drawing/Paths/FillPathCollection.cs @@ -7,6 +7,7 @@ using SixLabors.ImageSharp.Primitives; using SixLabors.ImageSharp.Processing; using SixLabors.ImageSharp.Processing.Processors.Drawing; using SixLabors.ImageSharp.Tests.Processing; +using SixLabors.ImageSharp.Tests.TestUtilities; using SixLabors.Shapes; using Xunit; @@ -14,7 +15,9 @@ namespace SixLabors.ImageSharp.Tests.Drawing.Paths { public class FillPathCollection : BaseImageOperationsExtensionTest { - GraphicsOptions noneDefault = new GraphicsOptions { Antialias = false }; + private static readonly GraphicsOptionsComparer graphicsOptionsComparer = new GraphicsOptionsComparer(); + + GraphicsOptions nonDefault = new GraphicsOptions { Antialias = false }; Color color = Color.HotPink; SolidBrush brush = Brushes.Solid(Rgba32.HotPink); IPath path1 = new Path(new LinearLineSegment(new SixLabors.Primitives.PointF[] { @@ -46,7 +49,7 @@ namespace SixLabors.ImageSharp.Tests.Drawing.Paths { FillRegionProcessor processor = this.Verify(i); - Assert.Equal(GraphicsOptions.Default, processor.Options); + Assert.Equal(new GraphicsOptions(), processor.Options, graphicsOptionsComparer); ShapeRegion region = Assert.IsType(processor.Region); @@ -61,13 +64,13 @@ namespace SixLabors.ImageSharp.Tests.Drawing.Paths [Fact] public void CorrectlySetsBrushPathOptions() { - this.operations.Fill(this.noneDefault, this.brush, this.pathCollection); + this.operations.Fill(this.nonDefault, this.brush, this.pathCollection); for (int i = 0; i < 2; i++) { FillRegionProcessor processor = this.Verify(i); - Assert.Equal(this.noneDefault, processor.Options); + Assert.Equal(this.nonDefault, processor.Options, graphicsOptionsComparer); ShapeRegion region = Assert.IsType(processor.Region); Polygon polygon = Assert.IsType(region.Shape); @@ -86,7 +89,7 @@ namespace SixLabors.ImageSharp.Tests.Drawing.Paths { FillRegionProcessor processor = this.Verify(i); - Assert.Equal(GraphicsOptions.Default, processor.Options); + Assert.Equal(new GraphicsOptions(), processor.Options, graphicsOptionsComparer); ShapeRegion region = Assert.IsType(processor.Region); Polygon polygon = Assert.IsType(region.Shape); @@ -100,13 +103,13 @@ namespace SixLabors.ImageSharp.Tests.Drawing.Paths [Fact] public void CorrectlySetsColorPathAndOptions() { - this.operations.Fill(this.noneDefault, this.color, this.pathCollection); + this.operations.Fill(this.nonDefault, this.color, this.pathCollection); for (int i = 0; i < 2; i++) { FillRegionProcessor processor = this.Verify(i); - Assert.Equal(this.noneDefault, processor.Options); + Assert.Equal(this.nonDefault, processor.Options, graphicsOptionsComparer); ShapeRegion region = Assert.IsType(processor.Region); Polygon polygon = Assert.IsType(region.Shape); diff --git a/tests/ImageSharp.Tests/Drawing/Paths/FillPolygon.cs b/tests/ImageSharp.Tests/Drawing/Paths/FillPolygon.cs index 03a827a6a9..8dacd1e7f6 100644 --- a/tests/ImageSharp.Tests/Drawing/Paths/FillPolygon.cs +++ b/tests/ImageSharp.Tests/Drawing/Paths/FillPolygon.cs @@ -7,6 +7,7 @@ using SixLabors.ImageSharp.Primitives; using SixLabors.ImageSharp.Processing; using SixLabors.ImageSharp.Processing.Processors.Drawing; using SixLabors.ImageSharp.Tests.Processing; +using SixLabors.ImageSharp.Tests.TestUtilities; using SixLabors.Shapes; using Xunit; @@ -14,7 +15,9 @@ namespace SixLabors.ImageSharp.Tests.Drawing.Paths { public class FillPolygon : BaseImageOperationsExtensionTest { - GraphicsOptions noneDefault = new GraphicsOptions { Antialias = false }; + private static readonly GraphicsOptionsComparer graphicsOptionsComparer = new GraphicsOptionsComparer(); + + GraphicsOptions nonDefault = new GraphicsOptions { Antialias = false }; Color color = Color.HotPink; SolidBrush brush = Brushes.Solid(Rgba32.HotPink); SixLabors.Primitives.PointF[] path = { @@ -32,7 +35,7 @@ namespace SixLabors.ImageSharp.Tests.Drawing.Paths FillRegionProcessor processor = this.Verify(); - Assert.Equal(GraphicsOptions.Default, processor.Options); + Assert.Equal(new GraphicsOptions(), processor.Options, graphicsOptionsComparer); ShapeRegion region = Assert.IsType(processor.Region); Polygon polygon = Assert.IsType(region.Shape); @@ -44,10 +47,10 @@ namespace SixLabors.ImageSharp.Tests.Drawing.Paths [Fact] public void CorrectlySetsBrushPathAndOptions() { - this.operations.FillPolygon(this.noneDefault, this.brush, this.path); + this.operations.FillPolygon(this.nonDefault, this.brush, this.path); FillRegionProcessor processor = this.Verify(); - Assert.Equal(this.noneDefault, processor.Options); + Assert.Equal(this.nonDefault, processor.Options, graphicsOptionsComparer); ShapeRegion region = Assert.IsType(processor.Region); Polygon polygon = Assert.IsType(region.Shape); @@ -63,7 +66,7 @@ namespace SixLabors.ImageSharp.Tests.Drawing.Paths FillRegionProcessor processor = this.Verify(); - Assert.Equal(GraphicsOptions.Default, processor.Options); + Assert.Equal(new GraphicsOptions(), processor.Options, graphicsOptionsComparer); ShapeRegion region = Assert.IsType(processor.Region); Polygon polygon = Assert.IsType(region.Shape); @@ -76,10 +79,10 @@ namespace SixLabors.ImageSharp.Tests.Drawing.Paths [Fact] public void CorrectlySetsColorPathAndOptions() { - this.operations.FillPolygon(this.noneDefault, this.color, this.path); + this.operations.FillPolygon(this.nonDefault, this.color, this.path); FillRegionProcessor processor = this.Verify(); - Assert.Equal(this.noneDefault, processor.Options); + Assert.Equal(this.nonDefault, processor.Options, graphicsOptionsComparer); ShapeRegion region = Assert.IsType(processor.Region); Polygon polygon = Assert.IsType(region.Shape); diff --git a/tests/ImageSharp.Tests/Drawing/Paths/FillRectangle.cs b/tests/ImageSharp.Tests/Drawing/Paths/FillRectangle.cs index cc108fb54e..6b08323b68 100644 --- a/tests/ImageSharp.Tests/Drawing/Paths/FillRectangle.cs +++ b/tests/ImageSharp.Tests/Drawing/Paths/FillRectangle.cs @@ -6,17 +6,19 @@ using SixLabors.ImageSharp.Primitives; using SixLabors.ImageSharp.Processing; using SixLabors.ImageSharp.Processing.Processors.Drawing; using SixLabors.ImageSharp.Tests.Processing; - +using SixLabors.ImageSharp.Tests.TestUtilities; using Xunit; namespace SixLabors.ImageSharp.Tests.Drawing.Paths { public class FillRectangle : BaseImageOperationsExtensionTest { - GraphicsOptions noneDefault = new GraphicsOptions { Antialias = false }; - Color color = Color.HotPink; - SolidBrush brush = Brushes.Solid(Rgba32.HotPink); - SixLabors.Primitives.Rectangle rectangle = new SixLabors.Primitives.Rectangle(10, 10, 77, 76); + private static readonly GraphicsOptionsComparer graphicsOptionsComparer = new GraphicsOptionsComparer(); + + private GraphicsOptions nonDefault = new GraphicsOptions { Antialias = false }; + private Color color = Color.HotPink; + private SolidBrush brush = Brushes.Solid(Rgba32.HotPink); + private SixLabors.Primitives.Rectangle rectangle = new SixLabors.Primitives.Rectangle(10, 10, 77, 76); [Fact] public void CorrectlySetsBrushAndRectangle() @@ -24,7 +26,7 @@ namespace SixLabors.ImageSharp.Tests.Drawing.Paths this.operations.Fill(this.brush, this.rectangle); FillRegionProcessor processor = this.Verify(); - Assert.Equal(GraphicsOptions.Default, processor.Options); + Assert.Equal(new GraphicsOptions(), processor.Options, graphicsOptionsComparer); ShapeRegion region = Assert.IsType(processor.Region); Shapes.RectangularPolygon rect = Assert.IsType(region.Shape); @@ -39,10 +41,10 @@ namespace SixLabors.ImageSharp.Tests.Drawing.Paths [Fact] public void CorrectlySetsBrushRectangleAndOptions() { - this.operations.Fill(this.noneDefault, this.brush, this.rectangle); + this.operations.Fill(this.nonDefault, this.brush, this.rectangle); FillRegionProcessor processor = this.Verify(); - Assert.Equal(this.noneDefault, processor.Options); + Assert.Equal(this.nonDefault, processor.Options, graphicsOptionsComparer); ShapeRegion region = Assert.IsType(processor.Region); Shapes.RectangularPolygon rect = Assert.IsType(region.Shape); @@ -60,7 +62,7 @@ namespace SixLabors.ImageSharp.Tests.Drawing.Paths this.operations.Fill(this.color, this.rectangle); FillRegionProcessor processor = this.Verify(); - Assert.Equal(GraphicsOptions.Default, processor.Options); + Assert.Equal(new GraphicsOptions(), processor.Options, graphicsOptionsComparer); ShapeRegion region = Assert.IsType(processor.Region); Shapes.RectangularPolygon rect = Assert.IsType(region.Shape); @@ -76,10 +78,10 @@ namespace SixLabors.ImageSharp.Tests.Drawing.Paths [Fact] public void CorrectlySetsColorRectangleAndOptions() { - this.operations.Fill(this.noneDefault, this.color, this.rectangle); + this.operations.Fill(this.nonDefault, this.color, this.rectangle); FillRegionProcessor processor = this.Verify(); - Assert.Equal(this.noneDefault, processor.Options); + Assert.Equal(this.nonDefault, processor.Options, graphicsOptionsComparer); ShapeRegion region = Assert.IsType(processor.Region); Shapes.RectangularPolygon rect = Assert.IsType(region.Shape); diff --git a/tests/ImageSharp.Tests/Drawing/Text/TextGraphicsOptionsTests.cs b/tests/ImageSharp.Tests/Drawing/Text/TextGraphicsOptionsTests.cs index d15b717bb6..a59afb271d 100644 --- a/tests/ImageSharp.Tests/Drawing/Text/TextGraphicsOptionsTests.cs +++ b/tests/ImageSharp.Tests/Drawing/Text/TextGraphicsOptionsTests.cs @@ -12,17 +12,15 @@ namespace SixLabors.ImageSharp.Tests.Drawing.Text public class TextGraphicsOptionsTests { private readonly TextGraphicsOptions newTextGraphicsOptions = new TextGraphicsOptions(); - private readonly TextGraphicsOptions defaultTextGraphicsOptions = TextGraphicsOptions.Default; - private readonly TextGraphicsOptions cloneTextGraphicsOptions = TextGraphicsOptions.Default.Clone(); + private readonly TextGraphicsOptions cloneTextGraphicsOptions = new TextGraphicsOptions().DeepClone(); [Fact] - public void DefaultTextGraphicsOptionsIsNotNull() => Assert.True(this.defaultTextGraphicsOptions != null); + public void CloneTextGraphicsOptionsIsNotNull() => Assert.True(this.cloneTextGraphicsOptions != null); [Fact] public void DefaultTextGraphicsOptionsAntialias() { Assert.True(this.newTextGraphicsOptions.Antialias); - Assert.True(this.defaultTextGraphicsOptions.Antialias); Assert.True(this.cloneTextGraphicsOptions.Antialias); } @@ -31,7 +29,6 @@ namespace SixLabors.ImageSharp.Tests.Drawing.Text { const int Expected = 16; Assert.Equal(Expected, this.newTextGraphicsOptions.AntialiasSubpixelDepth); - Assert.Equal(Expected, this.defaultTextGraphicsOptions.AntialiasSubpixelDepth); Assert.Equal(Expected, this.cloneTextGraphicsOptions.AntialiasSubpixelDepth); } @@ -40,7 +37,6 @@ namespace SixLabors.ImageSharp.Tests.Drawing.Text { const float Expected = 1F; Assert.Equal(Expected, this.newTextGraphicsOptions.BlendPercentage); - Assert.Equal(Expected, this.defaultTextGraphicsOptions.BlendPercentage); Assert.Equal(Expected, this.cloneTextGraphicsOptions.BlendPercentage); } @@ -49,7 +45,6 @@ namespace SixLabors.ImageSharp.Tests.Drawing.Text { const PixelColorBlendingMode Expected = PixelColorBlendingMode.Normal; Assert.Equal(Expected, this.newTextGraphicsOptions.ColorBlendingMode); - Assert.Equal(Expected, this.defaultTextGraphicsOptions.ColorBlendingMode); Assert.Equal(Expected, this.cloneTextGraphicsOptions.ColorBlendingMode); } @@ -58,7 +53,6 @@ namespace SixLabors.ImageSharp.Tests.Drawing.Text { const PixelAlphaCompositionMode Expected = PixelAlphaCompositionMode.SrcOver; Assert.Equal(Expected, this.newTextGraphicsOptions.AlphaCompositionMode); - Assert.Equal(Expected, this.defaultTextGraphicsOptions.AlphaCompositionMode); Assert.Equal(Expected, this.cloneTextGraphicsOptions.AlphaCompositionMode); } @@ -67,7 +61,6 @@ namespace SixLabors.ImageSharp.Tests.Drawing.Text { const bool Expected = true; Assert.Equal(Expected, this.newTextGraphicsOptions.ApplyKerning); - Assert.Equal(Expected, this.defaultTextGraphicsOptions.ApplyKerning); Assert.Equal(Expected, this.cloneTextGraphicsOptions.ApplyKerning); } @@ -76,7 +69,6 @@ namespace SixLabors.ImageSharp.Tests.Drawing.Text { const HorizontalAlignment Expected = HorizontalAlignment.Left; Assert.Equal(Expected, this.newTextGraphicsOptions.HorizontalAlignment); - Assert.Equal(Expected, this.defaultTextGraphicsOptions.HorizontalAlignment); Assert.Equal(Expected, this.cloneTextGraphicsOptions.HorizontalAlignment); } @@ -85,7 +77,6 @@ namespace SixLabors.ImageSharp.Tests.Drawing.Text { const VerticalAlignment Expected = VerticalAlignment.Top; Assert.Equal(Expected, this.newTextGraphicsOptions.VerticalAlignment); - Assert.Equal(Expected, this.defaultTextGraphicsOptions.VerticalAlignment); Assert.Equal(Expected, this.cloneTextGraphicsOptions.VerticalAlignment); } @@ -94,7 +85,6 @@ namespace SixLabors.ImageSharp.Tests.Drawing.Text { const float Expected = 72F; Assert.Equal(Expected, this.newTextGraphicsOptions.DpiX); - Assert.Equal(Expected, this.defaultTextGraphicsOptions.DpiX); Assert.Equal(Expected, this.cloneTextGraphicsOptions.DpiX); } @@ -103,7 +93,6 @@ namespace SixLabors.ImageSharp.Tests.Drawing.Text { const float Expected = 72F; Assert.Equal(Expected, this.newTextGraphicsOptions.DpiY); - Assert.Equal(Expected, this.defaultTextGraphicsOptions.DpiY); Assert.Equal(Expected, this.cloneTextGraphicsOptions.DpiY); } @@ -112,7 +101,6 @@ namespace SixLabors.ImageSharp.Tests.Drawing.Text { const float Expected = 4F; Assert.Equal(Expected, this.newTextGraphicsOptions.TabWidth); - Assert.Equal(Expected, this.defaultTextGraphicsOptions.TabWidth); Assert.Equal(Expected, this.cloneTextGraphicsOptions.TabWidth); } @@ -121,10 +109,77 @@ namespace SixLabors.ImageSharp.Tests.Drawing.Text { const float Expected = 0F; Assert.Equal(Expected, this.newTextGraphicsOptions.WrapTextWidth); - Assert.Equal(Expected, this.defaultTextGraphicsOptions.WrapTextWidth); Assert.Equal(Expected, this.cloneTextGraphicsOptions.WrapTextWidth); } + [Fact] + public void NonDefaultClone() + { + var expected = new TextGraphicsOptions + { + AlphaCompositionMode = PixelAlphaCompositionMode.DestAtop, + Antialias = false, + AntialiasSubpixelDepth = 23, + ApplyKerning = false, + BlendPercentage = .25F, + ColorBlendingMode = PixelColorBlendingMode.HardLight, + DpiX = 46F, + DpiY = 52F, + HorizontalAlignment = HorizontalAlignment.Center, + TabWidth = 3F, + VerticalAlignment = VerticalAlignment.Bottom, + WrapTextWidth = 42F + }; + + TextGraphicsOptions actual = expected.DeepClone(); + + Assert.Equal(expected.AlphaCompositionMode, actual.AlphaCompositionMode); + Assert.Equal(expected.Antialias, actual.Antialias); + Assert.Equal(expected.AntialiasSubpixelDepth, actual.AntialiasSubpixelDepth); + Assert.Equal(expected.ApplyKerning, actual.ApplyKerning); + Assert.Equal(expected.BlendPercentage, actual.BlendPercentage); + Assert.Equal(expected.ColorBlendingMode, actual.ColorBlendingMode); + Assert.Equal(expected.DpiX, actual.DpiX); + Assert.Equal(expected.DpiY, actual.DpiY); + Assert.Equal(expected.HorizontalAlignment, actual.HorizontalAlignment); + Assert.Equal(expected.TabWidth, actual.TabWidth); + Assert.Equal(expected.VerticalAlignment, actual.VerticalAlignment); + Assert.Equal(expected.WrapTextWidth, actual.WrapTextWidth); + } + + [Fact] + public void CloneIsDeep() + { + var expected = new TextGraphicsOptions(); + TextGraphicsOptions actual = expected.DeepClone(); + + actual.AlphaCompositionMode = PixelAlphaCompositionMode.DestAtop; + actual.Antialias = false; + actual.AntialiasSubpixelDepth = 23; + actual.ApplyKerning = false; + actual.BlendPercentage = .25F; + actual.ColorBlendingMode = PixelColorBlendingMode.HardLight; + actual.DpiX = 46F; + actual.DpiY = 52F; + actual.HorizontalAlignment = HorizontalAlignment.Center; + actual.TabWidth = 3F; + actual.VerticalAlignment = VerticalAlignment.Bottom; + actual.WrapTextWidth = 42F; + + Assert.NotEqual(expected.AlphaCompositionMode, actual.AlphaCompositionMode); + Assert.NotEqual(expected.Antialias, actual.Antialias); + Assert.NotEqual(expected.AntialiasSubpixelDepth, actual.AntialiasSubpixelDepth); + Assert.NotEqual(expected.ApplyKerning, actual.ApplyKerning); + Assert.NotEqual(expected.BlendPercentage, actual.BlendPercentage); + Assert.NotEqual(expected.ColorBlendingMode, actual.ColorBlendingMode); + Assert.NotEqual(expected.DpiX, actual.DpiX); + Assert.NotEqual(expected.DpiY, actual.DpiY); + Assert.NotEqual(expected.HorizontalAlignment, actual.HorizontalAlignment); + Assert.NotEqual(expected.TabWidth, actual.TabWidth); + Assert.NotEqual(expected.VerticalAlignment, actual.VerticalAlignment); + Assert.NotEqual(expected.WrapTextWidth, actual.WrapTextWidth); + } + [Fact] public void ExplicitCastOfGraphicsOptions() { diff --git a/tests/ImageSharp.Tests/GraphicsOptionsTests.cs b/tests/ImageSharp.Tests/GraphicsOptionsTests.cs index 4e23b17663..69f904f1cb 100644 --- a/tests/ImageSharp.Tests/GraphicsOptionsTests.cs +++ b/tests/ImageSharp.Tests/GraphicsOptionsTests.cs @@ -2,24 +2,24 @@ // Licensed under the Apache License, Version 2.0. using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Tests.TestUtilities; using Xunit; namespace SixLabors.ImageSharp.Tests { public class GraphicsOptionsTests { + private static readonly GraphicsOptionsComparer graphicsOptionsComparer = new GraphicsOptionsComparer(); private readonly GraphicsOptions newGraphicsOptions = new GraphicsOptions(); - private readonly GraphicsOptions defaultGraphicsOptions = GraphicsOptions.Default; - private readonly GraphicsOptions cloneGraphicsOptions = GraphicsOptions.Default.Clone(); + private readonly GraphicsOptions cloneGraphicsOptions = new GraphicsOptions().DeepClone(); [Fact] - public void DefaultGraphicsOptionsIsNotNull() => Assert.True(this.defaultGraphicsOptions != null); + public void CloneGraphicsOptionsIsNotNull() => Assert.True(this.cloneGraphicsOptions != null); [Fact] public void DefaultGraphicsOptionsAntialias() { Assert.True(this.newGraphicsOptions.Antialias); - Assert.True(this.defaultGraphicsOptions.Antialias); Assert.True(this.cloneGraphicsOptions.Antialias); } @@ -28,7 +28,6 @@ namespace SixLabors.ImageSharp.Tests { const int Expected = 16; Assert.Equal(Expected, this.newGraphicsOptions.AntialiasSubpixelDepth); - Assert.Equal(Expected, this.defaultGraphicsOptions.AntialiasSubpixelDepth); Assert.Equal(Expected, this.cloneGraphicsOptions.AntialiasSubpixelDepth); } @@ -37,7 +36,6 @@ namespace SixLabors.ImageSharp.Tests { const float Expected = 1F; Assert.Equal(Expected, this.newGraphicsOptions.BlendPercentage); - Assert.Equal(Expected, this.defaultGraphicsOptions.BlendPercentage); Assert.Equal(Expected, this.cloneGraphicsOptions.BlendPercentage); } @@ -46,7 +44,6 @@ namespace SixLabors.ImageSharp.Tests { const PixelColorBlendingMode Expected = PixelColorBlendingMode.Normal; Assert.Equal(Expected, this.newGraphicsOptions.ColorBlendingMode); - Assert.Equal(Expected, this.defaultGraphicsOptions.ColorBlendingMode); Assert.Equal(Expected, this.cloneGraphicsOptions.ColorBlendingMode); } @@ -55,10 +52,41 @@ namespace SixLabors.ImageSharp.Tests { const PixelAlphaCompositionMode Expected = PixelAlphaCompositionMode.SrcOver; Assert.Equal(Expected, this.newGraphicsOptions.AlphaCompositionMode); - Assert.Equal(Expected, this.defaultGraphicsOptions.AlphaCompositionMode); Assert.Equal(Expected, this.cloneGraphicsOptions.AlphaCompositionMode); } + [Fact] + public void NonDefaultClone() + { + var expected = new GraphicsOptions + { + AlphaCompositionMode = PixelAlphaCompositionMode.DestAtop, + Antialias = false, + AntialiasSubpixelDepth = 23, + BlendPercentage = .25F, + ColorBlendingMode = PixelColorBlendingMode.HardLight, + }; + + GraphicsOptions actual = expected.DeepClone(); + + Assert.Equal(expected, actual, graphicsOptionsComparer); + } + + [Fact] + public void CloneIsDeep() + { + var expected = new GraphicsOptions(); + GraphicsOptions actual = expected.DeepClone(); + + actual.AlphaCompositionMode = PixelAlphaCompositionMode.DestAtop; + actual.Antialias = false; + actual.AntialiasSubpixelDepth = 23; + actual.BlendPercentage = .25F; + actual.ColorBlendingMode = PixelColorBlendingMode.HardLight; + + Assert.NotEqual(expected, actual, graphicsOptionsComparer); + } + [Fact] public void IsOpaqueColor() { diff --git a/tests/ImageSharp.Tests/Processing/Effects/BackgroundColorTest.cs b/tests/ImageSharp.Tests/Processing/Effects/BackgroundColorTest.cs index 1b5bd656dc..a137a9f438 100644 --- a/tests/ImageSharp.Tests/Processing/Effects/BackgroundColorTest.cs +++ b/tests/ImageSharp.Tests/Processing/Effects/BackgroundColorTest.cs @@ -1,22 +1,24 @@ -// Copyright (c) Six Labors and contributors. +// Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. using SixLabors.ImageSharp.Processing; using SixLabors.ImageSharp.Processing.Processors.Overlays; - +using SixLabors.ImageSharp.Tests.TestUtilities; using Xunit; namespace SixLabors.ImageSharp.Tests.Processing.Effects { public class BackgroundColorTest : BaseImageOperationsExtensionTest { + private static readonly GraphicsOptionsComparer graphicsOptionsComparer = new GraphicsOptionsComparer(); + [Fact] public void BackgroundColor_amount_BackgroundColorProcessorDefaultsSet() { this.operations.BackgroundColor(Color.BlanchedAlmond); - var processor = this.Verify(); + BackgroundColorProcessor processor = this.Verify(); - Assert.Equal(GraphicsOptions.Default, processor.GraphicsOptions); + Assert.Equal(new GraphicsOptions(), processor.GraphicsOptions, graphicsOptionsComparer); Assert.Equal(Color.BlanchedAlmond, processor.Color); } @@ -24,9 +26,9 @@ namespace SixLabors.ImageSharp.Tests.Processing.Effects public void BackgroundColor_amount_rect_BackgroundColorProcessorDefaultsSet() { this.operations.BackgroundColor(Color.BlanchedAlmond, this.rect); - var processor = this.Verify(this.rect); + BackgroundColorProcessor processor = this.Verify(this.rect); - Assert.Equal(GraphicsOptions.Default, processor.GraphicsOptions); + Assert.Equal(new GraphicsOptions(), processor.GraphicsOptions, graphicsOptionsComparer); Assert.Equal(Color.BlanchedAlmond, processor.Color); } @@ -34,9 +36,9 @@ namespace SixLabors.ImageSharp.Tests.Processing.Effects public void BackgroundColor_amount_options_BackgroundColorProcessorDefaultsSet() { this.operations.BackgroundColor(this.options, Color.BlanchedAlmond); - var processor = this.Verify(); + BackgroundColorProcessor processor = this.Verify(); - Assert.Equal(this.options, processor.GraphicsOptions); + Assert.Equal(this.options, processor.GraphicsOptions, graphicsOptionsComparer); Assert.Equal(Color.BlanchedAlmond, processor.Color); } @@ -44,10 +46,10 @@ namespace SixLabors.ImageSharp.Tests.Processing.Effects public void BackgroundColor_amount_rect_options_BackgroundColorProcessorDefaultsSet() { this.operations.BackgroundColor(this.options, Color.BlanchedAlmond, this.rect); - var processor = this.Verify(this.rect); + BackgroundColorProcessor processor = this.Verify(this.rect); - Assert.Equal(this.options, processor.GraphicsOptions); + Assert.Equal(this.options, processor.GraphicsOptions, graphicsOptionsComparer); Assert.Equal(Color.BlanchedAlmond, processor.Color); } } -} \ No newline at end of file +} diff --git a/tests/ImageSharp.Tests/Processing/Overlays/GlowTest.cs b/tests/ImageSharp.Tests/Processing/Overlays/GlowTest.cs index 978fd416bc..32c4c6fe74 100644 --- a/tests/ImageSharp.Tests/Processing/Overlays/GlowTest.cs +++ b/tests/ImageSharp.Tests/Processing/Overlays/GlowTest.cs @@ -1,10 +1,11 @@ -// Copyright (c) Six Labors and contributors. +// Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Primitives; using SixLabors.ImageSharp.Processing; using SixLabors.ImageSharp.Processing.Processors.Overlays; +using SixLabors.ImageSharp.Tests.TestUtilities; using SixLabors.Primitives; using Xunit; @@ -12,13 +13,15 @@ namespace SixLabors.ImageSharp.Tests.Processing.Overlays { public class GlowTest : BaseImageOperationsExtensionTest { + private static readonly GraphicsOptionsComparer graphicsOptionsComparer = new GraphicsOptionsComparer(); + [Fact] public void Glow_GlowProcessorWithDefaultValues() { this.operations.Glow(); - var p = this.Verify(); + GlowProcessor p = this.Verify(); - Assert.Equal(GraphicsOptions.Default, p.GraphicsOptions); + Assert.Equal(new GraphicsOptions(), p.GraphicsOptions, graphicsOptionsComparer); Assert.Equal(Color.Black, p.GlowColor); Assert.Equal(ValueSize.PercentageOfWidth(.5f), p.Radius); } @@ -27,9 +30,9 @@ namespace SixLabors.ImageSharp.Tests.Processing.Overlays public void Glow_Color_GlowProcessorWithDefaultValues() { this.operations.Glow(Rgba32.Aquamarine); - var p = this.Verify(); + GlowProcessor p = this.Verify(); - Assert.Equal(GraphicsOptions.Default, p.GraphicsOptions); + Assert.Equal(new GraphicsOptions(), p.GraphicsOptions, graphicsOptionsComparer); Assert.Equal(Color.Aquamarine, p.GlowColor); Assert.Equal(ValueSize.PercentageOfWidth(.5f), p.Radius); } @@ -38,9 +41,9 @@ namespace SixLabors.ImageSharp.Tests.Processing.Overlays public void Glow_Radux_GlowProcessorWithDefaultValues() { this.operations.Glow(3.5f); - var p = this.Verify(); + GlowProcessor p = this.Verify(); - Assert.Equal(GraphicsOptions.Default, p.GraphicsOptions); + Assert.Equal(new GraphicsOptions(), p.GraphicsOptions, graphicsOptionsComparer); Assert.Equal(Color.Black, p.GlowColor); Assert.Equal(ValueSize.Absolute(3.5f), p.Radius); } @@ -50,11 +53,11 @@ namespace SixLabors.ImageSharp.Tests.Processing.Overlays { var rect = new Rectangle(12, 123, 43, 65); this.operations.Glow(rect); - var p = this.Verify(rect); + GlowProcessor p = this.Verify(rect); - Assert.Equal(GraphicsOptions.Default, p.GraphicsOptions); + Assert.Equal(new GraphicsOptions(), p.GraphicsOptions, graphicsOptionsComparer); Assert.Equal(Color.Black, p.GlowColor); Assert.Equal(ValueSize.PercentageOfWidth(.5f), p.Radius); } } -} \ No newline at end of file +} diff --git a/tests/ImageSharp.Tests/Processing/Overlays/VignetteTest.cs b/tests/ImageSharp.Tests/Processing/Overlays/VignetteTest.cs index 2484cf0cb8..ebf4fee317 100644 --- a/tests/ImageSharp.Tests/Processing/Overlays/VignetteTest.cs +++ b/tests/ImageSharp.Tests/Processing/Overlays/VignetteTest.cs @@ -1,9 +1,10 @@ -// Copyright (c) Six Labors and contributors. +// Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. using SixLabors.ImageSharp.Primitives; using SixLabors.ImageSharp.Processing; using SixLabors.ImageSharp.Processing.Processors.Overlays; +using SixLabors.ImageSharp.Tests.TestUtilities; using SixLabors.Primitives; using Xunit; @@ -11,13 +12,15 @@ namespace SixLabors.ImageSharp.Tests.Processing.Overlays { public class VignetteTest : BaseImageOperationsExtensionTest { + private static readonly GraphicsOptionsComparer graphicsOptionsComparer = new GraphicsOptionsComparer(); + [Fact] public void Vignette_VignetteProcessorWithDefaultValues() { this.operations.Vignette(); - var p = this.Verify(); + VignetteProcessor p = this.Verify(); - Assert.Equal(GraphicsOptions.Default, p.GraphicsOptions); + Assert.Equal(new GraphicsOptions(), p.GraphicsOptions, graphicsOptionsComparer); Assert.Equal(Color.Black, p.VignetteColor); Assert.Equal(ValueSize.PercentageOfWidth(.5f), p.RadiusX); Assert.Equal(ValueSize.PercentageOfHeight(.5f), p.RadiusY); @@ -27,9 +30,9 @@ namespace SixLabors.ImageSharp.Tests.Processing.Overlays public void Vignette_Color_VignetteProcessorWithDefaultValues() { this.operations.Vignette(Color.Aquamarine); - var p = this.Verify(); + VignetteProcessor p = this.Verify(); - Assert.Equal(GraphicsOptions.Default, p.GraphicsOptions); + Assert.Equal(new GraphicsOptions(), p.GraphicsOptions, graphicsOptionsComparer); Assert.Equal(Color.Aquamarine, p.VignetteColor); Assert.Equal(ValueSize.PercentageOfWidth(.5f), p.RadiusX); Assert.Equal(ValueSize.PercentageOfHeight(.5f), p.RadiusY); @@ -39,9 +42,9 @@ namespace SixLabors.ImageSharp.Tests.Processing.Overlays public void Vignette_Radux_VignetteProcessorWithDefaultValues() { this.operations.Vignette(3.5f, 12123f); - var p = this.Verify(); + VignetteProcessor p = this.Verify(); - Assert.Equal(GraphicsOptions.Default, p.GraphicsOptions); + Assert.Equal(new GraphicsOptions(), p.GraphicsOptions, graphicsOptionsComparer); Assert.Equal(Color.Black, p.VignetteColor); Assert.Equal(ValueSize.Absolute(3.5f), p.RadiusX); Assert.Equal(ValueSize.Absolute(12123f), p.RadiusY); @@ -52,12 +55,12 @@ namespace SixLabors.ImageSharp.Tests.Processing.Overlays { var rect = new Rectangle(12, 123, 43, 65); this.operations.Vignette(rect); - var p = this.Verify(rect); + VignetteProcessor p = this.Verify(rect); - Assert.Equal(GraphicsOptions.Default, p.GraphicsOptions); + Assert.Equal(new GraphicsOptions(), p.GraphicsOptions, graphicsOptionsComparer); Assert.Equal(Color.Black, p.VignetteColor); Assert.Equal(ValueSize.PercentageOfWidth(.5f), p.RadiusX); Assert.Equal(ValueSize.PercentageOfHeight(.5f), p.RadiusY); } } -} \ No newline at end of file +} diff --git a/tests/ImageSharp.Tests/TestUtilities/GraphicsOptionsComparer.cs b/tests/ImageSharp.Tests/TestUtilities/GraphicsOptionsComparer.cs new file mode 100644 index 0000000000..248755ea36 --- /dev/null +++ b/tests/ImageSharp.Tests/TestUtilities/GraphicsOptionsComparer.cs @@ -0,0 +1,21 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System.Collections.Generic; + +namespace SixLabors.ImageSharp.Tests.TestUtilities +{ + public class GraphicsOptionsComparer : IEqualityComparer + { + public bool Equals(GraphicsOptions x, GraphicsOptions y) + { + return x.AlphaCompositionMode == y.AlphaCompositionMode + && x.Antialias == y.Antialias + && x.AntialiasSubpixelDepth == y.AntialiasSubpixelDepth + && x.BlendPercentage == y.BlendPercentage + && x.ColorBlendingMode == y.ColorBlendingMode; + } + + public int GetHashCode(GraphicsOptions obj) => obj.GetHashCode(); + } +} From 058836d0c9ce5ff84519e540b4afc021220d874d Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Mon, 11 Nov 2019 12:48:09 +1100 Subject: [PATCH 52/75] Normalize GraphicsOptions parameter position --- .../Processing/BrushApplicator.cs | 4 ++-- .../Processing/EllipticGradientBrush.cs | 14 +++++------ .../Extensions/FillRegionExtensions.cs | 8 +++---- .../Processing/GradientBrush.cs | 12 +++++----- src/ImageSharp.Drawing/Processing/IBrush.cs | 6 ++--- .../Processing/ImageBrush.cs | 16 ++++++------- .../Processing/LinearGradientBrush.cs | 24 +++++++++---------- .../Processing/PathGradientBrush.cs | 14 +++++------ .../Processing/PatternBrush.cs | 16 ++++++------- .../Processors/Drawing/FillProcessor.cs | 4 ++-- .../Drawing/FillProcessor{TPixel}.cs | 4 ++-- .../Processors/Drawing/FillRegionProcessor.cs | 4 ++-- .../Drawing/FillRegionProcessor{TPixel}.cs | 2 +- .../Text/DrawTextProcessor{TPixel}.cs | 2 +- .../Processing/RadialGradientBrush.cs | 16 ++++++------- .../Processing/RecolorBrush.cs | 16 ++++++------- .../Processing/SolidBrush.cs | 14 +++++------ .../Extensions/BackgroundColorExtensions.cs | 4 ++-- .../Processing/Extensions/GlowExtensions.cs | 8 +++---- .../Extensions/VignetteExtensions.cs | 8 +++---- .../Overlays/BackgroundColorProcessor.cs | 4 ++-- .../Processors/Overlays/GlowProcessor.cs | 14 +++++------ .../Processors/Overlays/VignetteProcessor.cs | 10 ++++---- .../Drawing/FillRegionProcessorTests.cs | 6 ++--- 24 files changed, 115 insertions(+), 115 deletions(-) diff --git a/src/ImageSharp.Drawing/Processing/BrushApplicator.cs b/src/ImageSharp.Drawing/Processing/BrushApplicator.cs index 2b02c14fe1..a9df07ced3 100644 --- a/src/ImageSharp.Drawing/Processing/BrushApplicator.cs +++ b/src/ImageSharp.Drawing/Processing/BrushApplicator.cs @@ -21,9 +21,9 @@ namespace SixLabors.ImageSharp.Processing /// Initializes a new instance of the class. /// /// The configuration instance to use when performing operations. + /// The graphics options. /// The target. - /// The options. - internal BrushApplicator(Configuration configuration, ImageFrame target, GraphicsOptions options) + internal BrushApplicator(Configuration configuration, GraphicsOptions options, ImageFrame target) { this.Configuration = configuration; this.Target = target; diff --git a/src/ImageSharp.Drawing/Processing/EllipticGradientBrush.cs b/src/ImageSharp.Drawing/Processing/EllipticGradientBrush.cs index 7810c3c6d5..30902e4117 100644 --- a/src/ImageSharp.Drawing/Processing/EllipticGradientBrush.cs +++ b/src/ImageSharp.Drawing/Processing/EllipticGradientBrush.cs @@ -48,18 +48,18 @@ namespace SixLabors.ImageSharp.Processing /// public override BrushApplicator CreateApplicator( Configuration configuration, + GraphicsOptions options, ImageFrame source, - RectangleF region, - GraphicsOptions options) => + RectangleF region) => new RadialGradientBrushApplicator( configuration, + options, source, this.center, this.referenceAxisEnd, this.axisRatio, this.ColorStops, - this.RepetitionMode, - options); + this.RepetitionMode); /// private sealed class RadialGradientBrushApplicator : GradientBrushApplicator @@ -100,14 +100,14 @@ namespace SixLabors.ImageSharp.Processing /// The graphics options. public RadialGradientBrushApplicator( Configuration configuration, + GraphicsOptions options, ImageFrame target, PointF center, PointF referenceAxisEnd, float axisRatio, ColorStop[] colorStops, - GradientRepetitionMode repetitionMode, - GraphicsOptions options) - : base(configuration, target, colorStops, repetitionMode, options) + GradientRepetitionMode repetitionMode) + : base(configuration, options, target, colorStops, repetitionMode) { this.center = center; this.referenceAxisEnd = referenceAxisEnd; diff --git a/src/ImageSharp.Drawing/Processing/Extensions/FillRegionExtensions.cs b/src/ImageSharp.Drawing/Processing/Extensions/FillRegionExtensions.cs index f5dd76318c..fbb6dbda56 100644 --- a/src/ImageSharp.Drawing/Processing/Extensions/FillRegionExtensions.cs +++ b/src/ImageSharp.Drawing/Processing/Extensions/FillRegionExtensions.cs @@ -1,4 +1,4 @@ -// Copyright (c) Six Labors and contributors. +// Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. using SixLabors.ImageSharp.Primitives; @@ -77,7 +77,7 @@ namespace SixLabors.ImageSharp.Processing GraphicsOptions options, IBrush brush, Region region) => - source.ApplyProcessor(new FillRegionProcessor(brush, region, options)); + source.ApplyProcessor(new FillRegionProcessor(options, brush, region)); /// /// Flood fills the image with the specified brush. @@ -90,6 +90,6 @@ namespace SixLabors.ImageSharp.Processing this IImageProcessingContext source, GraphicsOptions options, IBrush brush) => - source.ApplyProcessor(new FillProcessor(brush, options)); + source.ApplyProcessor(new FillProcessor(options, brush)); } -} \ No newline at end of file +} diff --git a/src/ImageSharp.Drawing/Processing/GradientBrush.cs b/src/ImageSharp.Drawing/Processing/GradientBrush.cs index f17adf6335..3be56c0424 100644 --- a/src/ImageSharp.Drawing/Processing/GradientBrush.cs +++ b/src/ImageSharp.Drawing/Processing/GradientBrush.cs @@ -37,9 +37,9 @@ namespace SixLabors.ImageSharp.Processing /// public abstract BrushApplicator CreateApplicator( Configuration configuration, + GraphicsOptions options, ImageFrame source, - RectangleF region, - GraphicsOptions options) + RectangleF region) where TPixel : struct, IPixel; /// @@ -58,17 +58,17 @@ namespace SixLabors.ImageSharp.Processing /// Initializes a new instance of the class. /// /// The configuration instance to use when performing operations. + /// The graphics options. /// The target image. /// An array of color stops sorted by their position. /// Defines if and how the gradient should be repeated. - /// The graphics options. protected GradientBrushApplicator( Configuration configuration, + GraphicsOptions options, ImageFrame target, ColorStop[] colorStops, - GradientRepetitionMode repetitionMode, - GraphicsOptions options) - : base(configuration, target, options) + GradientRepetitionMode repetitionMode) + : base(configuration, options, target) { this.colorStops = colorStops; // TODO: requires colorStops to be sorted by position - should that be checked? this.repetitionMode = repetitionMode; diff --git a/src/ImageSharp.Drawing/Processing/IBrush.cs b/src/ImageSharp.Drawing/Processing/IBrush.cs index f2fdc32f05..f4c7ef7cbb 100644 --- a/src/ImageSharp.Drawing/Processing/IBrush.cs +++ b/src/ImageSharp.Drawing/Processing/IBrush.cs @@ -20,9 +20,9 @@ namespace SixLabors.ImageSharp.Processing /// /// The pixel type. /// The configuration instance to use when performing operations. + /// The graphic options. /// The source image. /// The region the brush will be applied to. - /// The graphic options /// /// The for this brush. /// @@ -32,9 +32,9 @@ namespace SixLabors.ImageSharp.Processing /// BrushApplicator CreateApplicator( Configuration configuration, + GraphicsOptions options, ImageFrame source, - RectangleF region, - GraphicsOptions options) + RectangleF region) where TPixel : struct, IPixel; } } diff --git a/src/ImageSharp.Drawing/Processing/ImageBrush.cs b/src/ImageSharp.Drawing/Processing/ImageBrush.cs index f23fb1f18d..e38614070f 100644 --- a/src/ImageSharp.Drawing/Processing/ImageBrush.cs +++ b/src/ImageSharp.Drawing/Processing/ImageBrush.cs @@ -32,19 +32,19 @@ namespace SixLabors.ImageSharp.Processing /// public BrushApplicator CreateApplicator( Configuration configuration, + GraphicsOptions options, ImageFrame source, - RectangleF region, - GraphicsOptions options) + RectangleF region) where TPixel : struct, IPixel { if (this.image is Image specificImage) { - return new ImageBrushApplicator(configuration, source, specificImage, region, false, options); + return new ImageBrushApplicator(configuration, options, source, specificImage, region, false); } specificImage = this.image.CloneAs(); - return new ImageBrushApplicator(configuration, source, specificImage, region, true, options); + return new ImageBrushApplicator(configuration, options, source, specificImage, region, true); } /// @@ -85,19 +85,19 @@ namespace SixLabors.ImageSharp.Processing /// Initializes a new instance of the class. /// /// The configuration instance to use when performing operations. + /// The graphics options. /// The target image. /// The image. /// The region. /// Whether to dispose the image on disposal of the applicator. - /// The graphics options. public ImageBrushApplicator( Configuration configuration, + GraphicsOptions options, ImageFrame target, Image image, RectangleF region, - bool shouldDisposeImage, - GraphicsOptions options) - : base(configuration, target, options) + bool shouldDisposeImage) + : base(configuration, options, target) { this.sourceImage = image; this.sourceFrame = image.Frames.RootFrame; diff --git a/src/ImageSharp.Drawing/Processing/LinearGradientBrush.cs b/src/ImageSharp.Drawing/Processing/LinearGradientBrush.cs index bf6a6356ac..044bee72c5 100644 --- a/src/ImageSharp.Drawing/Processing/LinearGradientBrush.cs +++ b/src/ImageSharp.Drawing/Processing/LinearGradientBrush.cs @@ -40,17 +40,17 @@ namespace SixLabors.ImageSharp.Processing /// public override BrushApplicator CreateApplicator( Configuration configuration, + GraphicsOptions options, ImageFrame source, - RectangleF region, - GraphicsOptions options) => + RectangleF region) => new LinearGradientBrushApplicator( configuration, + options, source, this.p1, this.p2, this.ColorStops, - this.RepetitionMode, - options); + this.RepetitionMode); /// /// The linear gradient brush applicator. @@ -96,21 +96,21 @@ namespace SixLabors.ImageSharp.Processing /// Initializes a new instance of the class. /// /// The configuration instance to use when performing operations. + /// The graphics options. /// The source image. - /// start point of the gradient. - /// end point of the gradient. - /// tuple list of colors and their respective position between 0 and 1 on the line. - /// defines how the gradient colors are repeated. - /// the graphics options. + /// The start point of the gradient. + /// The end point of the gradient. + /// A tuple list of colors and their respective position between 0 and 1 on the line. + /// Defines how the gradient colors are repeated. public LinearGradientBrushApplicator( Configuration configuration, + GraphicsOptions options, ImageFrame source, PointF start, PointF end, ColorStop[] colorStops, - GradientRepetitionMode repetitionMode, - GraphicsOptions options) - : base(configuration, source, colorStops, repetitionMode, options) + GradientRepetitionMode repetitionMode) + : base(configuration, options, source, colorStops, repetitionMode) { this.start = start; this.end = end; diff --git a/src/ImageSharp.Drawing/Processing/PathGradientBrush.cs b/src/ImageSharp.Drawing/Processing/PathGradientBrush.cs index be1af50118..9e354120e6 100644 --- a/src/ImageSharp.Drawing/Processing/PathGradientBrush.cs +++ b/src/ImageSharp.Drawing/Processing/PathGradientBrush.cs @@ -84,12 +84,12 @@ namespace SixLabors.ImageSharp.Processing /// public BrushApplicator CreateApplicator( Configuration configuration, + GraphicsOptions options, ImageFrame source, - RectangleF region, - GraphicsOptions options) + RectangleF region) where TPixel : struct, IPixel { - return new PathGradientBrushApplicator(configuration, source, this.edges, this.centerColor, options); + return new PathGradientBrushApplicator(configuration, options, source, this.edges, this.centerColor); } private static Color CalculateCenterColor(Color[] colors) @@ -201,17 +201,17 @@ namespace SixLabors.ImageSharp.Processing /// Initializes a new instance of the class. /// /// The configuration instance to use when performing operations. + /// The graphics options. /// The source image. /// Edges of the polygon. /// Color at the center of the gradient area to which the other colors converge. - /// The graphics options. public PathGradientBrushApplicator( Configuration configuration, + GraphicsOptions options, ImageFrame source, IList edges, - Color centerColor, - GraphicsOptions options) - : base(configuration, source, options) + Color centerColor) + : base(configuration, options, source) { this.edges = edges; diff --git a/src/ImageSharp.Drawing/Processing/PatternBrush.cs b/src/ImageSharp.Drawing/Processing/PatternBrush.cs index 9024036d02..726df5a797 100644 --- a/src/ImageSharp.Drawing/Processing/PatternBrush.cs +++ b/src/ImageSharp.Drawing/Processing/PatternBrush.cs @@ -93,15 +93,15 @@ namespace SixLabors.ImageSharp.Processing /// public BrushApplicator CreateApplicator( Configuration configuration, + GraphicsOptions options, ImageFrame source, - RectangleF region, - GraphicsOptions options) + RectangleF region) where TPixel : struct, IPixel => new PatternBrushApplicator( configuration, + options, source, - this.pattern.ToPixelMatrix(configuration), - options); + this.pattern.ToPixelMatrix(configuration)); /// /// The pattern brush applicator. @@ -118,15 +118,15 @@ namespace SixLabors.ImageSharp.Processing /// Initializes a new instance of the class. /// /// The configuration instance to use when performing operations. + /// The graphics options. /// The source image. /// The pattern. - /// The graphics options. public PatternBrushApplicator( Configuration configuration, + GraphicsOptions options, ImageFrame source, - in DenseMatrix pattern, - GraphicsOptions options) - : base(configuration, source, options) + in DenseMatrix pattern) + : base(configuration, options, source) { this.pattern = pattern; } diff --git a/src/ImageSharp.Drawing/Processing/Processors/Drawing/FillProcessor.cs b/src/ImageSharp.Drawing/Processing/Processors/Drawing/FillProcessor.cs index 1d3cf35576..3963f99a5c 100644 --- a/src/ImageSharp.Drawing/Processing/Processors/Drawing/FillProcessor.cs +++ b/src/ImageSharp.Drawing/Processing/Processors/Drawing/FillProcessor.cs @@ -15,9 +15,9 @@ namespace SixLabors.ImageSharp.Processing.Processors.Drawing /// /// Initializes a new instance of the class. /// - /// The brush to use for filling. /// The defining how to blend the brush pixels over the image pixels. - public FillProcessor(IBrush brush, GraphicsOptions options) + /// The brush to use for filling. + public FillProcessor(GraphicsOptions options, IBrush brush) { this.Brush = brush; this.Options = options; diff --git a/src/ImageSharp.Drawing/Processing/Processors/Drawing/FillProcessor{TPixel}.cs b/src/ImageSharp.Drawing/Processing/Processors/Drawing/FillProcessor{TPixel}.cs index 0309c561aa..fc94826187 100644 --- a/src/ImageSharp.Drawing/Processing/Processors/Drawing/FillProcessor{TPixel}.cs +++ b/src/ImageSharp.Drawing/Processing/Processors/Drawing/FillProcessor{TPixel}.cs @@ -85,9 +85,9 @@ namespace SixLabors.ImageSharp.Processing.Processors.Drawing using (IMemoryOwner amount = source.MemoryAllocator.Allocate(width)) using (BrushApplicator applicator = brush.CreateApplicator( configuration, + options, source, - sourceRectangle, - options)) + sourceRectangle)) { amount.Memory.Span.Fill(1f); diff --git a/src/ImageSharp.Drawing/Processing/Processors/Drawing/FillRegionProcessor.cs b/src/ImageSharp.Drawing/Processing/Processors/Drawing/FillRegionProcessor.cs index 2318f3168b..7d51be1c51 100644 --- a/src/ImageSharp.Drawing/Processing/Processors/Drawing/FillRegionProcessor.cs +++ b/src/ImageSharp.Drawing/Processing/Processors/Drawing/FillRegionProcessor.cs @@ -16,10 +16,10 @@ namespace SixLabors.ImageSharp.Processing.Processors.Drawing /// /// Initializes a new instance of the class. /// + /// The graphics options. /// The details how to fill the region of interest. /// The region of interest to be filled. - /// The configuration options. - public FillRegionProcessor(IBrush brush, Region region, GraphicsOptions options) + public FillRegionProcessor(GraphicsOptions options, IBrush brush, Region region) { this.Region = region; this.Brush = brush; diff --git a/src/ImageSharp.Drawing/Processing/Processors/Drawing/FillRegionProcessor{TPixel}.cs b/src/ImageSharp.Drawing/Processing/Processors/Drawing/FillRegionProcessor{TPixel}.cs index 8c665826fe..4744a4e920 100644 --- a/src/ImageSharp.Drawing/Processing/Processors/Drawing/FillRegionProcessor{TPixel}.cs +++ b/src/ImageSharp.Drawing/Processing/Processors/Drawing/FillRegionProcessor{TPixel}.cs @@ -71,7 +71,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Drawing } } - using (BrushApplicator applicator = brush.CreateApplicator(configuration, source, rect, options)) + using (BrushApplicator applicator = brush.CreateApplicator(configuration, options, source, rect)) { int scanlineWidth = maxX - minX; using (IMemoryOwner bBuffer = source.MemoryAllocator.Allocate(maxIntersections)) diff --git a/src/ImageSharp.Drawing/Processing/Processors/Text/DrawTextProcessor{TPixel}.cs b/src/ImageSharp.Drawing/Processing/Processors/Text/DrawTextProcessor{TPixel}.cs index 244e18b81c..64d32efb80 100644 --- a/src/ImageSharp.Drawing/Processing/Processors/Text/DrawTextProcessor{TPixel}.cs +++ b/src/ImageSharp.Drawing/Processing/Processors/Text/DrawTextProcessor{TPixel}.cs @@ -83,7 +83,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Text { if (operations?.Count > 0) { - using (BrushApplicator app = brush.CreateApplicator(this.Configuration, source, this.SourceRectangle, this.textRenderer.Options)) + using (BrushApplicator app = brush.CreateApplicator(this.Configuration, this.textRenderer.Options, source, this.SourceRectangle)) { foreach (DrawingOperation operation in operations) { diff --git a/src/ImageSharp.Drawing/Processing/RadialGradientBrush.cs b/src/ImageSharp.Drawing/Processing/RadialGradientBrush.cs index 7f1fa818eb..2b1b6913f8 100644 --- a/src/ImageSharp.Drawing/Processing/RadialGradientBrush.cs +++ b/src/ImageSharp.Drawing/Processing/RadialGradientBrush.cs @@ -36,17 +36,17 @@ namespace SixLabors.ImageSharp.Processing /// public override BrushApplicator CreateApplicator( Configuration configuration, + GraphicsOptions options, ImageFrame source, - RectangleF region, - GraphicsOptions options) => + RectangleF region) => new RadialGradientBrushApplicator( configuration, + options, source, this.center, this.radius, this.ColorStops, - this.RepetitionMode, - options); + this.RepetitionMode); /// private sealed class RadialGradientBrushApplicator : GradientBrushApplicator @@ -60,21 +60,21 @@ namespace SixLabors.ImageSharp.Processing /// Initializes a new instance of the class. /// /// The configuration instance to use when performing operations. + /// The graphics options. /// The target image. /// Center point of the gradient. /// Radius of the gradient. /// Definition of colors. /// How the colors are repeated beyond the first gradient. - /// The graphics options. public RadialGradientBrushApplicator( Configuration configuration, + GraphicsOptions options, ImageFrame target, PointF center, float radius, ColorStop[] colorStops, - GradientRepetitionMode repetitionMode, - GraphicsOptions options) - : base(configuration, target, colorStops, repetitionMode, options) + GradientRepetitionMode repetitionMode) + : base(configuration, options, target, colorStops, repetitionMode) { this.center = center; this.radius = radius; diff --git a/src/ImageSharp.Drawing/Processing/RecolorBrush.cs b/src/ImageSharp.Drawing/Processing/RecolorBrush.cs index 1ad613bf82..e0e43cf780 100644 --- a/src/ImageSharp.Drawing/Processing/RecolorBrush.cs +++ b/src/ImageSharp.Drawing/Processing/RecolorBrush.cs @@ -47,18 +47,18 @@ namespace SixLabors.ImageSharp.Processing /// public BrushApplicator CreateApplicator( Configuration configuration, + GraphicsOptions options, ImageFrame source, - RectangleF region, - GraphicsOptions options) + RectangleF region) where TPixel : struct, IPixel { return new RecolorBrushApplicator( configuration, + options, source, this.SourceColor.ToPixel(), this.TargetColor.ToPixel(), - this.Threshold, - options); + this.Threshold); } /// @@ -83,19 +83,19 @@ namespace SixLabors.ImageSharp.Processing /// Initializes a new instance of the class. /// /// The configuration instance to use when performing operations. + /// The options /// The source image. /// Color of the source. /// Color of the target. /// The threshold . - /// The options public RecolorBrushApplicator( Configuration configuration, + GraphicsOptions options, ImageFrame source, TPixel sourceColor, TPixel targetColor, - float threshold, - GraphicsOptions options) - : base(configuration, source, options) + float threshold) + : base(configuration, options, source) { this.sourceColor = sourceColor.ToVector4(); this.targetColorPixel = targetColor; diff --git a/src/ImageSharp.Drawing/Processing/SolidBrush.cs b/src/ImageSharp.Drawing/Processing/SolidBrush.cs index a2be775daa..c297ede211 100644 --- a/src/ImageSharp.Drawing/Processing/SolidBrush.cs +++ b/src/ImageSharp.Drawing/Processing/SolidBrush.cs @@ -33,12 +33,12 @@ namespace SixLabors.ImageSharp.Processing /// public BrushApplicator CreateApplicator( Configuration configuration, + GraphicsOptions options, ImageFrame source, - RectangleF region, - GraphicsOptions options) + RectangleF region) where TPixel : struct, IPixel { - return new SolidBrushApplicator(configuration, source, this.Color.ToPixel(), options); + return new SolidBrushApplicator(configuration, options, source, this.Color.ToPixel()); } /// @@ -53,15 +53,15 @@ namespace SixLabors.ImageSharp.Processing /// Initializes a new instance of the class. /// /// The configuration instance to use when performing operations. + /// The graphics options. /// The source image. /// The color. - /// The graphics options. public SolidBrushApplicator( Configuration configuration, + GraphicsOptions options, ImageFrame source, - TPixel color, - GraphicsOptions options) - : base(configuration, source, options) + TPixel color) + : base(configuration, options, source) { this.Colors = source.MemoryAllocator.Allocate(source.Width); this.Colors.Memory.Span.Fill(color); diff --git a/src/ImageSharp/Processing/Extensions/BackgroundColorExtensions.cs b/src/ImageSharp/Processing/Extensions/BackgroundColorExtensions.cs index cc2fb25120..4241721f46 100644 --- a/src/ImageSharp/Processing/Extensions/BackgroundColorExtensions.cs +++ b/src/ImageSharp/Processing/Extensions/BackgroundColorExtensions.cs @@ -47,7 +47,7 @@ namespace SixLabors.ImageSharp.Processing this IImageProcessingContext source, GraphicsOptions options, Color color) => - source.ApplyProcessor(new BackgroundColorProcessor(color, options)); + source.ApplyProcessor(new BackgroundColorProcessor(options, color)); /// /// Replaces the background color of image with the given one. @@ -64,6 +64,6 @@ namespace SixLabors.ImageSharp.Processing GraphicsOptions options, Color color, Rectangle rectangle) => - source.ApplyProcessor(new BackgroundColorProcessor(color, options), rectangle); + source.ApplyProcessor(new BackgroundColorProcessor(options, color), rectangle); } } diff --git a/src/ImageSharp/Processing/Extensions/GlowExtensions.cs b/src/ImageSharp/Processing/Extensions/GlowExtensions.cs index 90b73794ba..48ecb5108f 100644 --- a/src/ImageSharp/Processing/Extensions/GlowExtensions.cs +++ b/src/ImageSharp/Processing/Extensions/GlowExtensions.cs @@ -1,4 +1,4 @@ -// Copyright (c) Six Labors and contributors. +// Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. using SixLabors.ImageSharp.Primitives; @@ -155,7 +155,7 @@ namespace SixLabors.ImageSharp.Processing Color color, ValueSize radius, Rectangle rectangle) => - source.ApplyProcessor(new GlowProcessor(color, radius, options), rectangle); + source.ApplyProcessor(new GlowProcessor(options, color, radius), rectangle); /// /// Applies a radial glow effect to an image. @@ -170,6 +170,6 @@ namespace SixLabors.ImageSharp.Processing GraphicsOptions options, Color color, ValueSize radius) => - source.ApplyProcessor(new GlowProcessor(color, radius, options)); + source.ApplyProcessor(new GlowProcessor(options, color, radius)); } -} \ No newline at end of file +} diff --git a/src/ImageSharp/Processing/Extensions/VignetteExtensions.cs b/src/ImageSharp/Processing/Extensions/VignetteExtensions.cs index ffab8e2bf9..a1f3a6e8a0 100644 --- a/src/ImageSharp/Processing/Extensions/VignetteExtensions.cs +++ b/src/ImageSharp/Processing/Extensions/VignetteExtensions.cs @@ -1,4 +1,4 @@ -// Copyright (c) Six Labors and contributors. +// Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. using SixLabors.ImageSharp.Primitives; @@ -166,7 +166,7 @@ namespace SixLabors.ImageSharp.Processing ValueSize radiusX, ValueSize radiusY, Rectangle rectangle) => - source.ApplyProcessor(new VignetteProcessor(color, radiusX, radiusY, options), rectangle); + source.ApplyProcessor(new VignetteProcessor(options, color, radiusX, radiusY), rectangle); private static IImageProcessingContext VignetteInternal( this IImageProcessingContext source, @@ -174,6 +174,6 @@ namespace SixLabors.ImageSharp.Processing Color color, ValueSize radiusX, ValueSize radiusY) => - source.ApplyProcessor(new VignetteProcessor(color, radiusX, radiusY, options)); + source.ApplyProcessor(new VignetteProcessor(options, color, radiusX, radiusY)); } -} \ No newline at end of file +} diff --git a/src/ImageSharp/Processing/Processors/Overlays/BackgroundColorProcessor.cs b/src/ImageSharp/Processing/Processors/Overlays/BackgroundColorProcessor.cs index 4b4c537277..e78f7e5e75 100644 --- a/src/ImageSharp/Processing/Processors/Overlays/BackgroundColorProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Overlays/BackgroundColorProcessor.cs @@ -14,9 +14,9 @@ namespace SixLabors.ImageSharp.Processing.Processors.Overlays /// /// Initializes a new instance of the class. /// - /// The to set the background color to. /// The options defining blending algorithm and amount. - public BackgroundColorProcessor(Color color, GraphicsOptions options) + /// The to set the background color to. + public BackgroundColorProcessor(GraphicsOptions options, Color color) { this.Color = color; this.GraphicsOptions = options; diff --git a/src/ImageSharp/Processing/Processors/Overlays/GlowProcessor.cs b/src/ImageSharp/Processing/Processors/Overlays/GlowProcessor.cs index 1a60b79aca..4b9a23eff1 100644 --- a/src/ImageSharp/Processing/Processors/Overlays/GlowProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Overlays/GlowProcessor.cs @@ -24,10 +24,10 @@ namespace SixLabors.ImageSharp.Processing.Processors.Overlays /// /// Initializes a new instance of the class. /// - /// The color or the glow. /// The options effecting blending and composition. - public GlowProcessor(Color color, GraphicsOptions options) - : this(color, 0, options) + /// The color or the glow. + public GlowProcessor(GraphicsOptions options, Color color) + : this(options, color, 0) { } @@ -37,17 +37,17 @@ namespace SixLabors.ImageSharp.Processing.Processors.Overlays /// The color or the glow. /// The radius of the glow. internal GlowProcessor(Color color, ValueSize radius) - : this(color, radius, new GraphicsOptions()) + : this(new GraphicsOptions(), color, radius) { } /// /// Initializes a new instance of the class. /// + /// The options effecting blending and composition. /// The color or the glow. /// The radius of the glow. - /// The options effecting blending and composition. - internal GlowProcessor(Color color, ValueSize radius, GraphicsOptions options) + internal GlowProcessor(GraphicsOptions options, Color color, ValueSize radius) { this.GlowColor = color; this.Radius = radius; @@ -67,7 +67,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Overlays /// /// Gets the the radius. /// - internal ValueSize Radius { get; } + internal ValueSize Radius { get; } /// public IImageProcessor CreatePixelSpecificProcessor(Image source, Rectangle sourceRectangle) diff --git a/src/ImageSharp/Processing/Processors/Overlays/VignetteProcessor.cs b/src/ImageSharp/Processing/Processors/Overlays/VignetteProcessor.cs index e1c836ff56..3cf48e5a40 100644 --- a/src/ImageSharp/Processing/Processors/Overlays/VignetteProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Overlays/VignetteProcessor.cs @@ -17,16 +17,16 @@ namespace SixLabors.ImageSharp.Processing.Processors.Overlays /// /// The color of the vignette. public VignetteProcessor(Color color) - : this(color, new GraphicsOptions()) + : this(new GraphicsOptions(), color) { } /// /// Initializes a new instance of the class. /// - /// The color of the vignette. /// The options effecting blending and composition. - public VignetteProcessor(Color color, GraphicsOptions options) + /// The color of the vignette. + public VignetteProcessor(GraphicsOptions options, Color color) { this.VignetteColor = color; this.GraphicsOptions = options; @@ -35,11 +35,11 @@ namespace SixLabors.ImageSharp.Processing.Processors.Overlays /// /// Initializes a new instance of the class. /// + /// The options effecting blending and composition. /// The color of the vignette. /// The x-radius. /// The y-radius. - /// The options effecting blending and composition. - internal VignetteProcessor(Color color, ValueSize radiusX, ValueSize radiusY, GraphicsOptions options) + internal VignetteProcessor(GraphicsOptions options, Color color, ValueSize radiusX, ValueSize radiusY) { this.VignetteColor = color; this.RadiusX = radiusX; diff --git a/tests/ImageSharp.Tests/Drawing/FillRegionProcessorTests.cs b/tests/ImageSharp.Tests/Drawing/FillRegionProcessorTests.cs index 6230d52a17..e259d29d9c 100644 --- a/tests/ImageSharp.Tests/Drawing/FillRegionProcessorTests.cs +++ b/tests/ImageSharp.Tests/Drawing/FillRegionProcessorTests.cs @@ -38,7 +38,7 @@ namespace SixLabors.ImageSharp.Tests.Drawing Antialias = antialias, AntialiasSubpixelDepth = 1 }; - var processor = new FillRegionProcessor(brush.Object, region, options); + var processor = new FillRegionProcessor(options, brush.Object, region); var img = new Image(1, 1); processor.Execute(img, bounds); @@ -51,7 +51,7 @@ namespace SixLabors.ImageSharp.Tests.Drawing var bounds = new Rectangle(-100, -10, 10, 10); var brush = new Mock(); var options = new GraphicsOptions { Antialias = true }; - var processor = new FillRegionProcessor(brush.Object, new MockRegion1(), options); + var processor = new FillRegionProcessor(options, brush.Object, new MockRegion1()); var img = new Image(10, 10); processor.Execute(img, bounds); } @@ -72,7 +72,7 @@ namespace SixLabors.ImageSharp.Tests.Drawing public void DoesNotThrowForIssue928() { var rectText = new RectangleF(0, 0, 2000, 2000); - using (Image img = new Image((int)rectText.Width, (int)rectText.Height)) + using (var img = new Image((int)rectText.Width, (int)rectText.Height)) { img.Mutate(x => x.Fill(Rgba32.Transparent)); From b6e039bf63671e55051a349dc3ac78cba489ac9e Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Mon, 11 Nov 2019 12:50:55 +1100 Subject: [PATCH 53/75] Remove unused properties. --- src/ImageSharp.Drawing/Processing/TextGraphicsOptions.cs | 5 ----- src/ImageSharp/GraphicsOptions.cs | 5 ----- 2 files changed, 10 deletions(-) diff --git a/src/ImageSharp.Drawing/Processing/TextGraphicsOptions.cs b/src/ImageSharp.Drawing/Processing/TextGraphicsOptions.cs index 185fa1f74c..63730d1bf7 100644 --- a/src/ImageSharp.Drawing/Processing/TextGraphicsOptions.cs +++ b/src/ImageSharp.Drawing/Processing/TextGraphicsOptions.cs @@ -40,11 +40,6 @@ namespace SixLabors.ImageSharp.Processing this.VerticalAlignment = source.VerticalAlignment; } - /// - /// Gets the default instance. - /// - public static TextGraphicsOptions Default { get; } = new TextGraphicsOptions(); - /// /// Gets or sets a value indicating whether antialiasing should be applied. /// Defaults to true. diff --git a/src/ImageSharp/GraphicsOptions.cs b/src/ImageSharp/GraphicsOptions.cs index 9c62d23371..47b930e654 100644 --- a/src/ImageSharp/GraphicsOptions.cs +++ b/src/ImageSharp/GraphicsOptions.cs @@ -29,11 +29,6 @@ namespace SixLabors.ImageSharp this.ColorBlendingMode = source.ColorBlendingMode; } - /// - /// Gets the default instance. - /// - public static GraphicsOptions Default { get; } = new GraphicsOptions(); - /// /// Gets or sets a value indicating whether antialiasing should be applied. /// Defaults to true. From 3ed4f3cf7343008986660c64b0c637dd1f7eadb2 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Mon, 11 Nov 2019 13:02:13 +1100 Subject: [PATCH 54/75] Prevent duplicate ctr in extensions --- .../Processing/EllipticGradientBrush.cs | 2 +- .../Extensions/DrawImageExtensions.cs | 36 +++++++++++-------- 2 files changed, 22 insertions(+), 16 deletions(-) diff --git a/src/ImageSharp.Drawing/Processing/EllipticGradientBrush.cs b/src/ImageSharp.Drawing/Processing/EllipticGradientBrush.cs index 30902e4117..fbab3605d2 100644 --- a/src/ImageSharp.Drawing/Processing/EllipticGradientBrush.cs +++ b/src/ImageSharp.Drawing/Processing/EllipticGradientBrush.cs @@ -89,6 +89,7 @@ namespace SixLabors.ImageSharp.Processing /// Initializes a new instance of the class. /// /// The configuration instance to use when performing operations. + /// The graphics options. /// The target image. /// Center of the ellipse. /// Point on one angular points of the ellipse. @@ -97,7 +98,6 @@ namespace SixLabors.ImageSharp.Processing /// the first is defined by and . /// Definition of colors. /// Defines how the gradient colors are repeated. - /// The graphics options. public RadialGradientBrushApplicator( Configuration configuration, GraphicsOptions options, diff --git a/src/ImageSharp.Drawing/Processing/Extensions/DrawImageExtensions.cs b/src/ImageSharp.Drawing/Processing/Extensions/DrawImageExtensions.cs index 6c694ab73b..6c79984378 100644 --- a/src/ImageSharp.Drawing/Processing/Extensions/DrawImageExtensions.cs +++ b/src/ImageSharp.Drawing/Processing/Extensions/DrawImageExtensions.cs @@ -22,14 +22,17 @@ namespace SixLabors.ImageSharp.Processing public static IImageProcessingContext DrawImage( this IImageProcessingContext source, Image image, - float opacity) => - source.ApplyProcessor( + float opacity) + { + var options = new GraphicsOptions(); + return source.ApplyProcessor( new DrawImageProcessor( - image, - Point.Empty, - new GraphicsOptions().ColorBlendingMode, - new GraphicsOptions().AlphaCompositionMode, - opacity)); + image, + Point.Empty, + options.ColorBlendingMode, + options.AlphaCompositionMode, + opacity)); + } /// /// Draws the given image together with the current one by blending their pixels. @@ -100,14 +103,17 @@ namespace SixLabors.ImageSharp.Processing this IImageProcessingContext source, Image image, Point location, - float opacity) => - source.ApplyProcessor( + float opacity) + { + var options = new GraphicsOptions(); + return source.ApplyProcessor( new DrawImageProcessor( - image, - location, - new GraphicsOptions().ColorBlendingMode, - new GraphicsOptions().AlphaCompositionMode, - opacity)); + image, + location, + options.ColorBlendingMode, + options.AlphaCompositionMode, + opacity)); + } /// /// Draws the given image together with the current one by blending their pixels. @@ -172,4 +178,4 @@ namespace SixLabors.ImageSharp.Processing options.AlphaCompositionMode, options.BlendPercentage)); } -} \ No newline at end of file +} From c88e34d3441d0c5ef772c49837c345315b846e0f Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Tue, 12 Nov 2019 13:36:31 +1100 Subject: [PATCH 55/75] Initial working commit --- src/ImageSharp/Formats/Png/Zlib/Adler32.cs | 8 +- src/ImageSharp/Formats/Png/Zlib/Deflater.cs | 610 +++++++++++ .../Formats/Png/Zlib/DeflaterConstants.cs | 151 +++ .../Formats/Png/Zlib/DeflaterEngine.cs | 951 +++++++++++++++++ .../Formats/Png/Zlib/DeflaterHuffman.cs | 965 ++++++++++++++++++ .../Formats/Png/Zlib/DeflaterOutputStream.cs | 499 +++++++++ .../Formats/Png/Zlib/DeflaterPending.cs | 21 + .../Formats/Png/Zlib/PendingBuffer.cs | 276 +++++ .../Formats/Png/Zlib/ZlibDeflateStream.cs | 30 +- .../ImageSharp.Benchmarks/Codecs/EncodePng.cs | 8 +- 10 files changed, 3498 insertions(+), 21 deletions(-) create mode 100644 src/ImageSharp/Formats/Png/Zlib/Deflater.cs create mode 100644 src/ImageSharp/Formats/Png/Zlib/DeflaterConstants.cs create mode 100644 src/ImageSharp/Formats/Png/Zlib/DeflaterEngine.cs create mode 100644 src/ImageSharp/Formats/Png/Zlib/DeflaterHuffman.cs create mode 100644 src/ImageSharp/Formats/Png/Zlib/DeflaterOutputStream.cs create mode 100644 src/ImageSharp/Formats/Png/Zlib/DeflaterPending.cs create mode 100644 src/ImageSharp/Formats/Png/Zlib/PendingBuffer.cs diff --git a/src/ImageSharp/Formats/Png/Zlib/Adler32.cs b/src/ImageSharp/Formats/Png/Zlib/Adler32.cs index a06983b9ed..f6f6edd124 100644 --- a/src/ImageSharp/Formats/Png/Zlib/Adler32.cs +++ b/src/ImageSharp/Formats/Png/Zlib/Adler32.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; @@ -133,8 +133,8 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib count -= n; while (--n >= 0) { - s1 = s1 + (uint)(data[offset++] & 0xff); - s2 = s2 + s1; + s1 += (uint)(data[offset++] & 0xff); + s2 += s1; } s1 %= Base; @@ -144,4 +144,4 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib this.checksum = (s2 << 16) | s1; } } -} \ No newline at end of file +} diff --git a/src/ImageSharp/Formats/Png/Zlib/Deflater.cs b/src/ImageSharp/Formats/Png/Zlib/Deflater.cs new file mode 100644 index 0000000000..3581125492 --- /dev/null +++ b/src/ImageSharp/Formats/Png/Zlib/Deflater.cs @@ -0,0 +1,610 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +// +using System; +using System.Collections.Generic; +using System.Text; + +namespace SixLabors.ImageSharp.Formats.Png.Zlib +{ + /// + /// This is the Deflater class. The deflater class compresses input + /// with the deflate algorithm described in RFC 1951. It has several + /// compression levels and three different strategies described below. + /// + /// This class is not thread safe. This is inherent in the API, due + /// to the split of deflate and setInput. + /// + /// author of the original java version : Jochen Hoenicke + /// + public class Deflater + { + #region Deflater Documentation + + /* + * The Deflater can do the following state transitions: + * + * (1) -> INIT_STATE ----> INIT_FINISHING_STATE ---. + * / | (2) (5) | + * / v (5) | + * (3)| SETDICT_STATE ---> SETDICT_FINISHING_STATE |(3) + * \ | (3) | ,--------' + * | | | (3) / + * v v (5) v v + * (1) -> BUSY_STATE ----> FINISHING_STATE + * | (6) + * v + * FINISHED_STATE + * \_____________________________________/ + * | (7) + * v + * CLOSED_STATE + * + * (1) If we should produce a header we start in INIT_STATE, otherwise + * we start in BUSY_STATE. + * (2) A dictionary may be set only when we are in INIT_STATE, then + * we change the state as indicated. + * (3) Whether a dictionary is set or not, on the first call of deflate + * we change to BUSY_STATE. + * (4) -- intentionally left blank -- :) + * (5) FINISHING_STATE is entered, when flush() is called to indicate that + * there is no more INPUT. There are also states indicating, that + * the header wasn't written yet. + * (6) FINISHED_STATE is entered, when everything has been flushed to the + * internal pending output buffer. + * (7) At any time (7) + * + */ + + #endregion Deflater Documentation + + #region Public Constants + + /// + /// The best and slowest compression level. This tries to find very + /// long and distant string repetitions. + /// + public const int BEST_COMPRESSION = 9; + + /// + /// The worst but fastest compression level. + /// + public const int BEST_SPEED = 1; + + /// + /// The default compression level. + /// + public const int DEFAULT_COMPRESSION = -1; + + /// + /// This level won't compress at all but output uncompressed blocks. + /// + public const int NO_COMPRESSION = 0; + + /// + /// The compression method. This is the only method supported so far. + /// There is no need to use this constant at all. + /// + public const int DEFLATED = 8; + + #endregion Public Constants + + #region Public Enum + + /// + /// Compression Level as an enum for safer use + /// + public enum CompressionLevel + { + /// + /// The best and slowest compression level. This tries to find very + /// long and distant string repetitions. + /// + BEST_COMPRESSION = Deflater.BEST_COMPRESSION, + + /// + /// The worst but fastest compression level. + /// + BEST_SPEED = Deflater.BEST_SPEED, + + /// + /// The default compression level. + /// + DEFAULT_COMPRESSION = Deflater.DEFAULT_COMPRESSION, + + /// + /// This level won't compress at all but output uncompressed blocks. + /// + NO_COMPRESSION = Deflater.NO_COMPRESSION, + + /// + /// The compression method. This is the only method supported so far. + /// There is no need to use this constant at all. + /// + DEFLATED = Deflater.DEFLATED + } + + #endregion Public Enum + + #region Local Constants + + private const int IS_SETDICT = 0x01; + private const int IS_FLUSHING = 0x04; + private const int IS_FINISHING = 0x08; + + private const int INIT_STATE = 0x00; + private const int SETDICT_STATE = 0x01; + + // private static int INIT_FINISHING_STATE = 0x08; + // private static int SETDICT_FINISHING_STATE = 0x09; + private const int BUSY_STATE = 0x10; + + private const int FLUSHING_STATE = 0x14; + private const int FINISHING_STATE = 0x1c; + private const int FINISHED_STATE = 0x1e; + private const int CLOSED_STATE = 0x7f; + + #endregion Local Constants + + #region Constructors + + /// + /// Creates a new deflater with default compression level. + /// + public Deflater() : this(DEFAULT_COMPRESSION, false) + { + } + + /// + /// Creates a new deflater with given compression level. + /// + /// + /// the compression level, a value between NO_COMPRESSION + /// and BEST_COMPRESSION, or DEFAULT_COMPRESSION. + /// + /// if lvl is out of range. + public Deflater(int level) : this(level, false) + { + } + + /// + /// Creates a new deflater with given compression level. + /// + /// + /// the compression level, a value between NO_COMPRESSION + /// and BEST_COMPRESSION. + /// + /// + /// true, if we should suppress the Zlib/RFC1950 header at the + /// beginning and the adler checksum at the end of the output. This is + /// useful for the GZIP/PKZIP formats. + /// + /// if lvl is out of range. + public Deflater(int level, bool noZlibHeaderOrFooter) + { + if (level == DEFAULT_COMPRESSION) + { + level = 6; + } + else if (level < NO_COMPRESSION || level > BEST_COMPRESSION) + { + throw new ArgumentOutOfRangeException(nameof(level)); + } + + pending = new DeflaterPending(); + engine = new DeflaterEngine(pending, noZlibHeaderOrFooter); + this.noZlibHeaderOrFooter = noZlibHeaderOrFooter; + SetStrategy(DeflateStrategy.Default); + SetLevel(level); + Reset(); + } + + #endregion Constructors + + /// + /// Resets the deflater. The deflater acts afterwards as if it was + /// just created with the same compression level and strategy as it + /// had before. + /// + public void Reset() + { + state = (noZlibHeaderOrFooter ? BUSY_STATE : INIT_STATE); + totalOut = 0; + pending.Reset(); + engine.Reset(); + } + + /// + /// Gets the current adler checksum of the data that was processed so far. + /// + public int Adler + { + get + { + return engine.Adler; + } + } + + /// + /// Gets the number of input bytes processed so far. + /// + public long TotalIn + { + get + { + return engine.TotalIn; + } + } + + /// + /// Gets the number of output bytes so far. + /// + public long TotalOut + { + get + { + return totalOut; + } + } + + /// + /// Flushes the current input block. Further calls to deflate() will + /// produce enough output to inflate everything in the current input + /// block. This is not part of Sun's JDK so I have made it package + /// private. It is used by DeflaterOutputStream to implement + /// flush(). + /// + public void Flush() + { + state |= IS_FLUSHING; + } + + /// + /// Finishes the deflater with the current input block. It is an error + /// to give more input after this method was called. This method must + /// be called to force all bytes to be flushed. + /// + public void Finish() + { + state |= (IS_FLUSHING | IS_FINISHING); + } + + /// + /// Returns true if the stream was finished and no more output bytes + /// are available. + /// + public bool IsFinished + { + get + { + return (state == FINISHED_STATE) && pending.IsFlushed; + } + } + + /// + /// Returns true, if the input buffer is empty. + /// You should then call setInput(). + /// NOTE: This method can also return true when the stream + /// was finished. + /// + public bool IsNeedingInput + { + get + { + return engine.NeedsInput(); + } + } + + /// + /// Sets the data which should be compressed next. This should be only + /// called when needsInput indicates that more input is needed. + /// If you call setInput when needsInput() returns false, the + /// previous input that is still pending will be thrown away. + /// The given byte array should not be changed, before needsInput() returns + /// true again. + /// This call is equivalent to setInput(input, 0, input.length). + /// + /// + /// the buffer containing the input data. + /// + /// + /// if the buffer was finished() or ended(). + /// + public void SetInput(byte[] input) + { + SetInput(input, 0, input.Length); + } + + /// + /// Sets the data which should be compressed next. This should be + /// only called when needsInput indicates that more input is needed. + /// The given byte array should not be changed, before needsInput() returns + /// true again. + /// + /// + /// the buffer containing the input data. + /// + /// + /// the start of the data. + /// + /// + /// the number of data bytes of input. + /// + /// + /// if the buffer was Finish()ed or if previous input is still pending. + /// + public void SetInput(byte[] input, int offset, int count) + { + if ((state & IS_FINISHING) != 0) + { + throw new InvalidOperationException("Finish() already called"); + } + engine.SetInput(input, offset, count); + } + + /// + /// Sets the compression level. There is no guarantee of the exact + /// position of the change, but if you call this when needsInput is + /// true the change of compression level will occur somewhere near + /// before the end of the so far given input. + /// + /// + /// the new compression level. + /// + public void SetLevel(int level) + { + if (level == DEFAULT_COMPRESSION) + { + level = 6; + } + else if (level < NO_COMPRESSION || level > BEST_COMPRESSION) + { + throw new ArgumentOutOfRangeException(nameof(level)); + } + + if (this.level != level) + { + this.level = level; + engine.SetLevel(level); + } + } + + /// + /// Get current compression level + /// + /// Returns the current compression level + public int GetLevel() + { + return level; + } + + /// + /// Sets the compression strategy. Strategy is one of + /// DEFAULT_STRATEGY, HUFFMAN_ONLY and FILTERED. For the exact + /// position where the strategy is changed, the same as for + /// SetLevel() applies. + /// + /// + /// The new compression strategy. + /// + public void SetStrategy(DeflateStrategy strategy) + { + engine.Strategy = strategy; + } + + /// + /// Deflates the current input block with to the given array. + /// + /// + /// The buffer where compressed data is stored + /// + /// + /// The number of compressed bytes added to the output, or 0 if either + /// IsNeedingInput() or IsFinished returns true or length is zero. + /// + public int Deflate(byte[] output) + { + return Deflate(output, 0, output.Length); + } + + /// + /// Deflates the current input block to the given array. + /// + /// + /// Buffer to store the compressed data. + /// + /// + /// Offset into the output array. + /// + /// + /// The maximum number of bytes that may be stored. + /// + /// + /// The number of compressed bytes added to the output, or 0 if either + /// needsInput() or finished() returns true or length is zero. + /// + /// + /// If Finish() was previously called. + /// + /// + /// If offset or length don't match the array length. + /// + public int Deflate(byte[] output, int offset, int length) + { + int origLength = length; + + if (state == CLOSED_STATE) + { + throw new InvalidOperationException("Deflater closed"); + } + + if (state < BUSY_STATE) + { + // output header + int header = (DEFLATED + + ((DeflaterConstants.MAX_WBITS - 8) << 4)) << 8; + int level_flags = (level - 1) >> 1; + if (level_flags < 0 || level_flags > 3) + { + level_flags = 3; + } + header |= level_flags << 6; + if ((state & IS_SETDICT) != 0) + { + // Dictionary was set + header |= DeflaterConstants.PRESET_DICT; + } + header += 31 - (header % 31); + + pending.WriteShortMSB(header); + if ((state & IS_SETDICT) != 0) + { + int chksum = engine.Adler; + engine.ResetAdler(); + pending.WriteShortMSB(chksum >> 16); + pending.WriteShortMSB(chksum & 0xffff); + } + + state = BUSY_STATE | (state & (IS_FLUSHING | IS_FINISHING)); + } + + for (; ; ) + { + int count = pending.Flush(output, offset, length); + offset += count; + totalOut += count; + length -= count; + + if (length == 0 || state == FINISHED_STATE) + { + break; + } + + if (!engine.Deflate((state & IS_FLUSHING) != 0, (state & IS_FINISHING) != 0)) + { + switch (state) + { + case BUSY_STATE: + // We need more input now + return origLength - length; + + case FLUSHING_STATE: + if (level != NO_COMPRESSION) + { + /* We have to supply some lookahead. 8 bit lookahead + * is needed by the zlib inflater, and we must fill + * the next byte, so that all bits are flushed. + */ + int neededbits = 8 + ((-pending.BitCount) & 7); + while (neededbits > 0) + { + /* write a static tree block consisting solely of + * an EOF: + */ + pending.WriteBits(2, 10); + neededbits -= 10; + } + } + state = BUSY_STATE; + break; + + case FINISHING_STATE: + pending.AlignToByte(); + + // Compressed data is complete. Write footer information if required. + if (!noZlibHeaderOrFooter) + { + int adler = engine.Adler; + pending.WriteShortMSB(adler >> 16); + pending.WriteShortMSB(adler & 0xffff); + } + state = FINISHED_STATE; + break; + } + } + } + return origLength - length; + } + + /// + /// Sets the dictionary which should be used in the deflate process. + /// This call is equivalent to setDictionary(dict, 0, dict.Length). + /// + /// + /// the dictionary. + /// + /// + /// if SetInput () or Deflate () were already called or another dictionary was already set. + /// + public void SetDictionary(byte[] dictionary) + { + SetDictionary(dictionary, 0, dictionary.Length); + } + + /// + /// Sets the dictionary which should be used in the deflate process. + /// The dictionary is a byte array containing strings that are + /// likely to occur in the data which should be compressed. The + /// dictionary is not stored in the compressed output, only a + /// checksum. To decompress the output you need to supply the same + /// dictionary again. + /// + /// + /// The dictionary data + /// + /// + /// The index where dictionary information commences. + /// + /// + /// The number of bytes in the dictionary. + /// + /// + /// If SetInput () or Deflate() were already called or another dictionary was already set. + /// + public void SetDictionary(byte[] dictionary, int index, int count) + { + if (state != INIT_STATE) + { + throw new InvalidOperationException(); + } + + state = SETDICT_STATE; + engine.SetDictionary(dictionary, index, count); + } + + #region Instance Fields + + /// + /// Compression level. + /// + private int level; + + /// + /// If true no Zlib/RFC1950 headers or footers are generated + /// + private bool noZlibHeaderOrFooter; + + /// + /// The current state. + /// + private int state; + + /// + /// The total bytes of output written. + /// + private long totalOut; + + /// + /// The pending output. + /// + private DeflaterPending pending; + + /// + /// The deflater engine. + /// + private DeflaterEngine engine; + + #endregion Instance Fields + } +} diff --git a/src/ImageSharp/Formats/Png/Zlib/DeflaterConstants.cs b/src/ImageSharp/Formats/Png/Zlib/DeflaterConstants.cs new file mode 100644 index 0000000000..67e8c6900b --- /dev/null +++ b/src/ImageSharp/Formats/Png/Zlib/DeflaterConstants.cs @@ -0,0 +1,151 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +// +using System; +using System.Collections.Generic; +using System.Text; + +namespace SixLabors.ImageSharp.Formats.Png.Zlib +{ + /// + /// This class contains constants used for deflation. + /// + public static class DeflaterConstants + { + /// + /// Set to true to enable debugging + /// + public const bool DEBUGGING = false; + + /// + /// Written to Zip file to identify a stored block + /// + public const int STORED_BLOCK = 0; + + /// + /// Identifies static tree in Zip file + /// + public const int STATIC_TREES = 1; + + /// + /// Identifies dynamic tree in Zip file + /// + public const int DYN_TREES = 2; + + /// + /// Header flag indicating a preset dictionary for deflation + /// + public const int PRESET_DICT = 0x20; + + /// + /// Sets internal buffer sizes for Huffman encoding + /// + public const int DEFAULT_MEM_LEVEL = 8; + + /// + /// Internal compression engine constant + /// + public const int MAX_MATCH = 258; + + /// + /// Internal compression engine constant + /// + public const int MIN_MATCH = 3; + + /// + /// Internal compression engine constant + /// + public const int MAX_WBITS = 15; + + /// + /// Internal compression engine constant + /// + public const int WSIZE = 1 << MAX_WBITS; + + /// + /// Internal compression engine constant + /// + public const int WMASK = WSIZE - 1; + + /// + /// Internal compression engine constant + /// + public const int HASH_BITS = DEFAULT_MEM_LEVEL + 7; + + /// + /// Internal compression engine constant + /// + public const int HASH_SIZE = 1 << HASH_BITS; + + /// + /// Internal compression engine constant + /// + public const int HASH_MASK = HASH_SIZE - 1; + + /// + /// Internal compression engine constant + /// + public const int HASH_SHIFT = (HASH_BITS + MIN_MATCH - 1) / MIN_MATCH; + + /// + /// Internal compression engine constant + /// + public const int MIN_LOOKAHEAD = MAX_MATCH + MIN_MATCH + 1; + + /// + /// Internal compression engine constant + /// + public const int MAX_DIST = WSIZE - MIN_LOOKAHEAD; + + /// + /// Internal compression engine constant + /// + public const int PENDING_BUF_SIZE = 1 << (DEFAULT_MEM_LEVEL + 8); + + /// + /// Internal compression engine constant + /// + public static int MAX_BLOCK_SIZE = Math.Min(65535, PENDING_BUF_SIZE - 5); + + /// + /// Internal compression engine constant + /// + public const int DEFLATE_STORED = 0; + + /// + /// Internal compression engine constant + /// + public const int DEFLATE_FAST = 1; + + /// + /// Internal compression engine constant + /// + public const int DEFLATE_SLOW = 2; + + /// + /// Internal compression engine constant + /// + public static int[] GOOD_LENGTH = { 0, 4, 4, 4, 4, 8, 8, 8, 32, 32 }; + + /// + /// Internal compression engine constant + /// + public static int[] MAX_LAZY = { 0, 4, 5, 6, 4, 16, 16, 32, 128, 258 }; + + /// + /// Internal compression engine constant + /// + public static int[] NICE_LENGTH = { 0, 8, 16, 32, 16, 32, 128, 128, 258, 258 }; + + /// + /// Internal compression engine constant + /// + public static int[] MAX_CHAIN = { 0, 4, 8, 32, 16, 32, 128, 256, 1024, 4096 }; + + /// + /// Internal compression engine constant + /// + public static int[] COMPR_FUNC = { 0, 1, 1, 1, 1, 2, 2, 2, 2, 2 }; + } +} diff --git a/src/ImageSharp/Formats/Png/Zlib/DeflaterEngine.cs b/src/ImageSharp/Formats/Png/Zlib/DeflaterEngine.cs new file mode 100644 index 0000000000..7ac5b6c69f --- /dev/null +++ b/src/ImageSharp/Formats/Png/Zlib/DeflaterEngine.cs @@ -0,0 +1,951 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +// +using System; +using System.Collections.Generic; +using System.Text; + +namespace SixLabors.ImageSharp.Formats.Png.Zlib +{ + /// + /// Strategies for deflater + /// + public enum DeflateStrategy + { + /// + /// The default strategy + /// + Default = 0, + + /// + /// This strategy will only allow longer string repetitions. It is + /// useful for random data with a small character set. + /// + Filtered = 1, + + /// + /// This strategy will not look for string repetitions at all. It + /// only encodes with Huffman trees (which means, that more common + /// characters get a smaller encoding. + /// + HuffmanOnly = 2 + } + + // DEFLATE ALGORITHM: + // + // The uncompressed stream is inserted into the window array. When + // the window array is full the first half is thrown away and the + // second half is copied to the beginning. + // + // The head array is a hash table. Three characters build a hash value + // and they the value points to the corresponding index in window of + // the last string with this hash. The prev array implements a + // linked list of matches with the same hash: prev[index & WMASK] points + // to the previous index with the same hash. + // + + /// + /// Low level compression engine for deflate algorithm which uses a 32K sliding window + /// with secondary compression from Huffman/Shannon-Fano codes. + /// + public class DeflaterEngine + { + #region Constants + + private const int TooFar = 4096; + + #endregion Constants + + #region Constructors + + /// + /// Construct instance with pending buffer + /// Adler calculation will be peformed + /// + /// + /// Pending buffer to use + /// + public DeflaterEngine(DeflaterPending pending) + : this(pending, false) + { + } + + + + /// + /// Construct instance with pending buffer + /// + /// + /// Pending buffer to use + /// + /// + /// If no adler calculation should be performed + /// + public DeflaterEngine(DeflaterPending pending, bool noAdlerCalculation) + { + this.pending = pending; + huffman = new DeflaterHuffman(pending); + if (!noAdlerCalculation) + adler = new Adler32(); + + window = new byte[2 * DeflaterConstants.WSIZE]; + head = new short[DeflaterConstants.HASH_SIZE]; + prev = new short[DeflaterConstants.WSIZE]; + + // We start at index 1, to avoid an implementation deficiency, that + // we cannot build a repeat pattern at index 0. + blockStart = strstart = 1; + } + + #endregion Constructors + + /// + /// Deflate drives actual compression of data + /// + /// True to flush input buffers + /// Finish deflation with the current input. + /// Returns true if progress has been made. + public bool Deflate(bool flush, bool finish) + { + bool progress; + do + { + FillWindow(); + bool canFlush = flush && (inputOff == inputEnd); + +#if DebugDeflation + if (DeflaterConstants.DEBUGGING) { + Console.WriteLine("window: [" + blockStart + "," + strstart + "," + + lookahead + "], " + compressionFunction + "," + canFlush); + } +#endif + switch (compressionFunction) + { + case DeflaterConstants.DEFLATE_STORED: + progress = DeflateStored(canFlush, finish); + break; + + case DeflaterConstants.DEFLATE_FAST: + progress = DeflateFast(canFlush, finish); + break; + + case DeflaterConstants.DEFLATE_SLOW: + progress = DeflateSlow(canFlush, finish); + break; + + default: + throw new InvalidOperationException("unknown compressionFunction"); + } + } while (pending.IsFlushed && progress); // repeat while we have no pending output and progress was made + return progress; + } + + /// + /// Sets input data to be deflated. Should only be called when NeedsInput() + /// returns true + /// + /// The buffer containing input data. + /// The offset of the first byte of data. + /// The number of bytes of data to use as input. + public void SetInput(byte[] buffer, int offset, int count) + { + if (buffer == null) + { + throw new ArgumentNullException(nameof(buffer)); + } + + if (offset < 0) + { + throw new ArgumentOutOfRangeException(nameof(offset)); + } + + if (count < 0) + { + throw new ArgumentOutOfRangeException(nameof(count)); + } + + if (inputOff < inputEnd) + { + throw new InvalidOperationException("Old input was not completely processed"); + } + + int end = offset + count; + + /* We want to throw an ArrayIndexOutOfBoundsException early. The + * check is very tricky: it also handles integer wrap around. + */ + if ((offset > end) || (end > buffer.Length)) + { + throw new ArgumentOutOfRangeException(nameof(count)); + } + + inputBuf = buffer; + inputOff = offset; + inputEnd = end; + } + + /// + /// Determines if more input is needed. + /// + /// Return true if input is needed via SetInput + public bool NeedsInput() + { + return (inputEnd == inputOff); + } + + /// + /// Set compression dictionary + /// + /// The buffer containing the dictionary data + /// The offset in the buffer for the first byte of data + /// The length of the dictionary data. + public void SetDictionary(byte[] buffer, int offset, int length) + { +#if DebugDeflation + if (DeflaterConstants.DEBUGGING && (strstart != 1) ) + { + throw new InvalidOperationException("strstart not 1"); + } +#endif + adler?.Update(new ArraySegment(buffer, offset, length)); + if (length < DeflaterConstants.MIN_MATCH) + { + return; + } + + if (length > DeflaterConstants.MAX_DIST) + { + offset += length - DeflaterConstants.MAX_DIST; + length = DeflaterConstants.MAX_DIST; + } + + System.Array.Copy(buffer, offset, window, strstart, length); + + UpdateHash(); + --length; + while (--length > 0) + { + InsertString(); + strstart++; + } + strstart += 2; + blockStart = strstart; + } + + /// + /// Reset internal state + /// + public void Reset() + { + huffman.Reset(); + adler?.Reset(); + blockStart = strstart = 1; + lookahead = 0; + totalIn = 0; + prevAvailable = false; + matchLen = DeflaterConstants.MIN_MATCH - 1; + + for (int i = 0; i < DeflaterConstants.HASH_SIZE; i++) + { + head[i] = 0; + } + + for (int i = 0; i < DeflaterConstants.WSIZE; i++) + { + prev[i] = 0; + } + } + + /// + /// Reset Adler checksum + /// + public void ResetAdler() + { + adler?.Reset(); + } + + /// + /// Get current value of Adler checksum + /// + public int Adler + { + get + { + return (adler != null) ? unchecked((int)adler.Value) : 0; + } + } + + /// + /// Total data processed + /// + public long TotalIn + { + get + { + return totalIn; + } + } + + /// + /// Get/set the deflate strategy + /// + public DeflateStrategy Strategy + { + get + { + return strategy; + } + set + { + strategy = value; + } + } + + /// + /// Set the deflate level (0-9) + /// + /// The value to set the level to. + public void SetLevel(int level) + { + if ((level < 0) || (level > 9)) + { + throw new ArgumentOutOfRangeException(nameof(level)); + } + + goodLength = DeflaterConstants.GOOD_LENGTH[level]; + max_lazy = DeflaterConstants.MAX_LAZY[level]; + niceLength = DeflaterConstants.NICE_LENGTH[level]; + max_chain = DeflaterConstants.MAX_CHAIN[level]; + + if (DeflaterConstants.COMPR_FUNC[level] != compressionFunction) + { +#if DebugDeflation + if (DeflaterConstants.DEBUGGING) { + Console.WriteLine("Change from " + compressionFunction + " to " + + DeflaterConstants.COMPR_FUNC[level]); + } +#endif + switch (compressionFunction) + { + case DeflaterConstants.DEFLATE_STORED: + if (strstart > blockStart) + { + huffman.FlushStoredBlock(window, blockStart, + strstart - blockStart, false); + blockStart = strstart; + } + UpdateHash(); + break; + + case DeflaterConstants.DEFLATE_FAST: + if (strstart > blockStart) + { + huffman.FlushBlock(window, blockStart, strstart - blockStart, + false); + blockStart = strstart; + } + break; + + case DeflaterConstants.DEFLATE_SLOW: + if (prevAvailable) + { + huffman.TallyLit(window[strstart - 1] & 0xff); + } + if (strstart > blockStart) + { + huffman.FlushBlock(window, blockStart, strstart - blockStart, false); + blockStart = strstart; + } + prevAvailable = false; + matchLen = DeflaterConstants.MIN_MATCH - 1; + break; + } + compressionFunction = DeflaterConstants.COMPR_FUNC[level]; + } + } + + /// + /// Fill the window + /// + public void FillWindow() + { + /* If the window is almost full and there is insufficient lookahead, + * move the upper half to the lower one to make room in the upper half. + */ + if (strstart >= DeflaterConstants.WSIZE + DeflaterConstants.MAX_DIST) + { + SlideWindow(); + } + + /* If there is not enough lookahead, but still some input left, + * read in the input + */ + if (lookahead < DeflaterConstants.MIN_LOOKAHEAD && inputOff < inputEnd) + { + int more = 2 * DeflaterConstants.WSIZE - lookahead - strstart; + + if (more > inputEnd - inputOff) + { + more = inputEnd - inputOff; + } + + System.Array.Copy(inputBuf, inputOff, window, strstart + lookahead, more); + adler?.Update(new ArraySegment(inputBuf, inputOff, more)); + + inputOff += more; + totalIn += more; + lookahead += more; + } + + if (lookahead >= DeflaterConstants.MIN_MATCH) + { + UpdateHash(); + } + } + + private void UpdateHash() + { + /* + if (DEBUGGING) { + Console.WriteLine("updateHash: "+strstart); + } + */ + ins_h = (window[strstart] << DeflaterConstants.HASH_SHIFT) ^ window[strstart + 1]; + } + + /// + /// Inserts the current string in the head hash and returns the previous + /// value for this hash. + /// + /// The previous hash value + private int InsertString() + { + short match; + int hash = ((ins_h << DeflaterConstants.HASH_SHIFT) ^ window[strstart + (DeflaterConstants.MIN_MATCH - 1)]) & DeflaterConstants.HASH_MASK; + +#if DebugDeflation + if (DeflaterConstants.DEBUGGING) + { + if (hash != (((window[strstart] << (2*HASH_SHIFT)) ^ + (window[strstart + 1] << HASH_SHIFT) ^ + (window[strstart + 2])) & HASH_MASK)) { + throw new ImageFormatException("hash inconsistent: " + hash + "/" + +window[strstart] + "," + +window[strstart + 1] + "," + +window[strstart + 2] + "," + HASH_SHIFT); + } + } +#endif + prev[strstart & DeflaterConstants.WMASK] = match = head[hash]; + head[hash] = unchecked((short)strstart); + ins_h = hash; + return match & 0xffff; + } + + private void SlideWindow() + { + Array.Copy(window, DeflaterConstants.WSIZE, window, 0, DeflaterConstants.WSIZE); + matchStart -= DeflaterConstants.WSIZE; + strstart -= DeflaterConstants.WSIZE; + blockStart -= DeflaterConstants.WSIZE; + + // Slide the hash table (could be avoided with 32 bit values + // at the expense of memory usage). + for (int i = 0; i < DeflaterConstants.HASH_SIZE; ++i) + { + int m = head[i] & 0xffff; + head[i] = (short)(m >= DeflaterConstants.WSIZE ? (m - DeflaterConstants.WSIZE) : 0); + } + + // Slide the prev table. + for (int i = 0; i < DeflaterConstants.WSIZE; i++) + { + int m = prev[i] & 0xffff; + prev[i] = (short)(m >= DeflaterConstants.WSIZE ? (m - DeflaterConstants.WSIZE) : 0); + } + } + + /// + /// Find the best (longest) string in the window matching the + /// string starting at strstart. + /// + /// Preconditions: + /// + /// strstart + DeflaterConstants.MAX_MATCH <= window.length. + /// + /// + /// True if a match greater than the minimum length is found + private bool FindLongestMatch(int curMatch) + { + int match; + int scan = strstart; + // scanMax is the highest position that we can look at + int scanMax = scan + Math.Min(DeflaterConstants.MAX_MATCH, lookahead) - 1; + int limit = Math.Max(scan - DeflaterConstants.MAX_DIST, 0); + + byte[] window = this.window; + short[] prev = this.prev; + int chainLength = this.max_chain; + int niceLength = Math.Min(this.niceLength, lookahead); + + matchLen = Math.Max(matchLen, DeflaterConstants.MIN_MATCH - 1); + + if (scan + matchLen > scanMax) return false; + + byte scan_end1 = window[scan + matchLen - 1]; + byte scan_end = window[scan + matchLen]; + + // Do not waste too much time if we already have a good match: + if (matchLen >= this.goodLength) chainLength >>= 2; + + do + { + match = curMatch; + scan = strstart; + + if (window[match + matchLen] != scan_end + || window[match + matchLen - 1] != scan_end1 + || window[match] != window[scan] + || window[++match] != window[++scan]) + { + continue; + } + + // scan is set to strstart+1 and the comparison passed, so + // scanMax - scan is the maximum number of bytes we can compare. + // below we compare 8 bytes at a time, so first we compare + // (scanMax - scan) % 8 bytes, so the remainder is a multiple of 8 + + switch ((scanMax - scan) % 8) + { + case 1: + if (window[++scan] == window[++match]) break; + break; + + case 2: + if (window[++scan] == window[++match] + && window[++scan] == window[++match]) break; + break; + + case 3: + if (window[++scan] == window[++match] + && window[++scan] == window[++match] + && window[++scan] == window[++match]) break; + break; + + case 4: + if (window[++scan] == window[++match] + && window[++scan] == window[++match] + && window[++scan] == window[++match] + && window[++scan] == window[++match]) break; + break; + + case 5: + if (window[++scan] == window[++match] + && window[++scan] == window[++match] + && window[++scan] == window[++match] + && window[++scan] == window[++match] + && window[++scan] == window[++match]) break; + break; + + case 6: + if (window[++scan] == window[++match] + && window[++scan] == window[++match] + && window[++scan] == window[++match] + && window[++scan] == window[++match] + && window[++scan] == window[++match] + && window[++scan] == window[++match]) break; + break; + + case 7: + if (window[++scan] == window[++match] + && window[++scan] == window[++match] + && window[++scan] == window[++match] + && window[++scan] == window[++match] + && window[++scan] == window[++match] + && window[++scan] == window[++match] + && window[++scan] == window[++match]) break; + break; + } + + if (window[scan] == window[match]) + { + /* We check for insufficient lookahead only every 8th comparison; + * the 256th check will be made at strstart + 258 unless lookahead is + * exhausted first. + */ + do + { + if (scan == scanMax) + { + ++scan; // advance to first position not matched + ++match; + + break; + } + } + while (window[++scan] == window[++match] + && window[++scan] == window[++match] + && window[++scan] == window[++match] + && window[++scan] == window[++match] + && window[++scan] == window[++match] + && window[++scan] == window[++match] + && window[++scan] == window[++match] + && window[++scan] == window[++match]); + } + + if (scan - strstart > matchLen) + { +#if DebugDeflation + if (DeflaterConstants.DEBUGGING && (ins_h == 0) ) + Console.Error.WriteLine("Found match: " + curMatch + "-" + (scan - strstart)); +#endif + + matchStart = curMatch; + matchLen = scan - strstart; + + if (matchLen >= niceLength) + break; + + scan_end1 = window[scan - 1]; + scan_end = window[scan]; + } + } while ((curMatch = (prev[curMatch & DeflaterConstants.WMASK] & 0xffff)) > limit && 0 != --chainLength); + + return matchLen >= DeflaterConstants.MIN_MATCH; + } + + private bool DeflateStored(bool flush, bool finish) + { + if (!flush && (lookahead == 0)) + { + return false; + } + + strstart += lookahead; + lookahead = 0; + + int storedLength = strstart - blockStart; + + if ((storedLength >= DeflaterConstants.MAX_BLOCK_SIZE) || // Block is full + (blockStart < DeflaterConstants.WSIZE && storedLength >= DeflaterConstants.MAX_DIST) || // Block may move out of window + flush) + { + bool lastBlock = finish; + if (storedLength > DeflaterConstants.MAX_BLOCK_SIZE) + { + storedLength = DeflaterConstants.MAX_BLOCK_SIZE; + lastBlock = false; + } + +#if DebugDeflation + if (DeflaterConstants.DEBUGGING) + { + Console.WriteLine("storedBlock[" + storedLength + "," + lastBlock + "]"); + } +#endif + + huffman.FlushStoredBlock(window, blockStart, storedLength, lastBlock); + blockStart += storedLength; + return !(lastBlock || storedLength == 0); + } + return true; + } + + private bool DeflateFast(bool flush, bool finish) + { + if (lookahead < DeflaterConstants.MIN_LOOKAHEAD && !flush) + { + return false; + } + + while (lookahead >= DeflaterConstants.MIN_LOOKAHEAD || flush) + { + if (lookahead == 0) + { + // We are flushing everything + huffman.FlushBlock(window, blockStart, strstart - blockStart, finish); + blockStart = strstart; + return false; + } + + if (strstart > 2 * DeflaterConstants.WSIZE - DeflaterConstants.MIN_LOOKAHEAD) + { + /* slide window, as FindLongestMatch needs this. + * This should only happen when flushing and the window + * is almost full. + */ + SlideWindow(); + } + + int hashHead; + if (lookahead >= DeflaterConstants.MIN_MATCH && + (hashHead = InsertString()) != 0 && + strategy != DeflateStrategy.HuffmanOnly && + strstart - hashHead <= DeflaterConstants.MAX_DIST && + FindLongestMatch(hashHead)) + { + // longestMatch sets matchStart and matchLen +#if DebugDeflation + if (DeflaterConstants.DEBUGGING) + { + for (int i = 0 ; i < matchLen; i++) { + if (window[strstart + i] != window[matchStart + i]) { + throw new ImageFormatException("Match failure"); + } + } + } +#endif + + bool full = huffman.TallyDist(strstart - matchStart, matchLen); + + lookahead -= matchLen; + if (matchLen <= max_lazy && lookahead >= DeflaterConstants.MIN_MATCH) + { + while (--matchLen > 0) + { + ++strstart; + InsertString(); + } + ++strstart; + } + else + { + strstart += matchLen; + if (lookahead >= DeflaterConstants.MIN_MATCH - 1) + { + UpdateHash(); + } + } + matchLen = DeflaterConstants.MIN_MATCH - 1; + if (!full) + { + continue; + } + } + else + { + // No match found + huffman.TallyLit(window[strstart] & 0xff); + ++strstart; + --lookahead; + } + + if (huffman.IsFull()) + { + bool lastBlock = finish && (lookahead == 0); + huffman.FlushBlock(window, blockStart, strstart - blockStart, lastBlock); + blockStart = strstart; + return !lastBlock; + } + } + return true; + } + + private bool DeflateSlow(bool flush, bool finish) + { + if (lookahead < DeflaterConstants.MIN_LOOKAHEAD && !flush) + { + return false; + } + + while (lookahead >= DeflaterConstants.MIN_LOOKAHEAD || flush) + { + if (lookahead == 0) + { + if (prevAvailable) + { + huffman.TallyLit(window[strstart - 1] & 0xff); + } + prevAvailable = false; + + // We are flushing everything +#if DebugDeflation + if (DeflaterConstants.DEBUGGING && !flush) + { + throw new ImageFormatException("Not flushing, but no lookahead"); + } +#endif + huffman.FlushBlock(window, blockStart, strstart - blockStart, + finish); + blockStart = strstart; + return false; + } + + if (strstart >= 2 * DeflaterConstants.WSIZE - DeflaterConstants.MIN_LOOKAHEAD) + { + /* slide window, as FindLongestMatch needs this. + * This should only happen when flushing and the window + * is almost full. + */ + SlideWindow(); + } + + int prevMatch = matchStart; + int prevLen = matchLen; + if (lookahead >= DeflaterConstants.MIN_MATCH) + { + int hashHead = InsertString(); + + if (strategy != DeflateStrategy.HuffmanOnly && + hashHead != 0 && + strstart - hashHead <= DeflaterConstants.MAX_DIST && + FindLongestMatch(hashHead)) + { + // longestMatch sets matchStart and matchLen + + // Discard match if too small and too far away + if (matchLen <= 5 && (strategy == DeflateStrategy.Filtered || (matchLen == DeflaterConstants.MIN_MATCH && strstart - matchStart > TooFar))) + { + matchLen = DeflaterConstants.MIN_MATCH - 1; + } + } + } + + // previous match was better + if ((prevLen >= DeflaterConstants.MIN_MATCH) && (matchLen <= prevLen)) + { +#if DebugDeflation + if (DeflaterConstants.DEBUGGING) + { + for (int i = 0 ; i < matchLen; i++) { + if (window[strstart-1+i] != window[prevMatch + i]) + throw new ImageFormatException(); + } + } +#endif + huffman.TallyDist(strstart - 1 - prevMatch, prevLen); + prevLen -= 2; + do + { + strstart++; + lookahead--; + if (lookahead >= DeflaterConstants.MIN_MATCH) + { + InsertString(); + } + } while (--prevLen > 0); + + strstart++; + lookahead--; + prevAvailable = false; + matchLen = DeflaterConstants.MIN_MATCH - 1; + } + else + { + if (prevAvailable) + { + huffman.TallyLit(window[strstart - 1] & 0xff); + } + prevAvailable = true; + strstart++; + lookahead--; + } + + if (huffman.IsFull()) + { + int len = strstart - blockStart; + if (prevAvailable) + { + len--; + } + bool lastBlock = (finish && (lookahead == 0) && !prevAvailable); + huffman.FlushBlock(window, blockStart, len, lastBlock); + blockStart += len; + return !lastBlock; + } + } + return true; + } + + #region Instance Fields + + // Hash index of string to be inserted + private int ins_h; + + /// + /// Hashtable, hashing three characters to an index for window, so + /// that window[index]..window[index+2] have this hash code. + /// Note that the array should really be unsigned short, so you need + /// to and the values with 0xffff. + /// + private short[] head; + + /// + /// prev[index & WMASK] points to the previous index that has the + /// same hash code as the string starting at index. This way + /// entries with the same hash code are in a linked list. + /// Note that the array should really be unsigned short, so you need + /// to and the values with 0xffff. + /// + private short[] prev; + + private int matchStart; + + // Length of best match + private int matchLen; + + // Set if previous match exists + private bool prevAvailable; + + private int blockStart; + + /// + /// Points to the current character in the window. + /// + private int strstart; + + /// + /// lookahead is the number of characters starting at strstart in + /// window that are valid. + /// So window[strstart] until window[strstart+lookahead-1] are valid + /// characters. + /// + private int lookahead; + + /// + /// This array contains the part of the uncompressed stream that + /// is of relevance. The current character is indexed by strstart. + /// + private byte[] window; + + private DeflateStrategy strategy; + private int max_chain, max_lazy, niceLength, goodLength; + + /// + /// The current compression function. + /// + private int compressionFunction; + + /// + /// The input data for compression. + /// + private byte[] inputBuf; + + /// + /// The total bytes of input read. + /// + private long totalIn; + + /// + /// The offset into inputBuf, where input data starts. + /// + private int inputOff; + + /// + /// The end offset of the input data. + /// + private int inputEnd; + + private DeflaterPending pending; + private DeflaterHuffman huffman; + + /// + /// The adler checksum + /// + private Adler32 adler; + + #endregion Instance Fields + } +} diff --git a/src/ImageSharp/Formats/Png/Zlib/DeflaterHuffman.cs b/src/ImageSharp/Formats/Png/Zlib/DeflaterHuffman.cs new file mode 100644 index 0000000000..bf506d14c9 --- /dev/null +++ b/src/ImageSharp/Formats/Png/Zlib/DeflaterHuffman.cs @@ -0,0 +1,965 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +// +using System; +using System.Collections.Generic; +using System.Text; + +namespace SixLabors.ImageSharp.Formats.Png.Zlib +{ + /// + /// This is the DeflaterHuffman class. + /// + /// This class is not thread safe. This is inherent in the API, due + /// to the split of Deflate and SetInput. + /// + /// author of the original java version : Jochen Hoenicke + /// + public class DeflaterHuffman + { + private const int BUFSIZE = 1 << (DeflaterConstants.DEFAULT_MEM_LEVEL + 6); + private const int LITERAL_NUM = 286; + + // Number of distance codes + private const int DIST_NUM = 30; + + // Number of codes used to transfer bit lengths + private const int BITLEN_NUM = 19; + + // repeat previous bit length 3-6 times (2 bits of repeat count) + private const int REP_3_6 = 16; + + // repeat a zero length 3-10 times (3 bits of repeat count) + private const int REP_3_10 = 17; + + // repeat a zero length 11-138 times (7 bits of repeat count) + private const int REP_11_138 = 18; + + private const int EOF_SYMBOL = 256; + + // The lengths of the bit length codes are sent in order of decreasing + // probability, to avoid transmitting the lengths for unused bit length codes. + private static readonly int[] BL_ORDER = { 16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15 }; + + private static readonly byte[] bit4Reverse = { + 0, + 8, + 4, + 12, + 2, + 10, + 6, + 14, + 1, + 9, + 5, + 13, + 3, + 11, + 7, + 15 + }; + + private static short[] staticLCodes; + private static byte[] staticLLength; + private static short[] staticDCodes; + private static byte[] staticDLength; + + private class Tree + { + #region Instance Fields + + public short[] freqs; + + public byte[] length; + + public int minNumCodes; + + public int numCodes; + + private short[] codes; + private readonly int[] bl_counts; + private readonly int maxLength; + private DeflaterHuffman dh; + + #endregion Instance Fields + + #region Constructors + + public Tree(DeflaterHuffman dh, int elems, int minCodes, int maxLength) + { + this.dh = dh; + this.minNumCodes = minCodes; + this.maxLength = maxLength; + freqs = new short[elems]; + bl_counts = new int[maxLength]; + } + + #endregion Constructors + + /// + /// Resets the internal state of the tree + /// + public void Reset() + { + for (int i = 0; i < freqs.Length; i++) + { + freqs[i] = 0; + } + codes = null; + length = null; + } + + public void WriteSymbol(int code) + { + // if (DeflaterConstants.DEBUGGING) { + // freqs[code]--; + // // Console.Write("writeSymbol("+freqs.length+","+code+"): "); + // } + dh.pending.WriteBits(codes[code] & 0xffff, length[code]); + } + + /// + /// Check that all frequencies are zero + /// + /// + /// At least one frequency is non-zero + /// + public void CheckEmpty() + { + bool empty = true; + for (int i = 0; i < freqs.Length; i++) + { + empty &= freqs[i] == 0; + } + + if (!empty) + { + throw new ImageFormatException("!Empty"); + } + } + + /// + /// Set static codes and length + /// + /// new codes + /// length for new codes + public void SetStaticCodes(short[] staticCodes, byte[] staticLengths) + { + codes = staticCodes; + length = staticLengths; + } + + /// + /// Build dynamic codes and lengths + /// + public void BuildCodes() + { + int numSymbols = freqs.Length; + int[] nextCode = new int[maxLength]; + int code = 0; + + codes = new short[freqs.Length]; + + // if (DeflaterConstants.DEBUGGING) { + // //Console.WriteLine("buildCodes: "+freqs.Length); + // } + + for (int bits = 0; bits < maxLength; bits++) + { + nextCode[bits] = code; + code += bl_counts[bits] << (15 - bits); + + // if (DeflaterConstants.DEBUGGING) { + // //Console.WriteLine("bits: " + ( bits + 1) + " count: " + bl_counts[bits] + // +" nextCode: "+code); + // } + } + +#if DebugDeflation + if ( DeflaterConstants.DEBUGGING && (code != 65536) ) + { + throw new ImageFormatException("Inconsistent bl_counts!"); + } +#endif + for (int i = 0; i < numCodes; i++) + { + int bits = length[i]; + if (bits > 0) + { + // if (DeflaterConstants.DEBUGGING) { + // //Console.WriteLine("codes["+i+"] = rev(" + nextCode[bits-1]+"), + // +bits); + // } + + codes[i] = BitReverse(nextCode[bits - 1]); + nextCode[bits - 1] += 1 << (16 - bits); + } + } + } + + public void BuildTree() + { + int numSymbols = freqs.Length; + + /* heap is a priority queue, sorted by frequency, least frequent + * nodes first. The heap is a binary tree, with the property, that + * the parent node is smaller than both child nodes. This assures + * that the smallest node is the first parent. + * + * The binary tree is encoded in an array: 0 is root node and + * the nodes 2*n+1, 2*n+2 are the child nodes of node n. + */ + int[] heap = new int[numSymbols]; + int heapLen = 0; + int maxCode = 0; + for (int n = 0; n < numSymbols; n++) + { + int freq = freqs[n]; + if (freq != 0) + { + // Insert n into heap + int pos = heapLen++; + int ppos; + while (pos > 0 && freqs[heap[ppos = (pos - 1) / 2]] > freq) + { + heap[pos] = heap[ppos]; + pos = ppos; + } + heap[pos] = n; + + maxCode = n; + } + } + + /* We could encode a single literal with 0 bits but then we + * don't see the literals. Therefore we force at least two + * literals to avoid this case. We don't care about order in + * this case, both literals get a 1 bit code. + */ + while (heapLen < 2) + { + int node = maxCode < 2 ? ++maxCode : 0; + heap[heapLen++] = node; + } + + numCodes = Math.Max(maxCode + 1, minNumCodes); + + int numLeafs = heapLen; + int[] childs = new int[4 * heapLen - 2]; + int[] values = new int[2 * heapLen - 1]; + int numNodes = numLeafs; + for (int i = 0; i < heapLen; i++) + { + int node = heap[i]; + childs[2 * i] = node; + childs[2 * i + 1] = -1; + values[i] = freqs[node] << 8; + heap[i] = i; + } + + /* Construct the Huffman tree by repeatedly combining the least two + * frequent nodes. + */ + do + { + int first = heap[0]; + int last = heap[--heapLen]; + + // Propagate the hole to the leafs of the heap + int ppos = 0; + int path = 1; + + while (path < heapLen) + { + if (path + 1 < heapLen && values[heap[path]] > values[heap[path + 1]]) + { + path++; + } + + heap[ppos] = heap[path]; + ppos = path; + path = path * 2 + 1; + } + + /* Now propagate the last element down along path. Normally + * it shouldn't go too deep. + */ + int lastVal = values[last]; + while ((path = ppos) > 0 && values[heap[ppos = (path - 1) / 2]] > lastVal) + { + heap[path] = heap[ppos]; + } + heap[path] = last; + + int second = heap[0]; + + // Create a new node father of first and second + last = numNodes++; + childs[2 * last] = first; + childs[2 * last + 1] = second; + int mindepth = Math.Min(values[first] & 0xff, values[second] & 0xff); + values[last] = lastVal = values[first] + values[second] - mindepth + 1; + + // Again, propagate the hole to the leafs + ppos = 0; + path = 1; + + while (path < heapLen) + { + if (path + 1 < heapLen && values[heap[path]] > values[heap[path + 1]]) + { + path++; + } + + heap[ppos] = heap[path]; + ppos = path; + path = ppos * 2 + 1; + } + + // Now propagate the new element down along path + while ((path = ppos) > 0 && values[heap[ppos = (path - 1) / 2]] > lastVal) + { + heap[path] = heap[ppos]; + } + heap[path] = last; + } while (heapLen > 1); + + if (heap[0] != childs.Length / 2 - 1) + { + throw new ImageFormatException("Heap invariant violated"); + } + + BuildLength(childs); + } + + /// + /// Get encoded length + /// + /// Encoded length, the sum of frequencies * lengths + public int GetEncodedLength() + { + int len = 0; + for (int i = 0; i < freqs.Length; i++) + { + len += freqs[i] * length[i]; + } + return len; + } + + /// + /// Scan a literal or distance tree to determine the frequencies of the codes + /// in the bit length tree. + /// + public void CalcBLFreq(Tree blTree) + { + int max_count; /* max repeat count */ + int min_count; /* min repeat count */ + int count; /* repeat count of the current code */ + int curlen = -1; /* length of current code */ + + int i = 0; + while (i < numCodes) + { + count = 1; + int nextlen = length[i]; + if (nextlen == 0) + { + max_count = 138; + min_count = 3; + } + else + { + max_count = 6; + min_count = 3; + if (curlen != nextlen) + { + blTree.freqs[nextlen]++; + count = 0; + } + } + curlen = nextlen; + i++; + + while (i < numCodes && curlen == length[i]) + { + i++; + if (++count >= max_count) + { + break; + } + } + + if (count < min_count) + { + blTree.freqs[curlen] += (short)count; + } + else if (curlen != 0) + { + blTree.freqs[REP_3_6]++; + } + else if (count <= 10) + { + blTree.freqs[REP_3_10]++; + } + else + { + blTree.freqs[REP_11_138]++; + } + } + } + + /// + /// Write tree values + /// + /// Tree to write + public void WriteTree(Tree blTree) + { + int max_count; // max repeat count + int min_count; // min repeat count + int count; // repeat count of the current code + int curlen = -1; // length of current code + + int i = 0; + while (i < numCodes) + { + count = 1; + int nextlen = length[i]; + if (nextlen == 0) + { + max_count = 138; + min_count = 3; + } + else + { + max_count = 6; + min_count = 3; + if (curlen != nextlen) + { + blTree.WriteSymbol(nextlen); + count = 0; + } + } + curlen = nextlen; + i++; + + while (i < numCodes && curlen == length[i]) + { + i++; + if (++count >= max_count) + { + break; + } + } + + if (count < min_count) + { + while (count-- > 0) + { + blTree.WriteSymbol(curlen); + } + } + else if (curlen != 0) + { + blTree.WriteSymbol(REP_3_6); + dh.pending.WriteBits(count - 3, 2); + } + else if (count <= 10) + { + blTree.WriteSymbol(REP_3_10); + dh.pending.WriteBits(count - 3, 3); + } + else + { + blTree.WriteSymbol(REP_11_138); + dh.pending.WriteBits(count - 11, 7); + } + } + } + + private void BuildLength(int[] childs) + { + this.length = new byte[freqs.Length]; + int numNodes = childs.Length / 2; + int numLeafs = (numNodes + 1) / 2; + int overflow = 0; + + for (int i = 0; i < maxLength; i++) + { + bl_counts[i] = 0; + } + + // First calculate optimal bit lengths + int[] lengths = new int[numNodes]; + lengths[numNodes - 1] = 0; + + for (int i = numNodes - 1; i >= 0; i--) + { + if (childs[2 * i + 1] != -1) + { + int bitLength = lengths[i] + 1; + if (bitLength > maxLength) + { + bitLength = maxLength; + overflow++; + } + lengths[childs[2 * i]] = lengths[childs[2 * i + 1]] = bitLength; + } + else + { + // A leaf node + int bitLength = lengths[i]; + bl_counts[bitLength - 1]++; + this.length[childs[2 * i]] = (byte)lengths[i]; + } + } + + // if (DeflaterConstants.DEBUGGING) { + // //Console.WriteLine("Tree "+freqs.Length+" lengths:"); + // for (int i=0; i < numLeafs; i++) { + // //Console.WriteLine("Node "+childs[2*i]+" freq: "+freqs[childs[2*i]] + // + " len: "+length[childs[2*i]]); + // } + // } + + if (overflow == 0) + { + return; + } + + int incrBitLen = maxLength - 1; + do + { + // Find the first bit length which could increase: + while (bl_counts[--incrBitLen] == 0) + { + } + + // Move this node one down and remove a corresponding + // number of overflow nodes. + do + { + bl_counts[incrBitLen]--; + bl_counts[++incrBitLen]++; + overflow -= 1 << (maxLength - 1 - incrBitLen); + } while (overflow > 0 && incrBitLen < maxLength - 1); + } while (overflow > 0); + + /* We may have overshot above. Move some nodes from maxLength to + * maxLength-1 in that case. + */ + bl_counts[maxLength - 1] += overflow; + bl_counts[maxLength - 2] -= overflow; + + /* Now recompute all bit lengths, scanning in increasing + * frequency. It is simpler to reconstruct all lengths instead of + * fixing only the wrong ones. This idea is taken from 'ar' + * written by Haruhiko Okumura. + * + * The nodes were inserted with decreasing frequency into the childs + * array. + */ + int nodePtr = 2 * numLeafs; + for (int bits = maxLength; bits != 0; bits--) + { + int n = bl_counts[bits - 1]; + while (n > 0) + { + int childPtr = 2 * childs[nodePtr++]; + if (childs[childPtr + 1] == -1) + { + // We found another leaf + length[childs[childPtr]] = (byte)bits; + n--; + } + } + } + // if (DeflaterConstants.DEBUGGING) { + // //Console.WriteLine("*** After overflow elimination. ***"); + // for (int i=0; i < numLeafs; i++) { + // //Console.WriteLine("Node "+childs[2*i]+" freq: "+freqs[childs[2*i]] + // + " len: "+length[childs[2*i]]); + // } + // } + } + } + + #region Instance Fields + + /// + /// Pending buffer to use + /// + public DeflaterPending pending; + + private Tree literalTree; + private Tree distTree; + private Tree blTree; + + // Buffer for distances + private short[] d_buf; + + private byte[] l_buf; + private int last_lit; + private int extra_bits; + + #endregion Instance Fields + + static DeflaterHuffman() + { + // See RFC 1951 3.2.6 + // Literal codes + staticLCodes = new short[LITERAL_NUM]; + staticLLength = new byte[LITERAL_NUM]; + + int i = 0; + while (i < 144) + { + staticLCodes[i] = BitReverse((0x030 + i) << 8); + staticLLength[i++] = 8; + } + + while (i < 256) + { + staticLCodes[i] = BitReverse((0x190 - 144 + i) << 7); + staticLLength[i++] = 9; + } + + while (i < 280) + { + staticLCodes[i] = BitReverse((0x000 - 256 + i) << 9); + staticLLength[i++] = 7; + } + + while (i < LITERAL_NUM) + { + staticLCodes[i] = BitReverse((0x0c0 - 280 + i) << 8); + staticLLength[i++] = 8; + } + + // Distance codes + staticDCodes = new short[DIST_NUM]; + staticDLength = new byte[DIST_NUM]; + for (i = 0; i < DIST_NUM; i++) + { + staticDCodes[i] = BitReverse(i << 11); + staticDLength[i] = 5; + } + } + + /// + /// Construct instance with pending buffer + /// + /// Pending buffer to use + public DeflaterHuffman(DeflaterPending pending) + { + this.pending = pending; + + literalTree = new Tree(this, LITERAL_NUM, 257, 15); + distTree = new Tree(this, DIST_NUM, 1, 15); + blTree = new Tree(this, BITLEN_NUM, 4, 7); + + d_buf = new short[BUFSIZE]; + l_buf = new byte[BUFSIZE]; + } + + /// + /// Reset internal state + /// + public void Reset() + { + last_lit = 0; + extra_bits = 0; + literalTree.Reset(); + distTree.Reset(); + blTree.Reset(); + } + + /// + /// Write all trees to pending buffer + /// + /// The number/rank of treecodes to send. + public void SendAllTrees(int blTreeCodes) + { + blTree.BuildCodes(); + literalTree.BuildCodes(); + distTree.BuildCodes(); + pending.WriteBits(literalTree.numCodes - 257, 5); + pending.WriteBits(distTree.numCodes - 1, 5); + pending.WriteBits(blTreeCodes - 4, 4); + for (int rank = 0; rank < blTreeCodes; rank++) + { + pending.WriteBits(blTree.length[BL_ORDER[rank]], 3); + } + literalTree.WriteTree(blTree); + distTree.WriteTree(blTree); + +#if DebugDeflation + if (DeflaterConstants.DEBUGGING) { + blTree.CheckEmpty(); + } +#endif + } + + /// + /// Compress current buffer writing data to pending buffer + /// + public void CompressBlock() + { + for (int i = 0; i < last_lit; i++) + { + int litlen = l_buf[i] & 0xff; + int dist = d_buf[i]; + if (dist-- != 0) + { + // if (DeflaterConstants.DEBUGGING) { + // Console.Write("["+(dist+1)+","+(litlen+3)+"]: "); + // } + + int lc = Lcode(litlen); + literalTree.WriteSymbol(lc); + + int bits = (lc - 261) / 4; + if (bits > 0 && bits <= 5) + { + pending.WriteBits(litlen & ((1 << bits) - 1), bits); + } + + int dc = Dcode(dist); + distTree.WriteSymbol(dc); + + bits = dc / 2 - 1; + if (bits > 0) + { + pending.WriteBits(dist & ((1 << bits) - 1), bits); + } + } + else + { + // if (DeflaterConstants.DEBUGGING) { + // if (litlen > 32 && litlen < 127) { + // Console.Write("("+(char)litlen+"): "); + // } else { + // Console.Write("{"+litlen+"}: "); + // } + // } + literalTree.WriteSymbol(litlen); + } + } + +#if DebugDeflation + if (DeflaterConstants.DEBUGGING) { + Console.Write("EOF: "); + } +#endif + literalTree.WriteSymbol(EOF_SYMBOL); + +#if DebugDeflation + if (DeflaterConstants.DEBUGGING) { + literalTree.CheckEmpty(); + distTree.CheckEmpty(); + } +#endif + } + + /// + /// Flush block to output with no compression + /// + /// Data to write + /// Index of first byte to write + /// Count of bytes to write + /// True if this is the last block + public void FlushStoredBlock(byte[] stored, int storedOffset, int storedLength, bool lastBlock) + { +#if DebugDeflation + // if (DeflaterConstants.DEBUGGING) { + // //Console.WriteLine("Flushing stored block "+ storedLength); + // } +#endif + pending.WriteBits((DeflaterConstants.STORED_BLOCK << 1) + (lastBlock ? 1 : 0), 3); + pending.AlignToByte(); + pending.WriteShort(storedLength); + pending.WriteShort(~storedLength); + pending.WriteBlock(stored, storedOffset, storedLength); + Reset(); + } + + /// + /// Flush block to output with compression + /// + /// Data to flush + /// Index of first byte to flush + /// Count of bytes to flush + /// True if this is the last block + public void FlushBlock(byte[] stored, int storedOffset, int storedLength, bool lastBlock) + { + literalTree.freqs[EOF_SYMBOL]++; + + // Build trees + literalTree.BuildTree(); + distTree.BuildTree(); + + // Calculate bitlen frequency + literalTree.CalcBLFreq(blTree); + distTree.CalcBLFreq(blTree); + + // Build bitlen tree + blTree.BuildTree(); + + int blTreeCodes = 4; + for (int i = 18; i > blTreeCodes; i--) + { + if (blTree.length[BL_ORDER[i]] > 0) + { + blTreeCodes = i + 1; + } + } + int opt_len = 14 + blTreeCodes * 3 + blTree.GetEncodedLength() + + literalTree.GetEncodedLength() + distTree.GetEncodedLength() + + extra_bits; + + int static_len = extra_bits; + for (int i = 0; i < LITERAL_NUM; i++) + { + static_len += literalTree.freqs[i] * staticLLength[i]; + } + for (int i = 0; i < DIST_NUM; i++) + { + static_len += distTree.freqs[i] * staticDLength[i]; + } + if (opt_len >= static_len) + { + // Force static trees + opt_len = static_len; + } + + if (storedOffset >= 0 && storedLength + 4 < opt_len >> 3) + { + // Store Block + + // if (DeflaterConstants.DEBUGGING) { + // //Console.WriteLine("Storing, since " + storedLength + " < " + opt_len + // + " <= " + static_len); + // } + FlushStoredBlock(stored, storedOffset, storedLength, lastBlock); + } + else if (opt_len == static_len) + { + // Encode with static tree + pending.WriteBits((DeflaterConstants.STATIC_TREES << 1) + (lastBlock ? 1 : 0), 3); + literalTree.SetStaticCodes(staticLCodes, staticLLength); + distTree.SetStaticCodes(staticDCodes, staticDLength); + CompressBlock(); + Reset(); + } + else + { + // Encode with dynamic tree + pending.WriteBits((DeflaterConstants.DYN_TREES << 1) + (lastBlock ? 1 : 0), 3); + SendAllTrees(blTreeCodes); + CompressBlock(); + Reset(); + } + } + + /// + /// Get value indicating if internal buffer is full + /// + /// true if buffer is full + public bool IsFull() + { + return last_lit >= BUFSIZE; + } + + /// + /// Add literal to buffer + /// + /// Literal value to add to buffer. + /// Value indicating internal buffer is full + public bool TallyLit(int literal) + { + // if (DeflaterConstants.DEBUGGING) { + // if (lit > 32 && lit < 127) { + // //Console.WriteLine("("+(char)lit+")"); + // } else { + // //Console.WriteLine("{"+lit+"}"); + // } + // } + d_buf[last_lit] = 0; + l_buf[last_lit++] = (byte)literal; + literalTree.freqs[literal]++; + return IsFull(); + } + + /// + /// Add distance code and length to literal and distance trees + /// + /// Distance code + /// Length + /// Value indicating if internal buffer is full + public bool TallyDist(int distance, int length) + { + // if (DeflaterConstants.DEBUGGING) { + // //Console.WriteLine("[" + distance + "," + length + "]"); + // } + + d_buf[last_lit] = (short)distance; + l_buf[last_lit++] = (byte)(length - 3); + + int lc = Lcode(length - 3); + literalTree.freqs[lc]++; + if (lc >= 265 && lc < 285) + { + extra_bits += (lc - 261) / 4; + } + + int dc = Dcode(distance - 1); + distTree.freqs[dc]++; + if (dc >= 4) + { + extra_bits += dc / 2 - 1; + } + return IsFull(); + } + + /// + /// Reverse the bits of a 16 bit value. + /// + /// Value to reverse bits + /// Value with bits reversed + public static short BitReverse(int toReverse) + { + return (short)(bit4Reverse[toReverse & 0xF] << 12 | + bit4Reverse[(toReverse >> 4) & 0xF] << 8 | + bit4Reverse[(toReverse >> 8) & 0xF] << 4 | + bit4Reverse[toReverse >> 12]); + } + + private static int Lcode(int length) + { + if (length == 255) + { + return 285; + } + + int code = 257; + while (length >= 8) + { + code += 4; + length >>= 1; + } + return code + length; + } + + private static int Dcode(int distance) + { + int code = 0; + while (distance >= 4) + { + code += 2; + distance >>= 1; + } + return code + distance; + } + } +} diff --git a/src/ImageSharp/Formats/Png/Zlib/DeflaterOutputStream.cs b/src/ImageSharp/Formats/Png/Zlib/DeflaterOutputStream.cs new file mode 100644 index 0000000000..31db032aec --- /dev/null +++ b/src/ImageSharp/Formats/Png/Zlib/DeflaterOutputStream.cs @@ -0,0 +1,499 @@ +// 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.Text; + +namespace SixLabors.ImageSharp.Formats.Png.Zlib +{ + /// + /// A special stream deflating or compressing the bytes that are + /// written to it. It uses a Deflater to perform actual deflating.
+ /// Authors of the original java version : Tom Tromey, Jochen Hoenicke + ///
+ public class DeflaterOutputStream : Stream + { + #region Constructors + + /// + /// Creates a new DeflaterOutputStream with a default Deflater and default buffer size. + /// + /// + /// the output stream where deflated output should be written. + /// + public DeflaterOutputStream(Stream baseOutputStream) + : this(baseOutputStream, new Deflater(), 512) + { + } + + /// + /// Creates a new DeflaterOutputStream with the given Deflater and + /// default buffer size. + /// + /// + /// the output stream where deflated output should be written. + /// + /// + /// the underlying deflater. + /// + public DeflaterOutputStream(Stream baseOutputStream, Deflater deflater) + : this(baseOutputStream, deflater, 512) + { + } + + /// + /// Creates a new DeflaterOutputStream with the given Deflater and + /// buffer size. + /// + /// + /// The output stream where deflated output is written. + /// + /// + /// The underlying deflater to use + /// + /// + /// The buffer size in bytes to use when deflating (minimum value 512) + /// + /// + /// bufsize is less than or equal to zero. + /// + /// + /// baseOutputStream does not support writing + /// + /// + /// deflater instance is null + /// + public DeflaterOutputStream(Stream baseOutputStream, Deflater deflater, int bufferSize) + { + if (baseOutputStream == null) + { + throw new ArgumentNullException(nameof(baseOutputStream)); + } + + if (baseOutputStream.CanWrite == false) + { + throw new ArgumentException("Must support writing", nameof(baseOutputStream)); + } + + if (bufferSize < 512) + { + throw new ArgumentOutOfRangeException(nameof(bufferSize)); + } + + baseOutputStream_ = baseOutputStream; + buffer_ = new byte[bufferSize]; + deflater_ = deflater ?? throw new ArgumentNullException(nameof(deflater)); + } + + #endregion Constructors + + #region Public API + + /// + /// Finishes the stream by calling finish() on the deflater. + /// + /// + /// Not all input is deflated + /// + public virtual void Finish() + { + deflater_.Finish(); + while (!deflater_.IsFinished) + { + int len = deflater_.Deflate(buffer_, 0, buffer_.Length); + if (len <= 0) + { + break; + } + + //if (cryptoTransform_ != null) + //{ + // EncryptBlock(buffer_, 0, len); + //} + + baseOutputStream_.Write(buffer_, 0, len); + } + + if (!deflater_.IsFinished) + { + throw new ImageFormatException("Can't deflate all input?"); + } + + baseOutputStream_.Flush(); + + //if (cryptoTransform_ != null) + //{ + // if (cryptoTransform_ is ZipAESTransform) + // { + // AESAuthCode = ((ZipAESTransform)cryptoTransform_).GetAuthCode(); + // } + // cryptoTransform_.Dispose(); + // cryptoTransform_ = null; + //} + } + + /// + /// Gets or sets a flag indicating ownership of underlying stream. + /// When the flag is true will close the underlying stream also. + /// + /// The default value is true. + public bool IsStreamOwner { get; set; } = true; + + /// + /// Allows client to determine if an entry can be patched after its added + /// + public bool CanPatchEntries + { + get + { + return baseOutputStream_.CanSeek; + } + } + + #endregion Public API + + //#region Encryption + + //private string password; + + //private ICryptoTransform cryptoTransform_; + + ///// + ///// Returns the 10 byte AUTH CODE to be appended immediately following the AES data stream. + ///// + //protected byte[] AESAuthCode; + + ///// + ///// Get/set the password used for encryption. + ///// + ///// When set to null or if the password is empty no encryption is performed + //public string Password + //{ + // get + // { + // return password; + // } + // set + // { + // if ((value != null) && (value.Length == 0)) + // { + // password = null; + // } + // else + // { + // password = value; + // } + // } + //} + + ///// + ///// Encrypt a block of data + ///// + ///// + ///// Data to encrypt. NOTE the original contents of the buffer are lost + ///// + ///// + ///// Offset of first byte in buffer to encrypt + ///// + ///// + ///// Number of bytes in buffer to encrypt + ///// + //protected void EncryptBlock(byte[] buffer, int offset, int length) + //{ + // cryptoTransform_.TransformBlock(buffer, 0, length, buffer, 0); + //} + + ///// + ///// Initializes encryption keys based on given . + ///// + ///// The password. + //protected void InitializePassword(string password) + //{ + // var pkManaged = new PkzipClassicManaged(); + // byte[] key = PkzipClassic.GenerateKeys(ZipStrings.ConvertToArray(password)); + // cryptoTransform_ = pkManaged.CreateEncryptor(key, null); + //} + + ///// + ///// Initializes encryption keys based on given password. + ///// + //protected void InitializeAESPassword(ZipEntry entry, string rawPassword, + // out byte[] salt, out byte[] pwdVerifier) + //{ + // salt = new byte[entry.AESSaltLen]; + // // Salt needs to be cryptographically random, and unique per file + // if (_aesRnd == null) + // _aesRnd = RandomNumberGenerator.Create(); + // _aesRnd.GetBytes(salt); + // int blockSize = entry.AESKeySize / 8; // bits to bytes + + // cryptoTransform_ = new ZipAESTransform(rawPassword, salt, blockSize, true); + // pwdVerifier = ((ZipAESTransform)cryptoTransform_).PwdVerifier; + //} + + //#endregion Encryption + + #region Deflation Support + + /// + /// Deflates everything in the input buffers. This will call + /// def.deflate() until all bytes from the input buffers + /// are processed. + /// + protected void Deflate() + { + Deflate(false); + } + + private void Deflate(bool flushing) + { + while (flushing || !deflater_.IsNeedingInput) + { + int deflateCount = deflater_.Deflate(buffer_, 0, buffer_.Length); + + if (deflateCount <= 0) + { + break; + } + //if (cryptoTransform_ != null) + //{ + // EncryptBlock(buffer_, 0, deflateCount); + //} + + baseOutputStream_.Write(buffer_, 0, deflateCount); + } + + if (!deflater_.IsNeedingInput) + { + throw new ImageFormatException("DeflaterOutputStream can't deflate all input?"); + } + } + + #endregion Deflation Support + + #region Stream Overrides + + /// + /// Gets value indicating stream can be read from + /// + public override bool CanRead + { + get + { + return false; + } + } + + /// + /// Gets a value indicating if seeking is supported for this stream + /// This property always returns false + /// + public override bool CanSeek + { + get + { + return false; + } + } + + /// + /// Get value indicating if this stream supports writing + /// + public override bool CanWrite + { + get + { + return baseOutputStream_.CanWrite; + } + } + + /// + /// Get current length of stream + /// + public override long Length + { + get + { + return baseOutputStream_.Length; + } + } + + /// + /// Gets the current position within the stream. + /// + /// Any attempt to set position + public override long Position + { + get + { + return baseOutputStream_.Position; + } + set + { + throw new NotSupportedException("Position property not supported"); + } + } + + /// + /// Sets the current position of this stream to the given value. Not supported by this class! + /// + /// The offset relative to the to seek. + /// The to seek from. + /// The new position in the stream. + /// Any access + public override long Seek(long offset, SeekOrigin origin) + { + throw new NotSupportedException("DeflaterOutputStream Seek not supported"); + } + + /// + /// Sets the length of this stream to the given value. Not supported by this class! + /// + /// The new stream length. + /// Any access + public override void SetLength(long value) + { + throw new NotSupportedException("DeflaterOutputStream SetLength not supported"); + } + + /// + /// Read a byte from stream advancing position by one + /// + /// The byte read cast to an int. THe value is -1 if at the end of the stream. + /// Any access + public override int ReadByte() + { + throw new NotSupportedException("DeflaterOutputStream ReadByte not supported"); + } + + /// + /// Read a block of bytes from stream + /// + /// The buffer to store read data in. + /// The offset to start storing at. + /// The maximum number of bytes to read. + /// The actual number of bytes read. Zero if end of stream is detected. + /// Any access + public override int Read(byte[] buffer, int offset, int count) + { + throw new NotSupportedException("DeflaterOutputStream Read not supported"); + } + + /// + /// Flushes the stream by calling Flush on the deflater and then + /// on the underlying stream. This ensures that all bytes are flushed. + /// + public override void Flush() + { + deflater_.Flush(); + Deflate(true); + baseOutputStream_.Flush(); + } + + /// + /// Calls and closes the underlying + /// stream when is true. + /// + protected override void Dispose(bool disposing) + { + if (!isClosed_) + { + isClosed_ = true; + + try + { + Finish(); + //if (cryptoTransform_ != null) + //{ + // GetAuthCodeIfAES(); + // cryptoTransform_.Dispose(); + // cryptoTransform_ = null; + //} + } + finally + { + if (IsStreamOwner) + { + baseOutputStream_.Dispose(); + } + } + } + } + + ///// + ///// Get the Auth code for AES encrypted entries + ///// + //protected void GetAuthCodeIfAES() + //{ + // if (cryptoTransform_ is ZipAESTransform) + // { + // AESAuthCode = ((ZipAESTransform)cryptoTransform_).GetAuthCode(); + // } + //} + + /// + /// Writes a single byte to the compressed output stream. + /// + /// + /// The byte value. + /// + public override void WriteByte(byte value) + { + byte[] b = new byte[1]; + b[0] = value; + Write(b, 0, 1); + } + + /// + /// Writes bytes from an array to the compressed stream. + /// + /// + /// The byte array + /// + /// + /// The offset into the byte array where to start. + /// + /// + /// The number of bytes to write. + /// + public override void Write(byte[] buffer, int offset, int count) + { + deflater_.SetInput(buffer, offset, count); + Deflate(); + } + + #endregion Stream Overrides + + #region Instance Fields + + /// + /// This buffer is used temporarily to retrieve the bytes from the + /// deflater and write them to the underlying output stream. + /// + private byte[] buffer_; + + /// + /// The deflater which is used to deflate the stream. + /// + protected Deflater deflater_; + + /// + /// Base stream the deflater depends on. + /// + protected Stream baseOutputStream_; + + private bool isClosed_; + + #endregion Instance Fields + + #region Static Fields + + // Static to help ensure that multiple files within a zip will get different random salt + //private static RandomNumberGenerator _aesRnd = RandomNumberGenerator.Create(); + + #endregion Static Fields + } +} diff --git a/src/ImageSharp/Formats/Png/Zlib/DeflaterPending.cs b/src/ImageSharp/Formats/Png/Zlib/DeflaterPending.cs new file mode 100644 index 0000000000..cc421c5b79 --- /dev/null +++ b/src/ImageSharp/Formats/Png/Zlib/DeflaterPending.cs @@ -0,0 +1,21 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +// +namespace SixLabors.ImageSharp.Formats.Png.Zlib +{ + /// + /// This class stores the pending output of the Deflater. + /// + /// author of the original java version : Jochen Hoenicke + /// + public class DeflaterPending : PendingBuffer + { + /// + /// Construct instance with default buffer size + /// + public DeflaterPending() : base(DeflaterConstants.PENDING_BUF_SIZE) + { + } + } +} diff --git a/src/ImageSharp/Formats/Png/Zlib/PendingBuffer.cs b/src/ImageSharp/Formats/Png/Zlib/PendingBuffer.cs new file mode 100644 index 0000000000..ae02c5113d --- /dev/null +++ b/src/ImageSharp/Formats/Png/Zlib/PendingBuffer.cs @@ -0,0 +1,276 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +// +using System; +using System.Collections.Generic; +using System.Text; + +namespace SixLabors.ImageSharp.Formats.Png.Zlib +{ + /// + /// This class is general purpose class for writing data to a buffer. + /// + /// It allows you to write bits as well as bytes + /// Based on DeflaterPending.java + /// + /// author of the original java version : Jochen Hoenicke + /// + public class PendingBuffer + { + #region Instance Fields + + /// + /// Internal work buffer + /// + private readonly byte[] buffer; + + private int start; + private int end; + + private uint bits; + private int bitCount; + + #endregion Instance Fields + + #region Constructors + + /// + /// construct instance using default buffer size of 4096 + /// + public PendingBuffer() : this(4096) + { + } + + /// + /// construct instance using specified buffer size + /// + /// + /// size to use for internal buffer + /// + public PendingBuffer(int bufferSize) + { + buffer = new byte[bufferSize]; + } + + #endregion Constructors + + /// + /// Clear internal state/buffers + /// + public void Reset() + { + start = end = bitCount = 0; + } + + /// + /// Write a byte to buffer + /// + /// + /// The value to write + /// + public void WriteByte(int value) + { +#if DebugDeflation + if (DeflaterConstants.DEBUGGING && (start != 0) ) + { + throw new ImageFormatException("Debug check: start != 0"); + } +#endif + buffer[end++] = unchecked((byte)value); + } + + /// + /// Write a short value to buffer LSB first + /// + /// + /// The value to write. + /// + public void WriteShort(int value) + { +#if DebugDeflation + if (DeflaterConstants.DEBUGGING && (start != 0) ) + { + throw new ImageFormatException("Debug check: start != 0"); + } +#endif + buffer[end++] = unchecked((byte)value); + buffer[end++] = unchecked((byte)(value >> 8)); + } + + /// + /// write an integer LSB first + /// + /// The value to write. + public void WriteInt(int value) + { +#if DebugDeflation + if (DeflaterConstants.DEBUGGING && (start != 0) ) + { + throw new ImageFormatException("Debug check: start != 0"); + } +#endif + buffer[end++] = unchecked((byte)value); + buffer[end++] = unchecked((byte)(value >> 8)); + buffer[end++] = unchecked((byte)(value >> 16)); + buffer[end++] = unchecked((byte)(value >> 24)); + } + + /// + /// Write a block of data to buffer + /// + /// data to write + /// offset of first byte to write + /// number of bytes to write + public void WriteBlock(byte[] block, int offset, int length) + { +#if DebugDeflation + if (DeflaterConstants.DEBUGGING && (start != 0) ) + { + throw new ImageFormatException("Debug check: start != 0"); + } +#endif + System.Array.Copy(block, offset, buffer, end, length); + end += length; + } + + /// + /// The number of bits written to the buffer + /// + public int BitCount + { + get + { + return bitCount; + } + } + + /// + /// Align internal buffer on a byte boundary + /// + public void AlignToByte() + { +#if DebugDeflation + if (DeflaterConstants.DEBUGGING && (start != 0) ) + { + throw new ImageFormatException("Debug check: start != 0"); + } +#endif + if (bitCount > 0) + { + buffer[end++] = unchecked((byte)bits); + if (bitCount > 8) + { + buffer[end++] = unchecked((byte)(bits >> 8)); + } + } + bits = 0; + bitCount = 0; + } + + /// + /// Write bits to internal buffer + /// + /// source of bits + /// number of bits to write + public void WriteBits(int b, int count) + { +#if DebugDeflation + if (DeflaterConstants.DEBUGGING && (start != 0) ) + { + throw new ImageFormatException("Debug check: start != 0"); + } + + // if (DeflaterConstants.DEBUGGING) { + // //Console.WriteLine("writeBits("+b+","+count+")"); + // } +#endif + bits |= (uint)(b << bitCount); + bitCount += count; + if (bitCount >= 16) + { + buffer[end++] = unchecked((byte)bits); + buffer[end++] = unchecked((byte)(bits >> 8)); + bits >>= 16; + bitCount -= 16; + } + } + + /// + /// Write a short value to internal buffer most significant byte first + /// + /// value to write + public void WriteShortMSB(int s) + { +#if DebugDeflation + if (DeflaterConstants.DEBUGGING && (start != 0) ) + { + throw new ImageFormatException("Debug check: start != 0"); + } +#endif + buffer[end++] = unchecked((byte)(s >> 8)); + buffer[end++] = unchecked((byte)s); + } + + /// + /// Indicates if buffer has been flushed + /// + public bool IsFlushed + { + get + { + return end == 0; + } + } + + /// + /// Flushes the pending buffer into the given output array. If the + /// output array is to small, only a partial flush is done. + /// + /// The output array. + /// The offset into output array. + /// The maximum number of bytes to store. + /// The number of bytes flushed. + public int Flush(byte[] output, int offset, int length) + { + if (bitCount >= 8) + { + buffer[end++] = unchecked((byte)bits); + bits >>= 8; + bitCount -= 8; + } + + if (length > end - start) + { + length = end - start; + System.Array.Copy(buffer, start, output, offset, length); + start = 0; + end = 0; + } + else + { + System.Array.Copy(buffer, start, output, offset, length); + start += length; + } + return length; + } + + /// + /// Convert internal buffer to byte array. + /// Buffer is empty on completion + /// + /// + /// The internal buffer contents converted to a byte array. + /// + public byte[] ToByteArray() + { + AlignToByte(); + + byte[] result = new byte[end - start]; + System.Array.Copy(buffer, start, result, 0, result.Length); + start = 0; + end = 0; + return result; + } + } +} diff --git a/src/ImageSharp/Formats/Png/Zlib/ZlibDeflateStream.cs b/src/ImageSharp/Formats/Png/Zlib/ZlibDeflateStream.cs index 8e0bac938f..ffe176368c 100644 --- a/src/ImageSharp/Formats/Png/Zlib/ZlibDeflateStream.cs +++ b/src/ImageSharp/Formats/Png/Zlib/ZlibDeflateStream.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,7 +38,8 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib /// /// The stream responsible for compressing the input stream. /// - private System.IO.Compression.DeflateStream deflateStream; + // private DeflateStream deflateStream; + private DeflaterOutputStream deflateStream; /// /// Initializes a new instance of the class. @@ -89,18 +90,19 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib this.rawStream.WriteByte((byte)flg); // Initialize the deflate Stream. - CompressionLevel level = CompressionLevel.Optimal; - - if (compressionLevel >= 1 && compressionLevel <= 5) - { - level = CompressionLevel.Fastest; - } - else if (compressionLevel == 0) - { - level = CompressionLevel.NoCompression; - } - - this.deflateStream = new System.IO.Compression.DeflateStream(this.rawStream, level, true); + // CompressionLevel level = CompressionLevel.Optimal; + // + // if (compressionLevel >= 1 && compressionLevel <= 5) + // { + // level = CompressionLevel.Fastest; + // } + // else if (compressionLevel == 0) + // { + // level = CompressionLevel.NoCompression; + // } + this.deflateStream = new DeflaterOutputStream(this.rawStream, new Deflater(compressionLevel, true)) { IsStreamOwner = false }; + + // this.deflateStream = new DeflateStream(this.rawStream, level, true); } /// diff --git a/tests/ImageSharp.Benchmarks/Codecs/EncodePng.cs b/tests/ImageSharp.Benchmarks/Codecs/EncodePng.cs index 157dadd2c1..7bd1b80447 100644 --- a/tests/ImageSharp.Benchmarks/Codecs/EncodePng.cs +++ b/tests/ImageSharp.Benchmarks/Codecs/EncodePng.cs @@ -1,9 +1,10 @@ -// Copyright (c) Six Labors and contributors. +// Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. using System.Drawing.Imaging; using System.IO; using BenchmarkDotNet.Attributes; +using SixLabors.ImageSharp.Formats.Png; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Tests; using SDImage = System.Drawing.Image; @@ -56,8 +57,9 @@ namespace SixLabors.ImageSharp.Benchmarks.Codecs { using (var memoryStream = new MemoryStream()) { - this.bmpCore.SaveAsPng(memoryStream); + var encoder = new PngEncoder { FilterMethod = PngFilterMethod.None }; + this.bmpCore.SaveAsPng(memoryStream, encoder); } } } -} \ No newline at end of file +} From 16759b04297c005e2c73984df5917ceeccc7ea43 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Tue, 12 Nov 2019 15:44:34 +1100 Subject: [PATCH 56/75] Optimize DeflaterPendingBuffer --- src/ImageSharp/Formats/Png/PngEncoderCore.cs | 4 +- src/ImageSharp/Formats/Png/Zlib/Deflater.cs | 57 ++-- .../Formats/Png/Zlib/DeflaterEngine.cs | 6 +- .../Formats/Png/Zlib/DeflaterHuffman.cs | 4 +- .../Formats/Png/Zlib/DeflaterOutputStream.cs | 12 - .../Formats/Png/Zlib/DeflaterPending.cs | 21 -- .../Formats/Png/Zlib/DeflaterPendingBuffer.cs | 187 ++++++++++++ .../Formats/Png/Zlib/PendingBuffer.cs | 276 ------------------ .../Formats/Png/Zlib/ZlibDeflateStream.cs | 13 +- 9 files changed, 239 insertions(+), 341 deletions(-) delete mode 100644 src/ImageSharp/Formats/Png/Zlib/DeflaterPending.cs create mode 100644 src/ImageSharp/Formats/Png/Zlib/DeflaterPendingBuffer.cs delete mode 100644 src/ImageSharp/Formats/Png/Zlib/PendingBuffer.cs diff --git a/src/ImageSharp/Formats/Png/PngEncoderCore.cs b/src/ImageSharp/Formats/Png/PngEncoderCore.cs index 09575bb288..19c6af27f7 100644 --- a/src/ImageSharp/Formats/Png/PngEncoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngEncoderCore.cs @@ -699,7 +699,7 @@ namespace SixLabors.ImageSharp.Formats.Png { using (var memoryStream = new MemoryStream()) { - using (var deflateStream = new ZlibDeflateStream(memoryStream, this.options.CompressionLevel)) + using (var deflateStream = new ZlibDeflateStream(this.memoryAllocator, memoryStream, this.options.CompressionLevel)) { deflateStream.Write(textBytes); } @@ -790,7 +790,7 @@ namespace SixLabors.ImageSharp.Formats.Png using (var memoryStream = new MemoryStream()) { - using (var deflateStream = new ZlibDeflateStream(memoryStream, this.options.CompressionLevel)) + using (var deflateStream = new ZlibDeflateStream(this.memoryAllocator, memoryStream, this.options.CompressionLevel)) { if (this.options.InterlaceMethod == PngInterlaceMode.Adam7) { diff --git a/src/ImageSharp/Formats/Png/Zlib/Deflater.cs b/src/ImageSharp/Formats/Png/Zlib/Deflater.cs index 3581125492..90e25fb58e 100644 --- a/src/ImageSharp/Formats/Png/Zlib/Deflater.cs +++ b/src/ImageSharp/Formats/Png/Zlib/Deflater.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Text; +using SixLabors.Memory; namespace SixLabors.ImageSharp.Formats.Png.Zlib { @@ -18,7 +19,7 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib /// /// author of the original java version : Jochen Hoenicke /// - public class Deflater + public sealed class Deflater : IDisposable { #region Deflater Documentation @@ -149,28 +150,10 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib #region Constructors - /// - /// Creates a new deflater with default compression level. - /// - public Deflater() : this(DEFAULT_COMPRESSION, false) - { - } - - /// - /// Creates a new deflater with given compression level. - /// - /// - /// the compression level, a value between NO_COMPRESSION - /// and BEST_COMPRESSION, or DEFAULT_COMPRESSION. - /// - /// if lvl is out of range. - public Deflater(int level) : this(level, false) - { - } - /// /// Creates a new deflater with given compression level. /// + /// The memory allocator to use for buffer allocations. /// /// the compression level, a value between NO_COMPRESSION /// and BEST_COMPRESSION. @@ -181,7 +164,7 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib /// useful for the GZIP/PKZIP formats. /// /// if lvl is out of range. - public Deflater(int level, bool noZlibHeaderOrFooter) + public Deflater(MemoryAllocator memoryAllocator, int level, bool noZlibHeaderOrFooter) { if (level == DEFAULT_COMPRESSION) { @@ -192,7 +175,7 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib throw new ArgumentOutOfRangeException(nameof(level)); } - pending = new DeflaterPending(); + pending = new DeflaterPendingBuffer(memoryAllocator); engine = new DeflaterEngine(pending, noZlibHeaderOrFooter); this.noZlibHeaderOrFooter = noZlibHeaderOrFooter; SetStrategy(DeflateStrategy.Default); @@ -598,13 +581,41 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib /// /// The pending output. /// - private DeflaterPending pending; + private DeflaterPendingBuffer pending; /// /// The deflater engine. /// private DeflaterEngine engine; + #region IDisposable Support + private bool disposedValue = false; // To detect redundant calls + + void Dispose(bool disposing) + { + if (!disposedValue) + { + if (disposing) + { + this.pending.Dispose(); + // TODO: dispose managed state (managed objects). + } + + // TODO: free unmanaged resources (unmanaged objects) and override a finalizer below. + // TODO: set large fields to null. + this.pending = null; + disposedValue = true; + } + } + + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + #endregion + #endregion Instance Fields } } diff --git a/src/ImageSharp/Formats/Png/Zlib/DeflaterEngine.cs b/src/ImageSharp/Formats/Png/Zlib/DeflaterEngine.cs index 7ac5b6c69f..c9967056e8 100644 --- a/src/ImageSharp/Formats/Png/Zlib/DeflaterEngine.cs +++ b/src/ImageSharp/Formats/Png/Zlib/DeflaterEngine.cs @@ -66,7 +66,7 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib /// /// Pending buffer to use /// - public DeflaterEngine(DeflaterPending pending) + public DeflaterEngine(DeflaterPendingBuffer pending) : this(pending, false) { } @@ -82,7 +82,7 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib /// /// If no adler calculation should be performed /// - public DeflaterEngine(DeflaterPending pending, bool noAdlerCalculation) + public DeflaterEngine(DeflaterPendingBuffer pending, bool noAdlerCalculation) { this.pending = pending; huffman = new DeflaterHuffman(pending); @@ -938,7 +938,7 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib ///
private int inputEnd; - private DeflaterPending pending; + private DeflaterPendingBuffer pending; private DeflaterHuffman huffman; /// diff --git a/src/ImageSharp/Formats/Png/Zlib/DeflaterHuffman.cs b/src/ImageSharp/Formats/Png/Zlib/DeflaterHuffman.cs index bf506d14c9..3d2856b0e8 100644 --- a/src/ImageSharp/Formats/Png/Zlib/DeflaterHuffman.cs +++ b/src/ImageSharp/Formats/Png/Zlib/DeflaterHuffman.cs @@ -590,7 +590,7 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib /// /// Pending buffer to use /// - public DeflaterPending pending; + public DeflaterPendingBuffer pending; private Tree literalTree; private Tree distTree; @@ -651,7 +651,7 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib /// Construct instance with pending buffer /// /// Pending buffer to use - public DeflaterHuffman(DeflaterPending pending) + public DeflaterHuffman(DeflaterPendingBuffer pending) { this.pending = pending; diff --git a/src/ImageSharp/Formats/Png/Zlib/DeflaterOutputStream.cs b/src/ImageSharp/Formats/Png/Zlib/DeflaterOutputStream.cs index 31db032aec..ac5f229aaa 100644 --- a/src/ImageSharp/Formats/Png/Zlib/DeflaterOutputStream.cs +++ b/src/ImageSharp/Formats/Png/Zlib/DeflaterOutputStream.cs @@ -17,18 +17,6 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib public class DeflaterOutputStream : Stream { #region Constructors - - /// - /// Creates a new DeflaterOutputStream with a default Deflater and default buffer size. - /// - /// - /// the output stream where deflated output should be written. - /// - public DeflaterOutputStream(Stream baseOutputStream) - : this(baseOutputStream, new Deflater(), 512) - { - } - /// /// Creates a new DeflaterOutputStream with the given Deflater and /// default buffer size. diff --git a/src/ImageSharp/Formats/Png/Zlib/DeflaterPending.cs b/src/ImageSharp/Formats/Png/Zlib/DeflaterPending.cs deleted file mode 100644 index cc421c5b79..0000000000 --- a/src/ImageSharp/Formats/Png/Zlib/DeflaterPending.cs +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright (c) Six Labors and contributors. -// Licensed under the Apache License, Version 2.0. - -// -namespace SixLabors.ImageSharp.Formats.Png.Zlib -{ - /// - /// This class stores the pending output of the Deflater. - /// - /// author of the original java version : Jochen Hoenicke - /// - public class DeflaterPending : PendingBuffer - { - /// - /// Construct instance with default buffer size - /// - public DeflaterPending() : base(DeflaterConstants.PENDING_BUF_SIZE) - { - } - } -} diff --git a/src/ImageSharp/Formats/Png/Zlib/DeflaterPendingBuffer.cs b/src/ImageSharp/Formats/Png/Zlib/DeflaterPendingBuffer.cs new file mode 100644 index 0000000000..64214b47e6 --- /dev/null +++ b/src/ImageSharp/Formats/Png/Zlib/DeflaterPendingBuffer.cs @@ -0,0 +1,187 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Buffers; +using System.Runtime.CompilerServices; +using SixLabors.Memory; + +namespace SixLabors.ImageSharp.Formats.Png.Zlib +{ + /// + /// Stores pending data for writing data to the Deflater. + /// + public sealed unsafe class DeflaterPendingBuffer : IDisposable + { + private readonly byte[] buffer; + private readonly byte* pinnedBuffer; + private readonly IManagedByteBuffer managedBuffer; + private MemoryHandle handle; + + private int start; + private int end; + private uint bits; + private bool isDisposed; + + /// + /// Initializes a new instance of the class. + /// + /// The memory allocator to use for buffer allocations. + public DeflaterPendingBuffer(MemoryAllocator memoryAllocator) + { + this.buffer = new byte[DeflaterConstants.PENDING_BUF_SIZE]; + this.managedBuffer = memoryAllocator.AllocateManagedByteBuffer(DeflaterConstants.PENDING_BUF_SIZE); + this.buffer = this.managedBuffer.Array; + this.handle = this.managedBuffer.Memory.Pin(); + this.pinnedBuffer = (byte*)this.handle.Pointer; + } + + /// + /// Gets the number of bits written to the buffer. + /// + public int BitCount { get; private set; } + + /// + /// Gets a value indicating whether indicates the buffer has been flushed. + /// + public bool IsFlushed => this.end == 0; + + /// + /// Clear internal state/buffers. + /// + [MethodImpl(InliningOptions.ShortMethod)] + public void Reset() => this.start = this.end = this.BitCount = 0; + + /// + /// Write a short value to buffer LSB first. + /// + /// The value to write. + [MethodImpl(InliningOptions.ShortMethod)] + public void WriteShort(int value) + { + byte* pinned = this.pinnedBuffer; + pinned[this.end++] = unchecked((byte)value); + pinned[this.end++] = unchecked((byte)(value >> 8)); + } + + /// + /// Write a block of data to the internal buffer. + /// + /// The data to write. + /// The offset of first byte to write. + /// The number of bytes to write. + [MethodImpl(InliningOptions.ShortMethod)] + public void WriteBlock(byte[] block, int offset, int length) + { + Unsafe.CopyBlockUnaligned(ref this.buffer[this.end], ref block[offset], unchecked((uint)length)); + this.end += length; + } + + /// + /// Aligns internal buffer on a byte boundary. + /// + [MethodImpl(InliningOptions.ShortMethod)] + public void AlignToByte() + { + if (this.BitCount > 0) + { + byte* pinned = this.pinnedBuffer; + pinned[this.end++] = unchecked((byte)this.bits); + if (this.BitCount > 8) + { + pinned[this.end++] = unchecked((byte)(this.bits >> 8)); + } + } + + this.bits = 0; + this.BitCount = 0; + } + + /// + /// Write bits to internal buffer + /// + /// source of bits + /// number of bits to write + [MethodImpl(InliningOptions.ShortMethod)] + public void WriteBits(int b, int count) + { + this.bits |= (uint)(b << this.BitCount); + this.BitCount += count; + if (this.BitCount >= 16) + { + byte* pinned = this.pinnedBuffer; + pinned[this.end++] = unchecked((byte)this.bits); + pinned[this.end++] = unchecked((byte)(this.bits >> 8)); + this.bits >>= 16; + this.BitCount -= 16; + } + } + + /// + /// Write a short value to internal buffer most significant byte first + /// + /// The value to write + [MethodImpl(InliningOptions.ShortMethod)] + public void WriteShortMSB(int value) + { + byte* pinned = this.pinnedBuffer; + pinned[this.end++] = unchecked((byte)(value >> 8)); + pinned[this.end++] = unchecked((byte)value); + } + + /// + /// Flushes the pending buffer into the given output array. + /// If the output array is to small, only a partial flush is done. + /// + /// The output array. + /// The offset into output array. + /// The maximum number of bytes to store. + /// The number of bytes flushed. + public int Flush(byte[] output, int offset, int length) + { + if (this.BitCount >= 8) + { + this.pinnedBuffer[this.end++] = unchecked((byte)this.bits); + this.bits >>= 8; + this.BitCount -= 8; + } + + if (length > this.end - this.start) + { + length = this.end - this.start; + + Unsafe.CopyBlockUnaligned(ref output[offset], ref this.buffer[this.start], unchecked((uint)length)); + this.start = 0; + this.end = 0; + } + else + { + Unsafe.CopyBlockUnaligned(ref output[offset], ref this.buffer[this.start], unchecked((uint)length)); + this.start += length; + } + + return length; + } + + /// + public void Dispose() + { + this.Dispose(true); + GC.SuppressFinalize(this); + } + + private void Dispose(bool disposing) + { + if (!this.isDisposed) + { + if (disposing) + { + this.handle.Dispose(); + this.managedBuffer.Dispose(); + } + + this.isDisposed = true; + } + } + } +} diff --git a/src/ImageSharp/Formats/Png/Zlib/PendingBuffer.cs b/src/ImageSharp/Formats/Png/Zlib/PendingBuffer.cs deleted file mode 100644 index ae02c5113d..0000000000 --- a/src/ImageSharp/Formats/Png/Zlib/PendingBuffer.cs +++ /dev/null @@ -1,276 +0,0 @@ -// Copyright (c) Six Labors and contributors. -// Licensed under the Apache License, Version 2.0. - -// -using System; -using System.Collections.Generic; -using System.Text; - -namespace SixLabors.ImageSharp.Formats.Png.Zlib -{ - /// - /// This class is general purpose class for writing data to a buffer. - /// - /// It allows you to write bits as well as bytes - /// Based on DeflaterPending.java - /// - /// author of the original java version : Jochen Hoenicke - /// - public class PendingBuffer - { - #region Instance Fields - - /// - /// Internal work buffer - /// - private readonly byte[] buffer; - - private int start; - private int end; - - private uint bits; - private int bitCount; - - #endregion Instance Fields - - #region Constructors - - /// - /// construct instance using default buffer size of 4096 - /// - public PendingBuffer() : this(4096) - { - } - - /// - /// construct instance using specified buffer size - /// - /// - /// size to use for internal buffer - /// - public PendingBuffer(int bufferSize) - { - buffer = new byte[bufferSize]; - } - - #endregion Constructors - - /// - /// Clear internal state/buffers - /// - public void Reset() - { - start = end = bitCount = 0; - } - - /// - /// Write a byte to buffer - /// - /// - /// The value to write - /// - public void WriteByte(int value) - { -#if DebugDeflation - if (DeflaterConstants.DEBUGGING && (start != 0) ) - { - throw new ImageFormatException("Debug check: start != 0"); - } -#endif - buffer[end++] = unchecked((byte)value); - } - - /// - /// Write a short value to buffer LSB first - /// - /// - /// The value to write. - /// - public void WriteShort(int value) - { -#if DebugDeflation - if (DeflaterConstants.DEBUGGING && (start != 0) ) - { - throw new ImageFormatException("Debug check: start != 0"); - } -#endif - buffer[end++] = unchecked((byte)value); - buffer[end++] = unchecked((byte)(value >> 8)); - } - - /// - /// write an integer LSB first - /// - /// The value to write. - public void WriteInt(int value) - { -#if DebugDeflation - if (DeflaterConstants.DEBUGGING && (start != 0) ) - { - throw new ImageFormatException("Debug check: start != 0"); - } -#endif - buffer[end++] = unchecked((byte)value); - buffer[end++] = unchecked((byte)(value >> 8)); - buffer[end++] = unchecked((byte)(value >> 16)); - buffer[end++] = unchecked((byte)(value >> 24)); - } - - /// - /// Write a block of data to buffer - /// - /// data to write - /// offset of first byte to write - /// number of bytes to write - public void WriteBlock(byte[] block, int offset, int length) - { -#if DebugDeflation - if (DeflaterConstants.DEBUGGING && (start != 0) ) - { - throw new ImageFormatException("Debug check: start != 0"); - } -#endif - System.Array.Copy(block, offset, buffer, end, length); - end += length; - } - - /// - /// The number of bits written to the buffer - /// - public int BitCount - { - get - { - return bitCount; - } - } - - /// - /// Align internal buffer on a byte boundary - /// - public void AlignToByte() - { -#if DebugDeflation - if (DeflaterConstants.DEBUGGING && (start != 0) ) - { - throw new ImageFormatException("Debug check: start != 0"); - } -#endif - if (bitCount > 0) - { - buffer[end++] = unchecked((byte)bits); - if (bitCount > 8) - { - buffer[end++] = unchecked((byte)(bits >> 8)); - } - } - bits = 0; - bitCount = 0; - } - - /// - /// Write bits to internal buffer - /// - /// source of bits - /// number of bits to write - public void WriteBits(int b, int count) - { -#if DebugDeflation - if (DeflaterConstants.DEBUGGING && (start != 0) ) - { - throw new ImageFormatException("Debug check: start != 0"); - } - - // if (DeflaterConstants.DEBUGGING) { - // //Console.WriteLine("writeBits("+b+","+count+")"); - // } -#endif - bits |= (uint)(b << bitCount); - bitCount += count; - if (bitCount >= 16) - { - buffer[end++] = unchecked((byte)bits); - buffer[end++] = unchecked((byte)(bits >> 8)); - bits >>= 16; - bitCount -= 16; - } - } - - /// - /// Write a short value to internal buffer most significant byte first - /// - /// value to write - public void WriteShortMSB(int s) - { -#if DebugDeflation - if (DeflaterConstants.DEBUGGING && (start != 0) ) - { - throw new ImageFormatException("Debug check: start != 0"); - } -#endif - buffer[end++] = unchecked((byte)(s >> 8)); - buffer[end++] = unchecked((byte)s); - } - - /// - /// Indicates if buffer has been flushed - /// - public bool IsFlushed - { - get - { - return end == 0; - } - } - - /// - /// Flushes the pending buffer into the given output array. If the - /// output array is to small, only a partial flush is done. - /// - /// The output array. - /// The offset into output array. - /// The maximum number of bytes to store. - /// The number of bytes flushed. - public int Flush(byte[] output, int offset, int length) - { - if (bitCount >= 8) - { - buffer[end++] = unchecked((byte)bits); - bits >>= 8; - bitCount -= 8; - } - - if (length > end - start) - { - length = end - start; - System.Array.Copy(buffer, start, output, offset, length); - start = 0; - end = 0; - } - else - { - System.Array.Copy(buffer, start, output, offset, length); - start += length; - } - return length; - } - - /// - /// Convert internal buffer to byte array. - /// Buffer is empty on completion - /// - /// - /// The internal buffer contents converted to a byte array. - /// - public byte[] ToByteArray() - { - AlignToByte(); - - byte[] result = new byte[end - start]; - System.Array.Copy(buffer, start, result, 0, result.Length); - start = 0; - end = 0; - return result; - } - } -} diff --git a/src/ImageSharp/Formats/Png/Zlib/ZlibDeflateStream.cs b/src/ImageSharp/Formats/Png/Zlib/ZlibDeflateStream.cs index ffe176368c..69b3c06026 100644 --- a/src/ImageSharp/Formats/Png/Zlib/ZlibDeflateStream.cs +++ b/src/ImageSharp/Formats/Png/Zlib/ZlibDeflateStream.cs @@ -4,6 +4,7 @@ using System; using System.IO; using System.IO.Compression; +using SixLabors.Memory; namespace SixLabors.ImageSharp.Formats.Png.Zlib { @@ -41,12 +42,15 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib // private DeflateStream deflateStream; private DeflaterOutputStream deflateStream; + private Deflater deflater; + /// /// Initializes a new instance of the class. /// + /// The memory allocator to use for buffer allocations. /// The stream to compress. /// The compression level. - public ZlibDeflateStream(Stream stream, int compressionLevel) + public ZlibDeflateStream(MemoryAllocator memoryAllocator, Stream stream, int compressionLevel) { this.rawStream = stream; @@ -100,7 +104,8 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib // { // level = CompressionLevel.NoCompression; // } - this.deflateStream = new DeflaterOutputStream(this.rawStream, new Deflater(compressionLevel, true)) { IsStreamOwner = false }; + this.deflater = new Deflater(memoryAllocator, compressionLevel, true); + this.deflateStream = new DeflaterOutputStream(this.rawStream, this.deflater) { IsStreamOwner = false }; // this.deflateStream = new DeflateStream(this.rawStream, level, true); } @@ -170,6 +175,10 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib { this.deflateStream.Dispose(); this.deflateStream = null; + + // TODO: Remove temporal coupling here. + this.deflater.Dispose(); + this.deflater = null; } else { From 1bab6db93e69734bf4242e77008bb7509aef0023 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Tue, 12 Nov 2019 19:59:30 +1100 Subject: [PATCH 57/75] !st pass cleanup of Deflater --- src/ImageSharp/Formats/Png/Zlib/Deflater.cs | 573 +++++------------- .../Formats/Png/Zlib/DeflaterThrowHelper.cs | 17 + .../Formats/Png/Zlib/ZlibDeflateStream.cs | 9 +- 3 files changed, 165 insertions(+), 434 deletions(-) create mode 100644 src/ImageSharp/Formats/Png/Zlib/DeflaterThrowHelper.cs diff --git a/src/ImageSharp/Formats/Png/Zlib/Deflater.cs b/src/ImageSharp/Formats/Png/Zlib/Deflater.cs index 90e25fb58e..26d1f9a45a 100644 --- a/src/ImageSharp/Formats/Png/Zlib/Deflater.cs +++ b/src/ImageSharp/Formats/Png/Zlib/Deflater.cs @@ -1,97 +1,92 @@ // Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. -// using System; -using System.Collections.Generic; -using System.Text; +using System.Runtime.CompilerServices; using SixLabors.Memory; namespace SixLabors.ImageSharp.Formats.Png.Zlib { /// - /// This is the Deflater class. The deflater class compresses input - /// with the deflate algorithm described in RFC 1951. It has several - /// compression levels and three different strategies described below. - /// - /// This class is not thread safe. This is inherent in the API, due - /// to the split of deflate and setInput. - /// - /// author of the original java version : Jochen Hoenicke + /// This class compresses input with the deflate algorithm described in RFC 1951. + /// It has several compression levels and three different strategies described below. /// public sealed class Deflater : IDisposable { - #region Deflater Documentation - - /* - * The Deflater can do the following state transitions: - * - * (1) -> INIT_STATE ----> INIT_FINISHING_STATE ---. - * / | (2) (5) | - * / v (5) | - * (3)| SETDICT_STATE ---> SETDICT_FINISHING_STATE |(3) - * \ | (3) | ,--------' - * | | | (3) / - * v v (5) v v - * (1) -> BUSY_STATE ----> FINISHING_STATE - * | (6) - * v - * FINISHED_STATE - * \_____________________________________/ - * | (7) - * v - * CLOSED_STATE - * - * (1) If we should produce a header we start in INIT_STATE, otherwise - * we start in BUSY_STATE. - * (2) A dictionary may be set only when we are in INIT_STATE, then - * we change the state as indicated. - * (3) Whether a dictionary is set or not, on the first call of deflate - * we change to BUSY_STATE. - * (4) -- intentionally left blank -- :) - * (5) FINISHING_STATE is entered, when flush() is called to indicate that - * there is no more INPUT. There are also states indicating, that - * the header wasn't written yet. - * (6) FINISHED_STATE is entered, when everything has been flushed to the - * internal pending output buffer. - * (7) At any time (7) - * - */ - - #endregion Deflater Documentation - - #region Public Constants - /// /// The best and slowest compression level. This tries to find very /// long and distant string repetitions. /// - public const int BEST_COMPRESSION = 9; + public const int BestCompression = 9; /// /// The worst but fastest compression level. /// - public const int BEST_SPEED = 1; + public const int BestSpeed = 1; /// /// The default compression level. /// - public const int DEFAULT_COMPRESSION = -1; + public const int DefaultCompression = -1; /// /// This level won't compress at all but output uncompressed blocks. /// - public const int NO_COMPRESSION = 0; + public const int NoCompression = 0; /// /// The compression method. This is the only method supported so far. /// There is no need to use this constant at all. /// - public const int DEFLATED = 8; + public const int Deflated = 8; - #endregion Public Constants + /// + /// Compression level. + /// + private int level; + + /// + /// The current state. + /// + private int state; + + private DeflaterPendingBuffer pending; + private DeflaterEngine engine; + private bool isDisposed; - #region Public Enum + private const int IsSetDict = 0x01; + private const int IsFlushing = 0x04; + private const int IsFinishing = 0x08; + private const int BusyState = 0x10; + private const int FlushingState = 0x14; + private const int FinishingState = 0x1c; + private const int FinishedState = 0x1e; + private const int ClosedState = 0x7f; + + /// + /// Initializes a new instance of the class. + /// + /// The memory allocator to use for buffer allocations. + /// The compression level, a value between NoCompression and BestCompression. + /// + /// if level is out of range. + public Deflater(MemoryAllocator memoryAllocator, int level) + { + if (level == DefaultCompression) + { + level = 6; + } + else if (level < NoCompression || level > BestCompression) + { + throw new ArgumentOutOfRangeException(nameof(level)); + } + + this.pending = new DeflaterPendingBuffer(memoryAllocator); + this.engine = new DeflaterEngine(this.pending, true); + this.engine.Strategy = DeflateStrategy.Default; + this.SetLevel(level); + this.Reset(); + } /// /// Compression Level as an enum for safer use @@ -102,202 +97,72 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib /// The best and slowest compression level. This tries to find very /// long and distant string repetitions. /// - BEST_COMPRESSION = Deflater.BEST_COMPRESSION, + BestCompression = Deflater.BestCompression, /// /// The worst but fastest compression level. /// - BEST_SPEED = Deflater.BEST_SPEED, + BestSpeed = Deflater.BestSpeed, /// /// The default compression level. /// - DEFAULT_COMPRESSION = Deflater.DEFAULT_COMPRESSION, + DefaultCompression = Deflater.DefaultCompression, /// /// This level won't compress at all but output uncompressed blocks. /// - NO_COMPRESSION = Deflater.NO_COMPRESSION, + NoCompression = Deflater.NoCompression, /// /// The compression method. This is the only method supported so far. /// There is no need to use this constant at all. /// - DEFLATED = Deflater.DEFLATED + Deflated = Deflater.Deflated } - #endregion Public Enum - - #region Local Constants - - private const int IS_SETDICT = 0x01; - private const int IS_FLUSHING = 0x04; - private const int IS_FINISHING = 0x08; - - private const int INIT_STATE = 0x00; - private const int SETDICT_STATE = 0x01; - - // private static int INIT_FINISHING_STATE = 0x08; - // private static int SETDICT_FINISHING_STATE = 0x09; - private const int BUSY_STATE = 0x10; - - private const int FLUSHING_STATE = 0x14; - private const int FINISHING_STATE = 0x1c; - private const int FINISHED_STATE = 0x1e; - private const int CLOSED_STATE = 0x7f; - - #endregion Local Constants - - #region Constructors - /// - /// Creates a new deflater with given compression level. + /// Gets a value indicating whetherthe stream was finished and no more output bytes + /// are available. /// - /// The memory allocator to use for buffer allocations. - /// - /// the compression level, a value between NO_COMPRESSION - /// and BEST_COMPRESSION. - /// - /// - /// true, if we should suppress the Zlib/RFC1950 header at the - /// beginning and the adler checksum at the end of the output. This is - /// useful for the GZIP/PKZIP formats. - /// - /// if lvl is out of range. - public Deflater(MemoryAllocator memoryAllocator, int level, bool noZlibHeaderOrFooter) - { - if (level == DEFAULT_COMPRESSION) - { - level = 6; - } - else if (level < NO_COMPRESSION || level > BEST_COMPRESSION) - { - throw new ArgumentOutOfRangeException(nameof(level)); - } - - pending = new DeflaterPendingBuffer(memoryAllocator); - engine = new DeflaterEngine(pending, noZlibHeaderOrFooter); - this.noZlibHeaderOrFooter = noZlibHeaderOrFooter; - SetStrategy(DeflateStrategy.Default); - SetLevel(level); - Reset(); - } + public bool IsFinished => (this.state == FinishedState) && this.pending.IsFlushed; - #endregion Constructors + /// + /// Gets a value indicating whether the input buffer is empty. + /// You should then call setInput(). + /// NOTE: This method can also return true when the stream + /// was finished. + /// + public bool IsNeedingInput => this.engine.NeedsInput(); /// /// Resets the deflater. The deflater acts afterwards as if it was /// just created with the same compression level and strategy as it /// had before. /// + [MethodImpl(InliningOptions.ShortMethod)] public void Reset() { - state = (noZlibHeaderOrFooter ? BUSY_STATE : INIT_STATE); - totalOut = 0; - pending.Reset(); - engine.Reset(); - } - - /// - /// Gets the current adler checksum of the data that was processed so far. - /// - public int Adler - { - get - { - return engine.Adler; - } - } - - /// - /// Gets the number of input bytes processed so far. - /// - public long TotalIn - { - get - { - return engine.TotalIn; - } - } - - /// - /// Gets the number of output bytes so far. - /// - public long TotalOut - { - get - { - return totalOut; - } + this.state = BusyState; + this.pending.Reset(); + this.engine.Reset(); } /// - /// Flushes the current input block. Further calls to deflate() will + /// Flushes the current input block. Further calls to Deflate() will /// produce enough output to inflate everything in the current input - /// block. This is not part of Sun's JDK so I have made it package - /// private. It is used by DeflaterOutputStream to implement - /// flush(). + /// block. It is used by DeflaterOutputStream to implement Flush(). /// - public void Flush() - { - state |= IS_FLUSHING; - } + [MethodImpl(InliningOptions.ShortMethod)] + public void Flush() => this.state |= IsFlushing; /// - /// Finishes the deflater with the current input block. It is an error - /// to give more input after this method was called. This method must + /// Finishes the deflater with the current input block. It is an error + /// to give more input after this method was called. This method must /// be called to force all bytes to be flushed. /// - public void Finish() - { - state |= (IS_FLUSHING | IS_FINISHING); - } - - /// - /// Returns true if the stream was finished and no more output bytes - /// are available. - /// - public bool IsFinished - { - get - { - return (state == FINISHED_STATE) && pending.IsFlushed; - } - } - - /// - /// Returns true, if the input buffer is empty. - /// You should then call setInput(). - /// NOTE: This method can also return true when the stream - /// was finished. - /// - public bool IsNeedingInput - { - get - { - return engine.NeedsInput(); - } - } - - /// - /// Sets the data which should be compressed next. This should be only - /// called when needsInput indicates that more input is needed. - /// If you call setInput when needsInput() returns false, the - /// previous input that is still pending will be thrown away. - /// The given byte array should not be changed, before needsInput() returns - /// true again. - /// This call is equivalent to setInput(input, 0, input.length). - /// - /// - /// the buffer containing the input data. - /// - /// - /// if the buffer was finished() or ended(). - /// - public void SetInput(byte[] input) - { - SetInput(input, 0, input.Length); - } + [MethodImpl(InliningOptions.ShortMethod)] + public void Finish() => this.state |= IsFlushing | IsFinishing; /// /// Sets the data which should be compressed next. This should be @@ -305,25 +170,21 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib /// The given byte array should not be changed, before needsInput() returns /// true again. /// - /// - /// the buffer containing the input data. - /// - /// - /// the start of the data. - /// - /// - /// the number of data bytes of input. - /// - /// - /// if the buffer was Finish()ed or if previous input is still pending. + /// The buffer containing the input data. + /// The start of the data. + /// The number of data bytes of input. + /// + /// if the buffer was finished or if previous input is still pending. /// + [MethodImpl(InliningOptions.ShortMethod)] public void SetInput(byte[] input, int offset, int count) { - if ((state & IS_FINISHING) != 0) + if ((this.state & IsFinishing) != 0) { - throw new InvalidOperationException("Finish() already called"); + DeflaterThrowHelper.ThrowAlreadyFinished(); } - engine.SetInput(input, offset, count); + + this.engine.SetInput(input, offset, count); } /// @@ -337,11 +198,11 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib /// public void SetLevel(int level) { - if (level == DEFAULT_COMPRESSION) + if (level == DefaultCompression) { level = 6; } - else if (level < NO_COMPRESSION || level > BEST_COMPRESSION) + else if (level < NoCompression || level > BestCompression) { throw new ArgumentOutOfRangeException(nameof(level)); } @@ -349,273 +210,127 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib if (this.level != level) { this.level = level; - engine.SetLevel(level); + this.engine.SetLevel(level); } } - /// - /// Get current compression level - /// - /// Returns the current compression level - public int GetLevel() - { - return level; - } - - /// - /// Sets the compression strategy. Strategy is one of - /// DEFAULT_STRATEGY, HUFFMAN_ONLY and FILTERED. For the exact - /// position where the strategy is changed, the same as for - /// SetLevel() applies. - /// - /// - /// The new compression strategy. - /// - public void SetStrategy(DeflateStrategy strategy) - { - engine.Strategy = strategy; - } - - /// - /// Deflates the current input block with to the given array. - /// - /// - /// The buffer where compressed data is stored - /// - /// - /// The number of compressed bytes added to the output, or 0 if either - /// IsNeedingInput() or IsFinished returns true or length is zero. - /// - public int Deflate(byte[] output) - { - return Deflate(output, 0, output.Length); - } - /// /// Deflates the current input block to the given array. /// - /// - /// Buffer to store the compressed data. - /// - /// - /// Offset into the output array. - /// - /// - /// The maximum number of bytes that may be stored. - /// + /// Buffer to store the compressed data. + /// Offset into the output array. + /// The maximum number of bytes that may be stored. /// /// The number of compressed bytes added to the output, or 0 if either - /// needsInput() or finished() returns true or length is zero. + /// or returns true or length is zero. /// - /// - /// If Finish() was previously called. - /// - /// - /// If offset or length don't match the array length. - /// public int Deflate(byte[] output, int offset, int length) { int origLength = length; - if (state == CLOSED_STATE) + if (this.state == ClosedState) { - throw new InvalidOperationException("Deflater closed"); + DeflaterThrowHelper.ThrowAlreadyClosed(); } - if (state < BUSY_STATE) + if (this.state < BusyState) { - // output header - int header = (DEFLATED + - ((DeflaterConstants.MAX_WBITS - 8) << 4)) << 8; - int level_flags = (level - 1) >> 1; - if (level_flags < 0 || level_flags > 3) + // Output header + int header = (Deflated + ((DeflaterConstants.MAX_WBITS - 8) << 4)) << 8; + int levelFlags = (this.level - 1) >> 1; + if (levelFlags < 0 || levelFlags > 3) { - level_flags = 3; + levelFlags = 3; } - header |= level_flags << 6; - if ((state & IS_SETDICT) != 0) + + header |= levelFlags << 6; + if ((this.state & IsSetDict) != 0) { // Dictionary was set header |= DeflaterConstants.PRESET_DICT; } + header += 31 - (header % 31); - pending.WriteShortMSB(header); - if ((state & IS_SETDICT) != 0) + this.pending.WriteShortMSB(header); + if ((this.state & IsSetDict) != 0) { - int chksum = engine.Adler; - engine.ResetAdler(); - pending.WriteShortMSB(chksum >> 16); - pending.WriteShortMSB(chksum & 0xffff); + int chksum = this.engine.Adler; + this.engine.ResetAdler(); + this.pending.WriteShortMSB(chksum >> 16); + this.pending.WriteShortMSB(chksum & 0xffff); } - state = BUSY_STATE | (state & (IS_FLUSHING | IS_FINISHING)); + this.state = BusyState | (this.state & (IsFlushing | IsFinishing)); } - for (; ; ) + while (true) { - int count = pending.Flush(output, offset, length); + int count = this.pending.Flush(output, offset, length); offset += count; - totalOut += count; length -= count; - if (length == 0 || state == FINISHED_STATE) + if (length == 0 || this.state == FinishedState) { break; } - if (!engine.Deflate((state & IS_FLUSHING) != 0, (state & IS_FINISHING) != 0)) + if (!this.engine.Deflate((this.state & IsFlushing) != 0, (this.state & IsFinishing) != 0)) { - switch (state) + switch (this.state) { - case BUSY_STATE: + case BusyState: // We need more input now return origLength - length; - case FLUSHING_STATE: - if (level != NO_COMPRESSION) + case FlushingState: + if (this.level != NoCompression) { - /* We have to supply some lookahead. 8 bit lookahead - * is needed by the zlib inflater, and we must fill - * the next byte, so that all bits are flushed. - */ - int neededbits = 8 + ((-pending.BitCount) & 7); + // We have to supply some lookahead. 8 bit lookahead + // is needed by the zlib inflater, and we must fill + // the next byte, so that all bits are flushed. + int neededbits = 8 + ((-this.pending.BitCount) & 7); while (neededbits > 0) { - /* write a static tree block consisting solely of - * an EOF: - */ - pending.WriteBits(2, 10); + // Write a static tree block consisting solely of an EOF: + this.pending.WriteBits(2, 10); neededbits -= 10; } } - state = BUSY_STATE; - break; - case FINISHING_STATE: - pending.AlignToByte(); + this.state = BusyState; + break; - // Compressed data is complete. Write footer information if required. - if (!noZlibHeaderOrFooter) - { - int adler = engine.Adler; - pending.WriteShortMSB(adler >> 16); - pending.WriteShortMSB(adler & 0xffff); - } - state = FINISHED_STATE; + case FinishingState: + this.pending.AlignToByte(); + this.state = FinishedState; break; } } } - return origLength - length; - } - /// - /// Sets the dictionary which should be used in the deflate process. - /// This call is equivalent to setDictionary(dict, 0, dict.Length). - /// - /// - /// the dictionary. - /// - /// - /// if SetInput () or Deflate () were already called or another dictionary was already set. - /// - public void SetDictionary(byte[] dictionary) - { - SetDictionary(dictionary, 0, dictionary.Length); + return origLength - length; } - /// - /// Sets the dictionary which should be used in the deflate process. - /// The dictionary is a byte array containing strings that are - /// likely to occur in the data which should be compressed. The - /// dictionary is not stored in the compressed output, only a - /// checksum. To decompress the output you need to supply the same - /// dictionary again. - /// - /// - /// The dictionary data - /// - /// - /// The index where dictionary information commences. - /// - /// - /// The number of bytes in the dictionary. - /// - /// - /// If SetInput () or Deflate() were already called or another dictionary was already set. - /// - public void SetDictionary(byte[] dictionary, int index, int count) + /// + public void Dispose() { - if (state != INIT_STATE) - { - throw new InvalidOperationException(); - } - - state = SETDICT_STATE; - engine.SetDictionary(dictionary, index, count); + this.Dispose(true); + GC.SuppressFinalize(this); } - #region Instance Fields - - /// - /// Compression level. - /// - private int level; - - /// - /// If true no Zlib/RFC1950 headers or footers are generated - /// - private bool noZlibHeaderOrFooter; - - /// - /// The current state. - /// - private int state; - - /// - /// The total bytes of output written. - /// - private long totalOut; - - /// - /// The pending output. - /// - private DeflaterPendingBuffer pending; - - /// - /// The deflater engine. - /// - private DeflaterEngine engine; - - #region IDisposable Support - private bool disposedValue = false; // To detect redundant calls - - void Dispose(bool disposing) + private void Dispose(bool disposing) { - if (!disposedValue) + if (!this.isDisposed) { if (disposing) { this.pending.Dispose(); - // TODO: dispose managed state (managed objects). } - // TODO: free unmanaged resources (unmanaged objects) and override a finalizer below. - // TODO: set large fields to null. this.pending = null; - disposedValue = true; + this.isDisposed = true; } } - - /// - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - #endregion - - #endregion Instance Fields } } diff --git a/src/ImageSharp/Formats/Png/Zlib/DeflaterThrowHelper.cs b/src/ImageSharp/Formats/Png/Zlib/DeflaterThrowHelper.cs new file mode 100644 index 0000000000..b59d32c4d1 --- /dev/null +++ b/src/ImageSharp/Formats/Png/Zlib/DeflaterThrowHelper.cs @@ -0,0 +1,17 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Runtime.CompilerServices; + +namespace SixLabors.ImageSharp.Formats.Png.Zlib +{ + internal static class DeflaterThrowHelper + { + [MethodImpl(InliningOptions.ColdPath)] + public static void ThrowAlreadyFinished() => throw new InvalidOperationException("Finish() already called."); + + [MethodImpl(InliningOptions.ColdPath)] + public static void ThrowAlreadyClosed() => throw new InvalidOperationException("Deflator already closed."); + } +} diff --git a/src/ImageSharp/Formats/Png/Zlib/ZlibDeflateStream.cs b/src/ImageSharp/Formats/Png/Zlib/ZlibDeflateStream.cs index 69b3c06026..5724e027d2 100644 --- a/src/ImageSharp/Formats/Png/Zlib/ZlibDeflateStream.cs +++ b/src/ImageSharp/Formats/Png/Zlib/ZlibDeflateStream.cs @@ -3,7 +3,6 @@ using System; using System.IO; -using System.IO.Compression; using SixLabors.Memory; namespace SixLabors.ImageSharp.Formats.Png.Zlib @@ -65,7 +64,7 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib // +---+---+ // |CMF|FLG| // +---+---+ - int cmf = 0x78; + const int Cmf = 0x78; int flg = 218; // http://stackoverflow.com/a/2331025/277304 @@ -83,14 +82,14 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib } // Just in case - flg -= ((cmf * 256) + flg) % 31; + flg -= ((Cmf * 256) + flg) % 31; if (flg < 0) { flg += 31; } - this.rawStream.WriteByte((byte)cmf); + this.rawStream.WriteByte(Cmf); this.rawStream.WriteByte((byte)flg); // Initialize the deflate Stream. @@ -104,7 +103,7 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib // { // level = CompressionLevel.NoCompression; // } - this.deflater = new Deflater(memoryAllocator, compressionLevel, true); + this.deflater = new Deflater(memoryAllocator, compressionLevel); this.deflateStream = new DeflaterOutputStream(this.rawStream, this.deflater) { IsStreamOwner = false }; // this.deflateStream = new DeflateStream(this.rawStream, level, true); From 95636933ae4179881ba309345f090ded9d672eba Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Tue, 12 Nov 2019 20:35:37 +1100 Subject: [PATCH 58/75] Stylecop cleanup DeflaterEngine --- ...erThrowHelper.cs => DeflateThrowHelper.cs} | 2 +- src/ImageSharp/Formats/Png/Zlib/Deflater.cs | 4 +- .../Formats/Png/Zlib/DeflaterEngine.cs | 840 ++++++++---------- 3 files changed, 395 insertions(+), 451 deletions(-) rename src/ImageSharp/Formats/Png/Zlib/{DeflaterThrowHelper.cs => DeflateThrowHelper.cs} (92%) diff --git a/src/ImageSharp/Formats/Png/Zlib/DeflaterThrowHelper.cs b/src/ImageSharp/Formats/Png/Zlib/DeflateThrowHelper.cs similarity index 92% rename from src/ImageSharp/Formats/Png/Zlib/DeflaterThrowHelper.cs rename to src/ImageSharp/Formats/Png/Zlib/DeflateThrowHelper.cs index b59d32c4d1..a360e5e87c 100644 --- a/src/ImageSharp/Formats/Png/Zlib/DeflaterThrowHelper.cs +++ b/src/ImageSharp/Formats/Png/Zlib/DeflateThrowHelper.cs @@ -6,7 +6,7 @@ using System.Runtime.CompilerServices; namespace SixLabors.ImageSharp.Formats.Png.Zlib { - internal static class DeflaterThrowHelper + internal static class DeflateThrowHelper { [MethodImpl(InliningOptions.ColdPath)] public static void ThrowAlreadyFinished() => throw new InvalidOperationException("Finish() already called."); diff --git a/src/ImageSharp/Formats/Png/Zlib/Deflater.cs b/src/ImageSharp/Formats/Png/Zlib/Deflater.cs index 26d1f9a45a..33b3019b89 100644 --- a/src/ImageSharp/Formats/Png/Zlib/Deflater.cs +++ b/src/ImageSharp/Formats/Png/Zlib/Deflater.cs @@ -181,7 +181,7 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib { if ((this.state & IsFinishing) != 0) { - DeflaterThrowHelper.ThrowAlreadyFinished(); + DeflateThrowHelper.ThrowAlreadyFinished(); } this.engine.SetInput(input, offset, count); @@ -230,7 +230,7 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib if (this.state == ClosedState) { - DeflaterThrowHelper.ThrowAlreadyClosed(); + DeflateThrowHelper.ThrowAlreadyClosed(); } if (this.state < BusyState) diff --git a/src/ImageSharp/Formats/Png/Zlib/DeflaterEngine.cs b/src/ImageSharp/Formats/Png/Zlib/DeflaterEngine.cs index c9967056e8..ddf571884a 100644 --- a/src/ImageSharp/Formats/Png/Zlib/DeflaterEngine.cs +++ b/src/ImageSharp/Formats/Png/Zlib/DeflaterEngine.cs @@ -1,7 +1,6 @@ // Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. -// using System; using System.Collections.Generic; using System.Text; @@ -51,54 +50,145 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib /// public class DeflaterEngine { - #region Constants - private const int TooFar = 4096; - #endregion Constants + // Hash index of string to be inserted + private int insertHashIndex; - #region Constructors + /// + /// Hashtable, hashing three characters to an index for window, so + /// that window[index]..window[index+2] have this hash code. + /// Note that the array should really be unsigned short, so you need + /// to and the values with 0xFFFF. + /// + private short[] head; /// - /// Construct instance with pending buffer - /// Adler calculation will be peformed + /// prev[index & WMASK] points to the previous index that has the + /// same hash code as the string starting at index. This way + /// entries with the same hash code are in a linked list. + /// Note that the array should really be unsigned short, so you need + /// to and the values with 0xFFFF. /// - /// - /// Pending buffer to use - /// - public DeflaterEngine(DeflaterPendingBuffer pending) - : this(pending, false) - { - } + private short[] prev; + private int matchStart; + // Length of best match + private int matchLen; + + // Set if previous match exists + private bool prevAvailable; + + private int blockStart; /// - /// Construct instance with pending buffer + /// Points to the current character in the window. /// - /// - /// Pending buffer to use - /// + private int strstart; + + /// + /// lookahead is the number of characters starting at strstart in + /// window that are valid. + /// So window[strstart] until window[strstart+lookahead-1] are valid + /// characters. + /// + private int lookahead; + + /// + /// This array contains the part of the uncompressed stream that + /// is of relevance. The current character is indexed by strstart. + /// + private byte[] window; + private int maxChain; + private int maxLazy; + private int niceLength; + private int goodLength; + + /// + /// The current compression function. + /// + private int compressionFunction; + + /// + /// The input data for compression. + /// + private byte[] inputBuf; + + /// + /// The total bytes of input read. + /// + private long totalIn; + + /// + /// The offset into inputBuf, where input data starts. + /// + private int inputOff; + + /// + /// The end offset of the input data. + /// + private int inputEnd; + + private DeflaterPendingBuffer pending; + private DeflaterHuffman huffman; + + /// + /// The adler checksum + /// + private Adler32 adler; + + /// + /// Initializes a new instance of the class. + /// + /// The pending buffer to use. /// /// If no adler calculation should be performed /// public DeflaterEngine(DeflaterPendingBuffer pending, bool noAdlerCalculation) { this.pending = pending; - huffman = new DeflaterHuffman(pending); + this.huffman = new DeflaterHuffman(pending); if (!noAdlerCalculation) - adler = new Adler32(); + { + this.adler = new Adler32(); + } - window = new byte[2 * DeflaterConstants.WSIZE]; - head = new short[DeflaterConstants.HASH_SIZE]; - prev = new short[DeflaterConstants.WSIZE]; + this.window = new byte[2 * DeflaterConstants.WSIZE]; + this.head = new short[DeflaterConstants.HASH_SIZE]; + this.prev = new short[DeflaterConstants.WSIZE]; // We start at index 1, to avoid an implementation deficiency, that // we cannot build a repeat pattern at index 0. - blockStart = strstart = 1; + this.blockStart = this.strstart = 1; } - #endregion Constructors + /// + /// Gets the current value of Adler checksum + /// + public int Adler + { + get + { + return (this.adler != null) ? unchecked((int)this.adler.Value) : 0; + } + } + + /// + /// Gets the total data processed + /// + public long TotalIn + { + get + { + return this.totalIn; + } + } + + /// + /// Gets or sets the deflate strategy + /// + public DeflateStrategy Strategy { get; set; } /// /// Deflate drives actual compression of data @@ -111,33 +201,28 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib bool progress; do { - FillWindow(); - bool canFlush = flush && (inputOff == inputEnd); + this.FillWindow(); + bool canFlush = flush && (this.inputOff == this.inputEnd); -#if DebugDeflation - if (DeflaterConstants.DEBUGGING) { - Console.WriteLine("window: [" + blockStart + "," + strstart + "," - + lookahead + "], " + compressionFunction + "," + canFlush); - } -#endif - switch (compressionFunction) + switch (this.compressionFunction) { case DeflaterConstants.DEFLATE_STORED: - progress = DeflateStored(canFlush, finish); + progress = this.DeflateStored(canFlush, finish); break; case DeflaterConstants.DEFLATE_FAST: - progress = DeflateFast(canFlush, finish); + progress = this.DeflateFast(canFlush, finish); break; case DeflaterConstants.DEFLATE_SLOW: - progress = DeflateSlow(canFlush, finish); + progress = this.DeflateSlow(canFlush, finish); break; default: throw new InvalidOperationException("unknown compressionFunction"); } - } while (pending.IsFlushed && progress); // repeat while we have no pending output and progress was made + } + while (this.pending.IsFlushed && progress); // repeat while we have no pending output and progress was made return progress; } @@ -165,24 +250,23 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib throw new ArgumentOutOfRangeException(nameof(count)); } - if (inputOff < inputEnd) + if (this.inputOff < this.inputEnd) { - throw new InvalidOperationException("Old input was not completely processed"); + throw new InvalidOperationException("Old input was not completely processed."); } int end = offset + count; - /* We want to throw an ArrayIndexOutOfBoundsException early. The - * check is very tricky: it also handles integer wrap around. - */ + // We want to throw an ArrayIndexOutOfBoundsException early. + // The check is very tricky: it also handles integer wrap around. if ((offset > end) || (end > buffer.Length)) { throw new ArgumentOutOfRangeException(nameof(count)); } - inputBuf = buffer; - inputOff = offset; - inputEnd = end; + this.inputBuf = buffer; + this.inputOff = offset; + this.inputEnd = end; } /// @@ -191,7 +275,7 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib /// Return true if input is needed via SetInput public bool NeedsInput() { - return (inputEnd == inputOff); + return this.inputEnd == this.inputOff; } /// @@ -202,13 +286,7 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib /// The length of the dictionary data. public void SetDictionary(byte[] buffer, int offset, int length) { -#if DebugDeflation - if (DeflaterConstants.DEBUGGING && (strstart != 1) ) - { - throw new InvalidOperationException("strstart not 1"); - } -#endif - adler?.Update(new ArraySegment(buffer, offset, length)); + this.adler?.Update(new ArraySegment(buffer, offset, length)); if (length < DeflaterConstants.MIN_MATCH) { return; @@ -220,17 +298,18 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib length = DeflaterConstants.MAX_DIST; } - System.Array.Copy(buffer, offset, window, strstart, length); + System.Array.Copy(buffer, offset, this.window, this.strstart, length); - UpdateHash(); + this.UpdateHash(); --length; while (--length > 0) { - InsertString(); - strstart++; + this.InsertString(); + this.strstart++; } - strstart += 2; - blockStart = strstart; + + this.strstart += 2; + this.blockStart = this.strstart; } /// @@ -238,22 +317,22 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib /// public void Reset() { - huffman.Reset(); - adler?.Reset(); - blockStart = strstart = 1; - lookahead = 0; - totalIn = 0; - prevAvailable = false; - matchLen = DeflaterConstants.MIN_MATCH - 1; + this.huffman.Reset(); + this.adler?.Reset(); + this.blockStart = this.strstart = 1; + this.lookahead = 0; + this.totalIn = 0; + this.prevAvailable = false; + this.matchLen = DeflaterConstants.MIN_MATCH - 1; for (int i = 0; i < DeflaterConstants.HASH_SIZE; i++) { - head[i] = 0; + this.head[i] = 0; } for (int i = 0; i < DeflaterConstants.WSIZE; i++) { - prev[i] = 0; + this.prev[i] = 0; } } @@ -262,44 +341,7 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib /// public void ResetAdler() { - adler?.Reset(); - } - - /// - /// Get current value of Adler checksum - /// - public int Adler - { - get - { - return (adler != null) ? unchecked((int)adler.Value) : 0; - } - } - - /// - /// Total data processed - /// - public long TotalIn - { - get - { - return totalIn; - } - } - - /// - /// Get/set the deflate strategy - /// - public DeflateStrategy Strategy - { - get - { - return strategy; - } - set - { - strategy = value; - } + this.adler?.Reset(); } /// @@ -313,55 +355,52 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib throw new ArgumentOutOfRangeException(nameof(level)); } - goodLength = DeflaterConstants.GOOD_LENGTH[level]; - max_lazy = DeflaterConstants.MAX_LAZY[level]; - niceLength = DeflaterConstants.NICE_LENGTH[level]; - max_chain = DeflaterConstants.MAX_CHAIN[level]; + this.goodLength = DeflaterConstants.GOOD_LENGTH[level]; + this.maxLazy = DeflaterConstants.MAX_LAZY[level]; + this.niceLength = DeflaterConstants.NICE_LENGTH[level]; + this.maxChain = DeflaterConstants.MAX_CHAIN[level]; - if (DeflaterConstants.COMPR_FUNC[level] != compressionFunction) + if (DeflaterConstants.COMPR_FUNC[level] != this.compressionFunction) { -#if DebugDeflation - if (DeflaterConstants.DEBUGGING) { - Console.WriteLine("Change from " + compressionFunction + " to " - + DeflaterConstants.COMPR_FUNC[level]); - } -#endif - switch (compressionFunction) + switch (this.compressionFunction) { case DeflaterConstants.DEFLATE_STORED: - if (strstart > blockStart) + if (this.strstart > this.blockStart) { - huffman.FlushStoredBlock(window, blockStart, - strstart - blockStart, false); - blockStart = strstart; + this.huffman.FlushStoredBlock(this.window, this.blockStart, this.strstart - this.blockStart, false); + this.blockStart = this.strstart; } - UpdateHash(); + + this.UpdateHash(); break; case DeflaterConstants.DEFLATE_FAST: - if (strstart > blockStart) + if (this.strstart > this.blockStart) { - huffman.FlushBlock(window, blockStart, strstart - blockStart, - false); - blockStart = strstart; + this.huffman.FlushBlock(this.window, this.blockStart, this.strstart - this.blockStart, false); + this.blockStart = this.strstart; } + break; case DeflaterConstants.DEFLATE_SLOW: - if (prevAvailable) + if (this.prevAvailable) { - huffman.TallyLit(window[strstart - 1] & 0xff); + this.huffman.TallyLit(this.window[this.strstart - 1] & 0xff); } - if (strstart > blockStart) + + if (this.strstart > this.blockStart) { - huffman.FlushBlock(window, blockStart, strstart - blockStart, false); - blockStart = strstart; + this.huffman.FlushBlock(this.window, this.blockStart, this.strstart - this.blockStart, false); + this.blockStart = this.strstart; } - prevAvailable = false; - matchLen = DeflaterConstants.MIN_MATCH - 1; + + this.prevAvailable = false; + this.matchLen = DeflaterConstants.MIN_MATCH - 1; break; } - compressionFunction = DeflaterConstants.COMPR_FUNC[level]; + + this.compressionFunction = DeflaterConstants.COMPR_FUNC[level]; } } @@ -370,48 +409,40 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib /// public void FillWindow() { - /* If the window is almost full and there is insufficient lookahead, - * move the upper half to the lower one to make room in the upper half. - */ - if (strstart >= DeflaterConstants.WSIZE + DeflaterConstants.MAX_DIST) + // If the window is almost full and there is insufficient lookahead, + // move the upper half to the lower one to make room in the upper half. + if (this.strstart >= DeflaterConstants.WSIZE + DeflaterConstants.MAX_DIST) { - SlideWindow(); + this.SlideWindow(); } - /* If there is not enough lookahead, but still some input left, - * read in the input - */ - if (lookahead < DeflaterConstants.MIN_LOOKAHEAD && inputOff < inputEnd) + // If there is not enough lookahead, but still some input left, read in the input. + if (this.lookahead < DeflaterConstants.MIN_LOOKAHEAD && this.inputOff < this.inputEnd) { - int more = 2 * DeflaterConstants.WSIZE - lookahead - strstart; + int more = (2 * DeflaterConstants.WSIZE) - this.lookahead - this.strstart; - if (more > inputEnd - inputOff) + if (more > this.inputEnd - this.inputOff) { - more = inputEnd - inputOff; + more = this.inputEnd - this.inputOff; } - System.Array.Copy(inputBuf, inputOff, window, strstart + lookahead, more); - adler?.Update(new ArraySegment(inputBuf, inputOff, more)); + Array.Copy(this.inputBuf, this.inputOff, this.window, this.strstart + this.lookahead, more); + this.adler?.Update(new ArraySegment(this.inputBuf, this.inputOff, more)); - inputOff += more; - totalIn += more; - lookahead += more; + this.inputOff += more; + this.totalIn += more; + this.lookahead += more; } - if (lookahead >= DeflaterConstants.MIN_MATCH) + if (this.lookahead >= DeflaterConstants.MIN_MATCH) { - UpdateHash(); + this.UpdateHash(); } } private void UpdateHash() { - /* - if (DEBUGGING) { - Console.WriteLine("updateHash: "+strstart); - } - */ - ins_h = (window[strstart] << DeflaterConstants.HASH_SHIFT) ^ window[strstart + 1]; + this.insertHashIndex = (this.window[this.strstart] << DeflaterConstants.HASH_SHIFT) ^ this.window[this.strstart + 1]; } /// @@ -422,90 +453,87 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib private int InsertString() { short match; - int hash = ((ins_h << DeflaterConstants.HASH_SHIFT) ^ window[strstart + (DeflaterConstants.MIN_MATCH - 1)]) & DeflaterConstants.HASH_MASK; + int hash = ((this.insertHashIndex << DeflaterConstants.HASH_SHIFT) ^ this.window[this.strstart + (DeflaterConstants.MIN_MATCH - 1)]) & DeflaterConstants.HASH_MASK; -#if DebugDeflation - if (DeflaterConstants.DEBUGGING) - { - if (hash != (((window[strstart] << (2*HASH_SHIFT)) ^ - (window[strstart + 1] << HASH_SHIFT) ^ - (window[strstart + 2])) & HASH_MASK)) { - throw new ImageFormatException("hash inconsistent: " + hash + "/" - +window[strstart] + "," - +window[strstart + 1] + "," - +window[strstart + 2] + "," + HASH_SHIFT); - } - } -#endif - prev[strstart & DeflaterConstants.WMASK] = match = head[hash]; - head[hash] = unchecked((short)strstart); - ins_h = hash; - return match & 0xffff; + this.prev[this.strstart & DeflaterConstants.WMASK] = match = this.head[hash]; + this.head[hash] = unchecked((short)this.strstart); + this.insertHashIndex = hash; + return match & 0xFFFF; } private void SlideWindow() { - Array.Copy(window, DeflaterConstants.WSIZE, window, 0, DeflaterConstants.WSIZE); - matchStart -= DeflaterConstants.WSIZE; - strstart -= DeflaterConstants.WSIZE; - blockStart -= DeflaterConstants.WSIZE; + Array.Copy(this.window, DeflaterConstants.WSIZE, this.window, 0, DeflaterConstants.WSIZE); + this.matchStart -= DeflaterConstants.WSIZE; + this.strstart -= DeflaterConstants.WSIZE; + this.blockStart -= DeflaterConstants.WSIZE; // Slide the hash table (could be avoided with 32 bit values // at the expense of memory usage). for (int i = 0; i < DeflaterConstants.HASH_SIZE; ++i) { - int m = head[i] & 0xffff; - head[i] = (short)(m >= DeflaterConstants.WSIZE ? (m - DeflaterConstants.WSIZE) : 0); + int m = this.head[i] & 0xFFFF; + this.head[i] = (short)(m >= DeflaterConstants.WSIZE ? (m - DeflaterConstants.WSIZE) : 0); } // Slide the prev table. for (int i = 0; i < DeflaterConstants.WSIZE; i++) { - int m = prev[i] & 0xffff; - prev[i] = (short)(m >= DeflaterConstants.WSIZE ? (m - DeflaterConstants.WSIZE) : 0); + int m = this.prev[i] & 0xFFFF; + this.prev[i] = (short)(m >= DeflaterConstants.WSIZE ? (m - DeflaterConstants.WSIZE) : 0); } } /// + /// /// Find the best (longest) string in the window matching the /// string starting at strstart. - /// + /// + /// /// Preconditions: /// /// strstart + DeflaterConstants.MAX_MATCH <= window.length. + /// /// - /// + /// The current match. /// True if a match greater than the minimum length is found private bool FindLongestMatch(int curMatch) { int match; - int scan = strstart; + int scan = this.strstart; + // scanMax is the highest position that we can look at - int scanMax = scan + Math.Min(DeflaterConstants.MAX_MATCH, lookahead) - 1; + int scanMax = scan + Math.Min(DeflaterConstants.MAX_MATCH, this.lookahead) - 1; int limit = Math.Max(scan - DeflaterConstants.MAX_DIST, 0); byte[] window = this.window; short[] prev = this.prev; - int chainLength = this.max_chain; - int niceLength = Math.Min(this.niceLength, lookahead); + int chainLength = this.maxChain; + int niceLength = Math.Min(this.niceLength, this.lookahead); - matchLen = Math.Max(matchLen, DeflaterConstants.MIN_MATCH - 1); + this.matchLen = Math.Max(this.matchLen, DeflaterConstants.MIN_MATCH - 1); - if (scan + matchLen > scanMax) return false; + if (scan + this.matchLen > scanMax) + { + return false; + } - byte scan_end1 = window[scan + matchLen - 1]; - byte scan_end = window[scan + matchLen]; + byte scan_end1 = window[scan + this.matchLen - 1]; + byte scan_end = window[scan + this.matchLen]; // Do not waste too much time if we already have a good match: - if (matchLen >= this.goodLength) chainLength >>= 2; + if (this.matchLen >= this.goodLength) + { + chainLength >>= 2; + } do { match = curMatch; - scan = strstart; + scan = this.strstart; - if (window[match + matchLen] != scan_end - || window[match + matchLen - 1] != scan_end1 + if (window[match + this.matchLen] != scan_end + || window[match + this.matchLen - 1] != scan_end1 || window[match] != window[scan] || window[++match] != window[++scan]) { @@ -516,120 +544,144 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib // scanMax - scan is the maximum number of bytes we can compare. // below we compare 8 bytes at a time, so first we compare // (scanMax - scan) % 8 bytes, so the remainder is a multiple of 8 - switch ((scanMax - scan) % 8) { case 1: - if (window[++scan] == window[++match]) break; + if (window[++scan] == window[++match]) + { + break; + } + break; case 2: if (window[++scan] == window[++match] - && window[++scan] == window[++match]) break; + && window[++scan] == window[++match]) + { + break; + } + break; case 3: if (window[++scan] == window[++match] - && window[++scan] == window[++match] - && window[++scan] == window[++match]) break; + && window[++scan] == window[++match] + && window[++scan] == window[++match]) + { + break; + } + break; case 4: if (window[++scan] == window[++match] - && window[++scan] == window[++match] - && window[++scan] == window[++match] - && window[++scan] == window[++match]) break; + && window[++scan] == window[++match] + && window[++scan] == window[++match] + && window[++scan] == window[++match]) + { + break; + } + break; case 5: if (window[++scan] == window[++match] - && window[++scan] == window[++match] - && window[++scan] == window[++match] - && window[++scan] == window[++match] - && window[++scan] == window[++match]) break; + && window[++scan] == window[++match] + && window[++scan] == window[++match] + && window[++scan] == window[++match] + && window[++scan] == window[++match]) + { + break; + } + break; case 6: if (window[++scan] == window[++match] - && window[++scan] == window[++match] - && window[++scan] == window[++match] - && window[++scan] == window[++match] - && window[++scan] == window[++match] - && window[++scan] == window[++match]) break; + && window[++scan] == window[++match] + && window[++scan] == window[++match] + && window[++scan] == window[++match] + && window[++scan] == window[++match] + && window[++scan] == window[++match]) + { + break; + } + break; case 7: if (window[++scan] == window[++match] - && window[++scan] == window[++match] - && window[++scan] == window[++match] - && window[++scan] == window[++match] - && window[++scan] == window[++match] - && window[++scan] == window[++match] - && window[++scan] == window[++match]) break; + && window[++scan] == window[++match] + && window[++scan] == window[++match] + && window[++scan] == window[++match] + && window[++scan] == window[++match] + && window[++scan] == window[++match] + && window[++scan] == window[++match]) + { + break; + } + break; } if (window[scan] == window[match]) { - /* We check for insufficient lookahead only every 8th comparison; - * the 256th check will be made at strstart + 258 unless lookahead is - * exhausted first. - */ + // We check for insufficient lookahead only every 8th comparison; + // the 256th check will be made at strstart + 258 unless lookahead is + // exhausted first. do { if (scan == scanMax) { - ++scan; // advance to first position not matched + ++scan; // advance to first position not matched ++match; break; } } while (window[++scan] == window[++match] - && window[++scan] == window[++match] - && window[++scan] == window[++match] - && window[++scan] == window[++match] - && window[++scan] == window[++match] - && window[++scan] == window[++match] - && window[++scan] == window[++match] - && window[++scan] == window[++match]); + && window[++scan] == window[++match] + && window[++scan] == window[++match] + && window[++scan] == window[++match] + && window[++scan] == window[++match] + && window[++scan] == window[++match] + && window[++scan] == window[++match] + && window[++scan] == window[++match]); } - if (scan - strstart > matchLen) + if (scan - this.strstart > this.matchLen) { -#if DebugDeflation - if (DeflaterConstants.DEBUGGING && (ins_h == 0) ) - Console.Error.WriteLine("Found match: " + curMatch + "-" + (scan - strstart)); -#endif + this.matchStart = curMatch; + this.matchLen = scan - this.strstart; - matchStart = curMatch; - matchLen = scan - strstart; - - if (matchLen >= niceLength) + if (this.matchLen >= niceLength) + { break; + } scan_end1 = window[scan - 1]; scan_end = window[scan]; } - } while ((curMatch = (prev[curMatch & DeflaterConstants.WMASK] & 0xffff)) > limit && 0 != --chainLength); + } + while ((curMatch = prev[curMatch & DeflaterConstants.WMASK] & 0xFFFF) > limit && --chainLength != 0); - return matchLen >= DeflaterConstants.MIN_MATCH; + return this.matchLen >= DeflaterConstants.MIN_MATCH; } private bool DeflateStored(bool flush, bool finish) { - if (!flush && (lookahead == 0)) + if (!flush && (this.lookahead == 0)) { return false; } - strstart += lookahead; - lookahead = 0; + this.strstart += this.lookahead; + this.lookahead = 0; - int storedLength = strstart - blockStart; + int storedLength = this.strstart - this.blockStart; if ((storedLength >= DeflaterConstants.MAX_BLOCK_SIZE) || // Block is full - (blockStart < DeflaterConstants.WSIZE && storedLength >= DeflaterConstants.MAX_DIST) || // Block may move out of window + (this.blockStart < DeflaterConstants.WSIZE && storedLength >= DeflaterConstants.MAX_DIST) || // Block may move out of window flush) { bool lastBlock = finish; @@ -639,86 +691,70 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib lastBlock = false; } -#if DebugDeflation - if (DeflaterConstants.DEBUGGING) - { - Console.WriteLine("storedBlock[" + storedLength + "," + lastBlock + "]"); - } -#endif - - huffman.FlushStoredBlock(window, blockStart, storedLength, lastBlock); - blockStart += storedLength; + this.huffman.FlushStoredBlock(this.window, this.blockStart, storedLength, lastBlock); + this.blockStart += storedLength; return !(lastBlock || storedLength == 0); } + return true; } private bool DeflateFast(bool flush, bool finish) { - if (lookahead < DeflaterConstants.MIN_LOOKAHEAD && !flush) + if (this.lookahead < DeflaterConstants.MIN_LOOKAHEAD && !flush) { return false; } - while (lookahead >= DeflaterConstants.MIN_LOOKAHEAD || flush) + while (this.lookahead >= DeflaterConstants.MIN_LOOKAHEAD || flush) { - if (lookahead == 0) + if (this.lookahead == 0) { // We are flushing everything - huffman.FlushBlock(window, blockStart, strstart - blockStart, finish); - blockStart = strstart; + this.huffman.FlushBlock(this.window, this.blockStart, this.strstart - this.blockStart, finish); + this.blockStart = this.strstart; return false; } - if (strstart > 2 * DeflaterConstants.WSIZE - DeflaterConstants.MIN_LOOKAHEAD) + if (this.strstart > (2 * DeflaterConstants.WSIZE) - DeflaterConstants.MIN_LOOKAHEAD) { - /* slide window, as FindLongestMatch needs this. - * This should only happen when flushing and the window - * is almost full. - */ - SlideWindow(); + // slide window, as FindLongestMatch needs this. + // This should only happen when flushing and the window + // is almost full. + this.SlideWindow(); } int hashHead; - if (lookahead >= DeflaterConstants.MIN_MATCH && - (hashHead = InsertString()) != 0 && - strategy != DeflateStrategy.HuffmanOnly && - strstart - hashHead <= DeflaterConstants.MAX_DIST && - FindLongestMatch(hashHead)) + if (this.lookahead >= DeflaterConstants.MIN_MATCH && + (hashHead = this.InsertString()) != 0 && + this.Strategy != DeflateStrategy.HuffmanOnly && + this.strstart - hashHead <= DeflaterConstants.MAX_DIST && + this.FindLongestMatch(hashHead)) { // longestMatch sets matchStart and matchLen -#if DebugDeflation - if (DeflaterConstants.DEBUGGING) - { - for (int i = 0 ; i < matchLen; i++) { - if (window[strstart + i] != window[matchStart + i]) { - throw new ImageFormatException("Match failure"); - } - } - } -#endif + bool full = this.huffman.TallyDist(this.strstart - this.matchStart, this.matchLen); - bool full = huffman.TallyDist(strstart - matchStart, matchLen); - - lookahead -= matchLen; - if (matchLen <= max_lazy && lookahead >= DeflaterConstants.MIN_MATCH) + this.lookahead -= this.matchLen; + if (this.matchLen <= this.maxLazy && this.lookahead >= DeflaterConstants.MIN_MATCH) { - while (--matchLen > 0) + while (--this.matchLen > 0) { - ++strstart; - InsertString(); + ++this.strstart; + this.InsertString(); } - ++strstart; + + ++this.strstart; } else { - strstart += matchLen; - if (lookahead >= DeflaterConstants.MIN_MATCH - 1) + this.strstart += this.matchLen; + if (this.lookahead >= DeflaterConstants.MIN_MATCH - 1) { - UpdateHash(); + this.UpdateHash(); } } - matchLen = DeflaterConstants.MIN_MATCH - 1; + + this.matchLen = DeflaterConstants.MIN_MATCH - 1; if (!full) { continue; @@ -727,84 +763,77 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib else { // No match found - huffman.TallyLit(window[strstart] & 0xff); - ++strstart; - --lookahead; + this.huffman.TallyLit(this.window[this.strstart] & 0xff); + ++this.strstart; + --this.lookahead; } - if (huffman.IsFull()) + if (this.huffman.IsFull()) { - bool lastBlock = finish && (lookahead == 0); - huffman.FlushBlock(window, blockStart, strstart - blockStart, lastBlock); - blockStart = strstart; + bool lastBlock = finish && (this.lookahead == 0); + this.huffman.FlushBlock(this.window, this.blockStart, this.strstart - this.blockStart, lastBlock); + this.blockStart = this.strstart; return !lastBlock; } } + return true; } private bool DeflateSlow(bool flush, bool finish) { - if (lookahead < DeflaterConstants.MIN_LOOKAHEAD && !flush) + if (this.lookahead < DeflaterConstants.MIN_LOOKAHEAD && !flush) { return false; } - while (lookahead >= DeflaterConstants.MIN_LOOKAHEAD || flush) + while (this.lookahead >= DeflaterConstants.MIN_LOOKAHEAD || flush) { - if (lookahead == 0) + if (this.lookahead == 0) { - if (prevAvailable) + if (this.prevAvailable) { - huffman.TallyLit(window[strstart - 1] & 0xff); + this.huffman.TallyLit(this.window[this.strstart - 1] & 0xff); } - prevAvailable = false; + + this.prevAvailable = false; // We are flushing everything -#if DebugDeflation - if (DeflaterConstants.DEBUGGING && !flush) - { - throw new ImageFormatException("Not flushing, but no lookahead"); - } -#endif - huffman.FlushBlock(window, blockStart, strstart - blockStart, - finish); - blockStart = strstart; + this.huffman.FlushBlock(this.window, this.blockStart, this.strstart - this.blockStart, finish); + this.blockStart = this.strstart; return false; } - if (strstart >= 2 * DeflaterConstants.WSIZE - DeflaterConstants.MIN_LOOKAHEAD) + if (this.strstart >= (2 * DeflaterConstants.WSIZE) - DeflaterConstants.MIN_LOOKAHEAD) { - /* slide window, as FindLongestMatch needs this. - * This should only happen when flushing and the window - * is almost full. - */ - SlideWindow(); + // slide window, as FindLongestMatch needs this. + // This should only happen when flushing and the window + // is almost full. + this.SlideWindow(); } - int prevMatch = matchStart; - int prevLen = matchLen; - if (lookahead >= DeflaterConstants.MIN_MATCH) + int prevMatch = this.matchStart; + int prevLen = this.matchLen; + if (this.lookahead >= DeflaterConstants.MIN_MATCH) { - int hashHead = InsertString(); + int hashHead = this.InsertString(); - if (strategy != DeflateStrategy.HuffmanOnly && + if (this.Strategy != DeflateStrategy.HuffmanOnly && hashHead != 0 && - strstart - hashHead <= DeflaterConstants.MAX_DIST && - FindLongestMatch(hashHead)) + this.strstart - hashHead <= DeflaterConstants.MAX_DIST && + this.FindLongestMatch(hashHead)) { // longestMatch sets matchStart and matchLen - // Discard match if too small and too far away - if (matchLen <= 5 && (strategy == DeflateStrategy.Filtered || (matchLen == DeflaterConstants.MIN_MATCH && strstart - matchStart > TooFar))) + if (this.matchLen <= 5 && (this.Strategy == DeflateStrategy.Filtered || (this.matchLen == DeflaterConstants.MIN_MATCH && this.strstart - this.matchStart > TooFar))) { - matchLen = DeflaterConstants.MIN_MATCH - 1; + this.matchLen = DeflaterConstants.MIN_MATCH - 1; } } } // previous match was better - if ((prevLen >= DeflaterConstants.MIN_MATCH) && (matchLen <= prevLen)) + if ((prevLen >= DeflaterConstants.MIN_MATCH) && (this.matchLen <= prevLen)) { #if DebugDeflation if (DeflaterConstants.DEBUGGING) @@ -815,137 +844,52 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib } } #endif - huffman.TallyDist(strstart - 1 - prevMatch, prevLen); + this.huffman.TallyDist(this.strstart - 1 - prevMatch, prevLen); prevLen -= 2; do { - strstart++; - lookahead--; - if (lookahead >= DeflaterConstants.MIN_MATCH) + this.strstart++; + this.lookahead--; + if (this.lookahead >= DeflaterConstants.MIN_MATCH) { - InsertString(); + this.InsertString(); } - } while (--prevLen > 0); + } + while (--prevLen > 0); - strstart++; - lookahead--; - prevAvailable = false; - matchLen = DeflaterConstants.MIN_MATCH - 1; + this.strstart++; + this.lookahead--; + this.prevAvailable = false; + this.matchLen = DeflaterConstants.MIN_MATCH - 1; } else { - if (prevAvailable) + if (this.prevAvailable) { - huffman.TallyLit(window[strstart - 1] & 0xff); + this.huffman.TallyLit(this.window[this.strstart - 1] & 0xff); } - prevAvailable = true; - strstart++; - lookahead--; + + this.prevAvailable = true; + this.strstart++; + this.lookahead--; } - if (huffman.IsFull()) + if (this.huffman.IsFull()) { - int len = strstart - blockStart; - if (prevAvailable) + int len = this.strstart - this.blockStart; + if (this.prevAvailable) { len--; } - bool lastBlock = (finish && (lookahead == 0) && !prevAvailable); - huffman.FlushBlock(window, blockStart, len, lastBlock); - blockStart += len; + + bool lastBlock = finish && (this.lookahead == 0) && !this.prevAvailable; + this.huffman.FlushBlock(this.window, this.blockStart, len, lastBlock); + this.blockStart += len; return !lastBlock; } } + return true; } - - #region Instance Fields - - // Hash index of string to be inserted - private int ins_h; - - /// - /// Hashtable, hashing three characters to an index for window, so - /// that window[index]..window[index+2] have this hash code. - /// Note that the array should really be unsigned short, so you need - /// to and the values with 0xffff. - /// - private short[] head; - - /// - /// prev[index & WMASK] points to the previous index that has the - /// same hash code as the string starting at index. This way - /// entries with the same hash code are in a linked list. - /// Note that the array should really be unsigned short, so you need - /// to and the values with 0xffff. - /// - private short[] prev; - - private int matchStart; - - // Length of best match - private int matchLen; - - // Set if previous match exists - private bool prevAvailable; - - private int blockStart; - - /// - /// Points to the current character in the window. - /// - private int strstart; - - /// - /// lookahead is the number of characters starting at strstart in - /// window that are valid. - /// So window[strstart] until window[strstart+lookahead-1] are valid - /// characters. - /// - private int lookahead; - - /// - /// This array contains the part of the uncompressed stream that - /// is of relevance. The current character is indexed by strstart. - /// - private byte[] window; - - private DeflateStrategy strategy; - private int max_chain, max_lazy, niceLength, goodLength; - - /// - /// The current compression function. - /// - private int compressionFunction; - - /// - /// The input data for compression. - /// - private byte[] inputBuf; - - /// - /// The total bytes of input read. - /// - private long totalIn; - - /// - /// The offset into inputBuf, where input data starts. - /// - private int inputOff; - - /// - /// The end offset of the input data. - /// - private int inputEnd; - - private DeflaterPendingBuffer pending; - private DeflaterHuffman huffman; - - /// - /// The adler checksum - /// - private Adler32 adler; - - #endregion Instance Fields } } From 3079574c3cd43a1db5a4b0aeb693d7db93f2f662 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Tue, 12 Nov 2019 21:38:03 +1100 Subject: [PATCH 59/75] Make DeflaterEngine disposable --- src/ImageSharp/Formats/Png/Zlib/Deflater.cs | 40 +-- .../Formats/Png/Zlib/DeflaterEngine.cs | 273 ++++++++---------- 2 files changed, 129 insertions(+), 184 deletions(-) diff --git a/src/ImageSharp/Formats/Png/Zlib/Deflater.cs b/src/ImageSharp/Formats/Png/Zlib/Deflater.cs index 33b3019b89..5732f82d22 100644 --- a/src/ImageSharp/Formats/Png/Zlib/Deflater.cs +++ b/src/ImageSharp/Formats/Png/Zlib/Deflater.cs @@ -54,7 +54,6 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib private DeflaterEngine engine; private bool isDisposed; - private const int IsSetDict = 0x01; private const int IsFlushing = 0x04; private const int IsFinishing = 0x08; private const int BusyState = 0x10; @@ -82,8 +81,10 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib } this.pending = new DeflaterPendingBuffer(memoryAllocator); - this.engine = new DeflaterEngine(this.pending, true); - this.engine.Strategy = DeflateStrategy.Default; + + // TODO: Possibly provide DeflateStrategy as an option. + this.engine = new DeflaterEngine(memoryAllocator, this.pending, DeflateStrategy.Default); + this.SetLevel(level); this.Reset(); } @@ -233,37 +234,6 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib DeflateThrowHelper.ThrowAlreadyClosed(); } - if (this.state < BusyState) - { - // Output header - int header = (Deflated + ((DeflaterConstants.MAX_WBITS - 8) << 4)) << 8; - int levelFlags = (this.level - 1) >> 1; - if (levelFlags < 0 || levelFlags > 3) - { - levelFlags = 3; - } - - header |= levelFlags << 6; - if ((this.state & IsSetDict) != 0) - { - // Dictionary was set - header |= DeflaterConstants.PRESET_DICT; - } - - header += 31 - (header % 31); - - this.pending.WriteShortMSB(header); - if ((this.state & IsSetDict) != 0) - { - int chksum = this.engine.Adler; - this.engine.ResetAdler(); - this.pending.WriteShortMSB(chksum >> 16); - this.pending.WriteShortMSB(chksum & 0xffff); - } - - this.state = BusyState | (this.state & (IsFlushing | IsFinishing)); - } - while (true) { int count = this.pending.Flush(output, offset, length); @@ -326,9 +296,11 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib if (disposing) { this.pending.Dispose(); + this.engine.Dispose(); } this.pending = null; + this.engine = null; this.isDisposed = true; } } diff --git a/src/ImageSharp/Formats/Png/Zlib/DeflaterEngine.cs b/src/ImageSharp/Formats/Png/Zlib/DeflaterEngine.cs index ddf571884a..0870f629c5 100644 --- a/src/ImageSharp/Formats/Png/Zlib/DeflaterEngine.cs +++ b/src/ImageSharp/Formats/Png/Zlib/DeflaterEngine.cs @@ -2,8 +2,9 @@ // Licensed under the Apache License, Version 2.0. using System; -using System.Collections.Generic; -using System.Text; +using System.Buffers; +using System.Runtime.CompilerServices; +using SixLabors.Memory; namespace SixLabors.ImageSharp.Formats.Png.Zlib { @@ -48,30 +49,13 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib /// Low level compression engine for deflate algorithm which uses a 32K sliding window /// with secondary compression from Huffman/Shannon-Fano codes. /// - public class DeflaterEngine + public sealed unsafe class DeflaterEngine : IDisposable { private const int TooFar = 4096; // Hash index of string to be inserted private int insertHashIndex; - /// - /// Hashtable, hashing three characters to an index for window, so - /// that window[index]..window[index+2] have this hash code. - /// Note that the array should really be unsigned short, so you need - /// to and the values with 0xFFFF. - /// - private short[] head; - - /// - /// prev[index & WMASK] points to the previous index that has the - /// same hash code as the string starting at index. This way - /// entries with the same hash code are in a linked list. - /// Note that the array should really be unsigned short, so you need - /// to and the values with 0xFFFF. - /// - private short[] prev; - private int matchStart; // Length of best match @@ -95,16 +79,6 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib /// private int lookahead; - /// - /// This array contains the part of the uncompressed stream that - /// is of relevance. The current character is indexed by strstart. - /// - private byte[] window; - private int maxChain; - private int maxLazy; - private int niceLength; - private int goodLength; - /// /// The current compression function. /// @@ -115,11 +89,6 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib /// private byte[] inputBuf; - /// - /// The total bytes of input read. - /// - private long totalIn; - /// /// The offset into inputBuf, where input data starts. /// @@ -130,31 +99,59 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib /// private int inputEnd; + private DeflateStrategy strategy; private DeflaterPendingBuffer pending; private DeflaterHuffman huffman; + private bool isDisposed; /// - /// The adler checksum + /// Hashtable, hashing three characters to an index for window, so + /// that window[index]..window[index+2] have this hash code. + /// Note that the array should really be unsigned short, so you need + /// to and the values with 0xFFFF. /// - private Adler32 adler; + private short[] head; + + /// + /// prev[index & WMASK] points to the previous index that has the + /// same hash code as the string starting at index. This way + /// entries with the same hash code are in a linked list. + /// Note that the array should really be unsigned short, so you need + /// to and the values with 0xFFFF. + /// + private short[] prev; + + /// + /// This array contains the part of the uncompressed stream that + /// is of relevance. The current character is indexed by strstart. + /// + private readonly IManagedByteBuffer windowBuffer; + private MemoryHandle windowBufferHandle; + private byte[] window; + private readonly byte* pinnedWindowPointer; + + private int maxChain; + private int maxLazy; + private int niceLength; + private int goodLength; /// /// Initializes a new instance of the class. /// + /// The memory allocator to use for buffer allocations. /// The pending buffer to use. - /// - /// If no adler calculation should be performed - /// - public DeflaterEngine(DeflaterPendingBuffer pending, bool noAdlerCalculation) + /// The deflate strategy to use. + public DeflaterEngine(MemoryAllocator memoryAllocator, DeflaterPendingBuffer pending, DeflateStrategy strategy) { this.pending = pending; this.huffman = new DeflaterHuffman(pending); - if (!noAdlerCalculation) - { - this.adler = new Adler32(); - } + this.strategy = strategy; + + this.windowBuffer = memoryAllocator.AllocateManagedByteBuffer(2 * DeflaterConstants.WSIZE); + this.window = this.windowBuffer.Array; + this.windowBufferHandle = this.windowBuffer.Memory.Pin(); + this.pinnedWindowPointer = (byte*)this.windowBufferHandle.Pointer; - this.window = new byte[2 * DeflaterConstants.WSIZE]; this.head = new short[DeflaterConstants.HASH_SIZE]; this.prev = new short[DeflaterConstants.WSIZE]; @@ -163,33 +160,6 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib this.blockStart = this.strstart = 1; } - /// - /// Gets the current value of Adler checksum - /// - public int Adler - { - get - { - return (this.adler != null) ? unchecked((int)this.adler.Value) : 0; - } - } - - /// - /// Gets the total data processed - /// - public long TotalIn - { - get - { - return this.totalIn; - } - } - - /// - /// Gets or sets the deflate strategy - /// - public DeflateStrategy Strategy { get; set; } - /// /// Deflate drives actual compression of data /// @@ -286,7 +256,6 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib /// The length of the dictionary data. public void SetDictionary(byte[] buffer, int offset, int length) { - this.adler?.Update(new ArraySegment(buffer, offset, length)); if (length < DeflaterConstants.MIN_MATCH) { return; @@ -318,10 +287,8 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib public void Reset() { this.huffman.Reset(); - this.adler?.Reset(); this.blockStart = this.strstart = 1; this.lookahead = 0; - this.totalIn = 0; this.prevAvailable = false; this.matchLen = DeflaterConstants.MIN_MATCH - 1; @@ -336,14 +303,6 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib } } - /// - /// Reset Adler checksum - /// - public void ResetAdler() - { - this.adler?.Reset(); - } - /// /// Set the deflate level (0-9) /// @@ -386,7 +345,7 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib case DeflaterConstants.DEFLATE_SLOW: if (this.prevAvailable) { - this.huffman.TallyLit(this.window[this.strstart - 1] & 0xff); + this.huffman.TallyLit(this.pinnedWindowPointer[this.strstart - 1] & 0xFF); } if (this.strstart > this.blockStart) @@ -427,10 +386,8 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib } Array.Copy(this.inputBuf, this.inputOff, this.window, this.strstart + this.lookahead, more); - this.adler?.Update(new ArraySegment(this.inputBuf, this.inputOff, more)); this.inputOff += more; - this.totalIn += more; this.lookahead += more; } @@ -440,9 +397,19 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib } } + /// + public void Dispose() + { + // Do not change this code. Put cleanup code in Dispose(bool disposing) above. + this.Dispose(true); + GC.SuppressFinalize(this); + } + + [MethodImpl(InliningOptions.ShortMethod)] private void UpdateHash() { - this.insertHashIndex = (this.window[this.strstart] << DeflaterConstants.HASH_SHIFT) ^ this.window[this.strstart + 1]; + byte* pinned = this.pinnedWindowPointer; + this.insertHashIndex = (pinned[this.strstart] << DeflaterConstants.HASH_SHIFT) ^ pinned[this.strstart + 1]; } /// @@ -453,7 +420,7 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib private int InsertString() { short match; - int hash = ((this.insertHashIndex << DeflaterConstants.HASH_SHIFT) ^ this.window[this.strstart + (DeflaterConstants.MIN_MATCH - 1)]) & DeflaterConstants.HASH_MASK; + int hash = ((this.insertHashIndex << DeflaterConstants.HASH_SHIFT) ^ this.pinnedWindowPointer[this.strstart + (DeflaterConstants.MIN_MATCH - 1)]) & DeflaterConstants.HASH_MASK; this.prev[this.strstart & DeflaterConstants.WMASK] = match = this.head[hash]; this.head[hash] = unchecked((short)this.strstart); @@ -463,7 +430,7 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib private void SlideWindow() { - Array.Copy(this.window, DeflaterConstants.WSIZE, this.window, 0, DeflaterConstants.WSIZE); + Unsafe.CopyBlockUnaligned(ref this.window[0], ref this.window[DeflaterConstants.WSIZE], DeflaterConstants.WSIZE); this.matchStart -= DeflaterConstants.WSIZE; this.strstart -= DeflaterConstants.WSIZE; this.blockStart -= DeflaterConstants.WSIZE; @@ -518,8 +485,9 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib return false; } - byte scan_end1 = window[scan + this.matchLen - 1]; - byte scan_end = window[scan + this.matchLen]; + byte* pinnedWindow = this.pinnedWindowPointer; + byte scan_end1 = pinnedWindow[scan + this.matchLen - 1]; + byte scan_end = pinnedWindow[scan + this.matchLen]; // Do not waste too much time if we already have a good match: if (this.matchLen >= this.goodLength) @@ -532,10 +500,10 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib match = curMatch; scan = this.strstart; - if (window[match + this.matchLen] != scan_end - || window[match + this.matchLen - 1] != scan_end1 - || window[match] != window[scan] - || window[++match] != window[++scan]) + if (pinnedWindow[match + this.matchLen] != scan_end + || pinnedWindow[match + this.matchLen - 1] != scan_end1 + || pinnedWindow[match] != pinnedWindow[scan] + || pinnedWindow[++match] != pinnedWindow[++scan]) { continue; } @@ -547,7 +515,7 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib switch ((scanMax - scan) % 8) { case 1: - if (window[++scan] == window[++match]) + if (pinnedWindow[++scan] == pinnedWindow[++match]) { break; } @@ -555,8 +523,8 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib break; case 2: - if (window[++scan] == window[++match] - && window[++scan] == window[++match]) + if (pinnedWindow[++scan] == pinnedWindow[++match] + && pinnedWindow[++scan] == pinnedWindow[++match]) { break; } @@ -564,9 +532,9 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib break; case 3: - if (window[++scan] == window[++match] - && window[++scan] == window[++match] - && window[++scan] == window[++match]) + if (pinnedWindow[++scan] == pinnedWindow[++match] + && pinnedWindow[++scan] == pinnedWindow[++match] + && pinnedWindow[++scan] == pinnedWindow[++match]) { break; } @@ -574,10 +542,10 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib break; case 4: - if (window[++scan] == window[++match] - && window[++scan] == window[++match] - && window[++scan] == window[++match] - && window[++scan] == window[++match]) + if (pinnedWindow[++scan] == pinnedWindow[++match] + && pinnedWindow[++scan] == pinnedWindow[++match] + && pinnedWindow[++scan] == pinnedWindow[++match] + && pinnedWindow[++scan] == pinnedWindow[++match]) { break; } @@ -585,11 +553,11 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib break; case 5: - if (window[++scan] == window[++match] - && window[++scan] == window[++match] - && window[++scan] == window[++match] - && window[++scan] == window[++match] - && window[++scan] == window[++match]) + if (pinnedWindow[++scan] == pinnedWindow[++match] + && pinnedWindow[++scan] == pinnedWindow[++match] + && pinnedWindow[++scan] == pinnedWindow[++match] + && pinnedWindow[++scan] == pinnedWindow[++match] + && pinnedWindow[++scan] == pinnedWindow[++match]) { break; } @@ -597,12 +565,12 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib break; case 6: - if (window[++scan] == window[++match] - && window[++scan] == window[++match] - && window[++scan] == window[++match] - && window[++scan] == window[++match] - && window[++scan] == window[++match] - && window[++scan] == window[++match]) + if (pinnedWindow[++scan] == pinnedWindow[++match] + && pinnedWindow[++scan] == pinnedWindow[++match] + && pinnedWindow[++scan] == pinnedWindow[++match] + && pinnedWindow[++scan] == pinnedWindow[++match] + && pinnedWindow[++scan] == pinnedWindow[++match] + && pinnedWindow[++scan] == pinnedWindow[++match]) { break; } @@ -610,13 +578,13 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib break; case 7: - if (window[++scan] == window[++match] - && window[++scan] == window[++match] - && window[++scan] == window[++match] - && window[++scan] == window[++match] - && window[++scan] == window[++match] - && window[++scan] == window[++match] - && window[++scan] == window[++match]) + if (pinnedWindow[++scan] == pinnedWindow[++match] + && pinnedWindow[++scan] == pinnedWindow[++match] + && pinnedWindow[++scan] == pinnedWindow[++match] + && pinnedWindow[++scan] == pinnedWindow[++match] + && pinnedWindow[++scan] == pinnedWindow[++match] + && pinnedWindow[++scan] == pinnedWindow[++match] + && pinnedWindow[++scan] == pinnedWindow[++match]) { break; } @@ -624,7 +592,7 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib break; } - if (window[scan] == window[match]) + if (pinnedWindow[scan] == pinnedWindow[match]) { // We check for insufficient lookahead only every 8th comparison; // the 256th check will be made at strstart + 258 unless lookahead is @@ -639,14 +607,14 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib break; } } - while (window[++scan] == window[++match] - && window[++scan] == window[++match] - && window[++scan] == window[++match] - && window[++scan] == window[++match] - && window[++scan] == window[++match] - && window[++scan] == window[++match] - && window[++scan] == window[++match] - && window[++scan] == window[++match]); + while (pinnedWindow[++scan] == pinnedWindow[++match] + && pinnedWindow[++scan] == pinnedWindow[++match] + && pinnedWindow[++scan] == pinnedWindow[++match] + && pinnedWindow[++scan] == pinnedWindow[++match] + && pinnedWindow[++scan] == pinnedWindow[++match] + && pinnedWindow[++scan] == pinnedWindow[++match] + && pinnedWindow[++scan] == pinnedWindow[++match] + && pinnedWindow[++scan] == pinnedWindow[++match]); } if (scan - this.strstart > this.matchLen) @@ -659,8 +627,8 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib break; } - scan_end1 = window[scan - 1]; - scan_end = window[scan]; + scan_end1 = pinnedWindow[scan - 1]; + scan_end = pinnedWindow[scan]; } } while ((curMatch = prev[curMatch & DeflaterConstants.WMASK] & 0xFFFF) > limit && --chainLength != 0); @@ -727,7 +695,7 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib int hashHead; if (this.lookahead >= DeflaterConstants.MIN_MATCH && (hashHead = this.InsertString()) != 0 && - this.Strategy != DeflateStrategy.HuffmanOnly && + this.strategy != DeflateStrategy.HuffmanOnly && this.strstart - hashHead <= DeflaterConstants.MAX_DIST && this.FindLongestMatch(hashHead)) { @@ -763,7 +731,7 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib else { // No match found - this.huffman.TallyLit(this.window[this.strstart] & 0xff); + this.huffman.TallyLit(this.pinnedWindowPointer[this.strstart] & 0xff); ++this.strstart; --this.lookahead; } @@ -793,7 +761,7 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib { if (this.prevAvailable) { - this.huffman.TallyLit(this.window[this.strstart - 1] & 0xff); + this.huffman.TallyLit(this.pinnedWindowPointer[this.strstart - 1] & 0xff); } this.prevAvailable = false; @@ -818,14 +786,14 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib { int hashHead = this.InsertString(); - if (this.Strategy != DeflateStrategy.HuffmanOnly && + if (this.strategy != DeflateStrategy.HuffmanOnly && hashHead != 0 && this.strstart - hashHead <= DeflaterConstants.MAX_DIST && this.FindLongestMatch(hashHead)) { // longestMatch sets matchStart and matchLen // Discard match if too small and too far away - if (this.matchLen <= 5 && (this.Strategy == DeflateStrategy.Filtered || (this.matchLen == DeflaterConstants.MIN_MATCH && this.strstart - this.matchStart > TooFar))) + if (this.matchLen <= 5 && (this.strategy == DeflateStrategy.Filtered || (this.matchLen == DeflaterConstants.MIN_MATCH && this.strstart - this.matchStart > TooFar))) { this.matchLen = DeflaterConstants.MIN_MATCH - 1; } @@ -835,15 +803,6 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib // previous match was better if ((prevLen >= DeflaterConstants.MIN_MATCH) && (this.matchLen <= prevLen)) { -#if DebugDeflation - if (DeflaterConstants.DEBUGGING) - { - for (int i = 0 ; i < matchLen; i++) { - if (window[strstart-1+i] != window[prevMatch + i]) - throw new ImageFormatException(); - } - } -#endif this.huffman.TallyDist(this.strstart - 1 - prevMatch, prevLen); prevLen -= 2; do @@ -866,7 +825,7 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib { if (this.prevAvailable) { - this.huffman.TallyLit(this.window[this.strstart - 1] & 0xff); + this.huffman.TallyLit(this.pinnedWindowPointer[this.strstart - 1] & 0xff); } this.prevAvailable = true; @@ -891,5 +850,19 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib return true; } + + private void Dispose(bool disposing) + { + if (!this.isDisposed) + { + if (disposing) + { + this.windowBufferHandle.Dispose(); + this.windowBuffer.Dispose(); + } + + this.isDisposed = true; + } + } } } From bac80eedcc5b17fbe568397ab89ec539d5dfa1de Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Tue, 12 Nov 2019 23:28:01 +1100 Subject: [PATCH 60/75] Optimize DeflaterEngine --- .../Formats/Png/Zlib/DeflateThrowHelper.cs | 11 ++ src/ImageSharp/Formats/Png/Zlib/Deflater.cs | 2 +- .../Formats/Png/Zlib/DeflaterEngine.cs | 133 ++++++++---------- .../Formats/Png/Zlib/DeflaterOutputStream.cs | 21 --- 4 files changed, 70 insertions(+), 97 deletions(-) diff --git a/src/ImageSharp/Formats/Png/Zlib/DeflateThrowHelper.cs b/src/ImageSharp/Formats/Png/Zlib/DeflateThrowHelper.cs index a360e5e87c..2698c9b99b 100644 --- a/src/ImageSharp/Formats/Png/Zlib/DeflateThrowHelper.cs +++ b/src/ImageSharp/Formats/Png/Zlib/DeflateThrowHelper.cs @@ -13,5 +13,16 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib [MethodImpl(InliningOptions.ColdPath)] public static void ThrowAlreadyClosed() => throw new InvalidOperationException("Deflator already closed."); + + [MethodImpl(InliningOptions.ColdPath)] + public static void ThrowUnknownCompression() => throw new InvalidOperationException("Unknown compression function."); + + public static void ThrowNotProcessed() => throw new InvalidOperationException("Old input was not completely processed."); + + [MethodImpl(InliningOptions.ColdPath)] + public static void ThrowNull(string name) => throw new ArgumentNullException(name); + + [MethodImpl(InliningOptions.ColdPath)] + public static void ThrowOutOfRange(string name) => throw new ArgumentOutOfRangeException(name); } } diff --git a/src/ImageSharp/Formats/Png/Zlib/Deflater.cs b/src/ImageSharp/Formats/Png/Zlib/Deflater.cs index 5732f82d22..ef1d0e116d 100644 --- a/src/ImageSharp/Formats/Png/Zlib/Deflater.cs +++ b/src/ImageSharp/Formats/Png/Zlib/Deflater.cs @@ -295,8 +295,8 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib { if (disposing) { - this.pending.Dispose(); this.engine.Dispose(); + this.pending.Dispose(); } this.pending = null; diff --git a/src/ImageSharp/Formats/Png/Zlib/DeflaterEngine.cs b/src/ImageSharp/Formats/Png/Zlib/DeflaterEngine.cs index 0870f629c5..4db87c57e0 100644 --- a/src/ImageSharp/Formats/Png/Zlib/DeflaterEngine.cs +++ b/src/ImageSharp/Formats/Png/Zlib/DeflaterEngine.cs @@ -99,8 +99,8 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib /// private int inputEnd; - private DeflateStrategy strategy; - private DeflaterPendingBuffer pending; + private readonly DeflateStrategy strategy; + private readonly DeflaterPendingBuffer pending; private DeflaterHuffman huffman; private bool isDisposed; @@ -110,7 +110,10 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib /// Note that the array should really be unsigned short, so you need /// to and the values with 0xFFFF. ///
- private short[] head; + private readonly IMemoryOwner headBuffer; + private MemoryHandle headBufferHandle; + private readonly Memory head; + private readonly short* pinnedHeadPointer; /// /// prev[index & WMASK] points to the previous index that has the @@ -119,7 +122,10 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib /// Note that the array should really be unsigned short, so you need /// to and the values with 0xFFFF. /// - private short[] prev; + private readonly IMemoryOwner prevBuffer; + private MemoryHandle prevBufferHandle; + private readonly Memory prev; + private readonly short* pinnedPrevPointer; /// /// This array contains the part of the uncompressed stream that @@ -127,7 +133,7 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib /// private readonly IManagedByteBuffer windowBuffer; private MemoryHandle windowBufferHandle; - private byte[] window; + private readonly byte[] window; private readonly byte* pinnedWindowPointer; private int maxChain; @@ -147,13 +153,22 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib this.huffman = new DeflaterHuffman(pending); this.strategy = strategy; - this.windowBuffer = memoryAllocator.AllocateManagedByteBuffer(2 * DeflaterConstants.WSIZE); + // Create pinned pointers to the various buffers to allow indexing + // without bounds checks. + this.windowBuffer = memoryAllocator.AllocateManagedByteBuffer(2 * DeflaterConstants.WSIZE, AllocationOptions.Clean); this.window = this.windowBuffer.Array; this.windowBufferHandle = this.windowBuffer.Memory.Pin(); this.pinnedWindowPointer = (byte*)this.windowBufferHandle.Pointer; - this.head = new short[DeflaterConstants.HASH_SIZE]; - this.prev = new short[DeflaterConstants.WSIZE]; + this.headBuffer = memoryAllocator.Allocate(DeflaterConstants.HASH_SIZE, AllocationOptions.Clean); + this.head = this.headBuffer.Memory; + this.headBufferHandle = this.headBuffer.Memory.Pin(); + this.pinnedHeadPointer = (short*)this.headBufferHandle.Pointer; + + this.prevBuffer = memoryAllocator.Allocate(DeflaterConstants.WSIZE, AllocationOptions.Clean); + this.prev = this.prevBuffer.Memory; + this.prevBufferHandle = this.prevBuffer.Memory.Pin(); + this.pinnedPrevPointer = (short*)this.prevBufferHandle.Pointer; // We start at index 1, to avoid an implementation deficiency, that // we cannot build a repeat pattern at index 0. @@ -168,7 +183,7 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib /// Returns true if progress has been made. public bool Deflate(bool flush, bool finish) { - bool progress; + bool progress = false; do { this.FillWindow(); @@ -189,7 +204,8 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib break; default: - throw new InvalidOperationException("unknown compressionFunction"); + DeflateThrowHelper.ThrowUnknownCompression(); + break; } } while (this.pending.IsFlushed && progress); // repeat while we have no pending output and progress was made @@ -197,7 +213,7 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib } /// - /// Sets input data to be deflated. Should only be called when NeedsInput() + /// Sets input data to be deflated. Should only be called when /// returns true /// /// The buffer containing input data. @@ -205,33 +221,33 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib /// The number of bytes of data to use as input. public void SetInput(byte[] buffer, int offset, int count) { - if (buffer == null) + if (buffer is null) { - throw new ArgumentNullException(nameof(buffer)); + DeflateThrowHelper.ThrowNull(nameof(buffer)); } if (offset < 0) { - throw new ArgumentOutOfRangeException(nameof(offset)); + DeflateThrowHelper.ThrowOutOfRange(nameof(offset)); } if (count < 0) { - throw new ArgumentOutOfRangeException(nameof(count)); + DeflateThrowHelper.ThrowOutOfRange(nameof(count)); } if (this.inputOff < this.inputEnd) { - throw new InvalidOperationException("Old input was not completely processed."); + DeflateThrowHelper.ThrowNotProcessed(); } int end = offset + count; - // We want to throw an ArrayIndexOutOfBoundsException early. + // We want to throw an ArgumentOutOfRangeException early. // The check is very tricky: it also handles integer wrap around. if ((offset > end) || (end > buffer.Length)) { - throw new ArgumentOutOfRangeException(nameof(count)); + DeflateThrowHelper.ThrowOutOfRange(nameof(count)); } this.inputBuf = buffer; @@ -243,47 +259,13 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib /// Determines if more input is needed. ///
/// Return true if input is needed via SetInput - public bool NeedsInput() - { - return this.inputEnd == this.inputOff; - } - - /// - /// Set compression dictionary - /// - /// The buffer containing the dictionary data - /// The offset in the buffer for the first byte of data - /// The length of the dictionary data. - public void SetDictionary(byte[] buffer, int offset, int length) - { - if (length < DeflaterConstants.MIN_MATCH) - { - return; - } - - if (length > DeflaterConstants.MAX_DIST) - { - offset += length - DeflaterConstants.MAX_DIST; - length = DeflaterConstants.MAX_DIST; - } - - System.Array.Copy(buffer, offset, this.window, this.strstart, length); - - this.UpdateHash(); - --length; - while (--length > 0) - { - this.InsertString(); - this.strstart++; - } - - this.strstart += 2; - this.blockStart = this.strstart; - } + [MethodImpl(InliningOptions.ShortMethod)] + public bool NeedsInput() => this.inputEnd == this.inputOff; /// /// Reset internal state /// + [MethodImpl(InliningOptions.ShortMethod)] public void Reset() { this.huffman.Reset(); @@ -291,16 +273,8 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib this.lookahead = 0; this.prevAvailable = false; this.matchLen = DeflaterConstants.MIN_MATCH - 1; - - for (int i = 0; i < DeflaterConstants.HASH_SIZE; i++) - { - this.head[i] = 0; - } - - for (int i = 0; i < DeflaterConstants.WSIZE; i++) - { - this.prev[i] = 0; - } + this.head.Span.Slice(0, DeflaterConstants.HASH_SIZE).Clear(); + this.prev.Span.Slice(0, DeflaterConstants.WSIZE).Clear(); } /// @@ -311,7 +285,7 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib { if ((level < 0) || (level > 9)) { - throw new ArgumentOutOfRangeException(nameof(level)); + DeflateThrowHelper.ThrowOutOfRange(nameof(level)); } this.goodLength = DeflaterConstants.GOOD_LENGTH[level]; @@ -417,13 +391,15 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib /// value for this hash. /// /// The previous hash value + [MethodImpl(InliningOptions.ShortMethod)] private int InsertString() { short match; int hash = ((this.insertHashIndex << DeflaterConstants.HASH_SHIFT) ^ this.pinnedWindowPointer[this.strstart + (DeflaterConstants.MIN_MATCH - 1)]) & DeflaterConstants.HASH_MASK; - this.prev[this.strstart & DeflaterConstants.WMASK] = match = this.head[hash]; - this.head[hash] = unchecked((short)this.strstart); + short* pinnedHead = this.pinnedHeadPointer; + this.pinnedPrevPointer[this.strstart & DeflaterConstants.WMASK] = match = pinnedHead[hash]; + pinnedHead[hash] = unchecked((short)this.strstart); this.insertHashIndex = hash; return match & 0xFFFF; } @@ -437,17 +413,19 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib // Slide the hash table (could be avoided with 32 bit values // at the expense of memory usage). + short* pinnedHead = this.pinnedHeadPointer; for (int i = 0; i < DeflaterConstants.HASH_SIZE; ++i) { - int m = this.head[i] & 0xFFFF; - this.head[i] = (short)(m >= DeflaterConstants.WSIZE ? (m - DeflaterConstants.WSIZE) : 0); + int m = pinnedHead[i] & 0xFFFF; + pinnedHead[i] = (short)(m >= DeflaterConstants.WSIZE ? (m - DeflaterConstants.WSIZE) : 0); } // Slide the prev table. + short* pinnedPrev = this.pinnedPrevPointer; for (int i = 0; i < DeflaterConstants.WSIZE; i++) { - int m = this.prev[i] & 0xFFFF; - this.prev[i] = (short)(m >= DeflaterConstants.WSIZE ? (m - DeflaterConstants.WSIZE) : 0); + int m = pinnedPrev[i] & 0xFFFF; + pinnedPrev[i] = (short)(m >= DeflaterConstants.WSIZE ? (m - DeflaterConstants.WSIZE) : 0); } } @@ -473,8 +451,6 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib int scanMax = scan + Math.Min(DeflaterConstants.MAX_MATCH, this.lookahead) - 1; int limit = Math.Max(scan - DeflaterConstants.MAX_DIST, 0); - byte[] window = this.window; - short[] prev = this.prev; int chainLength = this.maxChain; int niceLength = Math.Min(this.niceLength, this.lookahead); @@ -495,6 +471,7 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib chainLength >>= 2; } + short* pinnedPrev = this.pinnedPrevPointer; do { match = curMatch; @@ -631,7 +608,7 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib scan_end = pinnedWindow[scan]; } } - while ((curMatch = prev[curMatch & DeflaterConstants.WMASK] & 0xFFFF) > limit && --chainLength != 0); + while ((curMatch = pinnedPrev[curMatch & DeflaterConstants.WMASK] & 0xFFFF) > limit && --chainLength != 0); return this.matchLen >= DeflaterConstants.MIN_MATCH; } @@ -859,6 +836,12 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib { this.windowBufferHandle.Dispose(); this.windowBuffer.Dispose(); + + this.headBufferHandle.Dispose(); + this.headBuffer.Dispose(); + + this.prevBufferHandle.Dispose(); + this.prevBuffer.Dispose(); } this.isDisposed = true; diff --git a/src/ImageSharp/Formats/Png/Zlib/DeflaterOutputStream.cs b/src/ImageSharp/Formats/Png/Zlib/DeflaterOutputStream.cs index ac5f229aaa..837e8b7958 100644 --- a/src/ImageSharp/Formats/Png/Zlib/DeflaterOutputStream.cs +++ b/src/ImageSharp/Formats/Png/Zlib/DeflaterOutputStream.cs @@ -97,11 +97,6 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib break; } - //if (cryptoTransform_ != null) - //{ - // EncryptBlock(buffer_, 0, len); - //} - baseOutputStream_.Write(buffer_, 0, len); } @@ -111,16 +106,6 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib } baseOutputStream_.Flush(); - - //if (cryptoTransform_ != null) - //{ - // if (cryptoTransform_ is ZipAESTransform) - // { - // AESAuthCode = ((ZipAESTransform)cryptoTransform_).GetAuthCode(); - // } - // cryptoTransform_.Dispose(); - // cryptoTransform_ = null; - //} } /// @@ -394,12 +379,6 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib try { Finish(); - //if (cryptoTransform_ != null) - //{ - // GetAuthCodeIfAES(); - // cryptoTransform_.Dispose(); - // cryptoTransform_ = null; - //} } finally { From 11cc8121fa8c66ce81e2d824eadc8db04fcf9e7f Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Tue, 12 Nov 2019 19:22:07 +0100 Subject: [PATCH 61/75] Update external for issue 984 testimages --- tests/Images/External | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Images/External b/tests/Images/External index 563ec6f777..ca4cf8318f 160000 --- a/tests/Images/External +++ b/tests/Images/External @@ -1 +1 @@ -Subproject commit 563ec6f7774734ba39924174c8961705a1ea6fa2 +Subproject commit ca4cf8318fe4d09f0fc825686dcd477ebfb5e3e5 From 50bacca782af71e666628b346bb596a524de23b4 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Tue, 12 Nov 2019 20:27:54 +0100 Subject: [PATCH 62/75] Add GetBT709Luminance with vector test --- .../Helpers/ImageMathsTests.cs | 24 ++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/tests/ImageSharp.Tests/Helpers/ImageMathsTests.cs b/tests/ImageSharp.Tests/Helpers/ImageMathsTests.cs index 018fabd982..817672f34a 100644 --- a/tests/ImageSharp.Tests/Helpers/ImageMathsTests.cs +++ b/tests/ImageSharp.Tests/Helpers/ImageMathsTests.cs @@ -2,6 +2,11 @@ // Licensed under the Apache License, Version 2.0. using System; +using System.Numerics; + +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.Memory; + using Xunit; namespace SixLabors.ImageSharp.Tests.Helpers @@ -131,6 +136,23 @@ namespace SixLabors.ImageSharp.Tests.Helpers Assert.Equal(expected, actual); } + [Theory] + [InlineData(0.2f, 0.7f, 0.1f, 256, 140)] + [InlineData(0.5f, 0.5f, 0.5f, 256, 128)] + [InlineData(0.5f, 0.5f, 0.5f, 65536, 32768)] + [InlineData(0.2f, 0.7f, 0.1f, 65536, 36069)] + public void GetBT709Luminance_WithVector4(float x, float y, float z, int luminanceLevels, int expected) + { + // arrange + var vector = new Vector4(x, y, z, 0.0f); + + // act + int actual = ImageMaths.GetBT709Luminance(ref vector, luminanceLevels); + + // assert + Assert.Equal(expected, actual); + } + // TODO: We need to test all ImageMaths methods! } -} \ No newline at end of file +} From dd8ac6006e0fe4d6144833e77d02ba39a953daa9 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Wed, 13 Nov 2019 13:25:59 +1100 Subject: [PATCH 63/75] No need to clean the arrays. We write over everything. --- src/ImageSharp/Formats/Png/Zlib/DeflaterEngine.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/ImageSharp/Formats/Png/Zlib/DeflaterEngine.cs b/src/ImageSharp/Formats/Png/Zlib/DeflaterEngine.cs index 4db87c57e0..b146825015 100644 --- a/src/ImageSharp/Formats/Png/Zlib/DeflaterEngine.cs +++ b/src/ImageSharp/Formats/Png/Zlib/DeflaterEngine.cs @@ -155,17 +155,17 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib // Create pinned pointers to the various buffers to allow indexing // without bounds checks. - this.windowBuffer = memoryAllocator.AllocateManagedByteBuffer(2 * DeflaterConstants.WSIZE, AllocationOptions.Clean); + this.windowBuffer = memoryAllocator.AllocateManagedByteBuffer(2 * DeflaterConstants.WSIZE); this.window = this.windowBuffer.Array; this.windowBufferHandle = this.windowBuffer.Memory.Pin(); this.pinnedWindowPointer = (byte*)this.windowBufferHandle.Pointer; - this.headBuffer = memoryAllocator.Allocate(DeflaterConstants.HASH_SIZE, AllocationOptions.Clean); + this.headBuffer = memoryAllocator.Allocate(DeflaterConstants.HASH_SIZE); this.head = this.headBuffer.Memory; this.headBufferHandle = this.headBuffer.Memory.Pin(); this.pinnedHeadPointer = (short*)this.headBufferHandle.Pointer; - this.prevBuffer = memoryAllocator.Allocate(DeflaterConstants.WSIZE, AllocationOptions.Clean); + this.prevBuffer = memoryAllocator.Allocate(DeflaterConstants.WSIZE); this.prev = this.prevBuffer.Memory; this.prevBufferHandle = this.prevBuffer.Memory.Pin(); this.pinnedPrevPointer = (short*)this.prevBufferHandle.Pointer; From 62507f0ff9eb10de7dfba249a94df5feeb0d2bce Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Wed, 13 Nov 2019 15:35:38 +1100 Subject: [PATCH 64/75] Appease StyleCop --- .../Formats/Png/Zlib/DeflaterHuffman.cs | 1022 ++++++++--------- 1 file changed, 458 insertions(+), 564 deletions(-) diff --git a/src/ImageSharp/Formats/Png/Zlib/DeflaterHuffman.cs b/src/ImageSharp/Formats/Png/Zlib/DeflaterHuffman.cs index 3d2856b0e8..bc7ad7a38b 100644 --- a/src/ImageSharp/Formats/Png/Zlib/DeflaterHuffman.cs +++ b/src/ImageSharp/Formats/Png/Zlib/DeflaterHuffman.cs @@ -1,7 +1,6 @@ // Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. -// using System; using System.Collections.Generic; using System.Text; @@ -18,106 +17,409 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib /// public class DeflaterHuffman { - private const int BUFSIZE = 1 << (DeflaterConstants.DEFAULT_MEM_LEVEL + 6); - private const int LITERAL_NUM = 286; + private const int BufferSize = 1 << (DeflaterConstants.DEFAULT_MEM_LEVEL + 6); + + // The number of literal codes. + private const int LiteralNumber = 286; // Number of distance codes - private const int DIST_NUM = 30; + private const int DistanceNumber = 30; // Number of codes used to transfer bit lengths - private const int BITLEN_NUM = 19; + private const int BitLengthNumber = 19; // repeat previous bit length 3-6 times (2 bits of repeat count) - private const int REP_3_6 = 16; + private const int Repeat3To6 = 16; // repeat a zero length 3-10 times (3 bits of repeat count) - private const int REP_3_10 = 17; + private const int Repeat3To10 = 17; // repeat a zero length 11-138 times (7 bits of repeat count) - private const int REP_11_138 = 18; + private const int Repeat11To138 = 18; - private const int EOF_SYMBOL = 256; + private const int EofSymbol = 256; // The lengths of the bit length codes are sent in order of decreasing // probability, to avoid transmitting the lengths for unused bit length codes. - private static readonly int[] BL_ORDER = { 16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15 }; - - private static readonly byte[] bit4Reverse = { - 0, - 8, - 4, - 12, - 2, - 10, - 6, - 14, - 1, - 9, - 5, - 13, - 3, - 11, - 7, - 15 - }; + private static readonly int[] BitLengthOrder = { 16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15 }; + + private static readonly byte[] Bit4Reverse = { 0, 8, 4, 12, 2, 10, 6, 14, 1, 9, 5, 13, 3, 11, 7, 15 }; private static short[] staticLCodes; private static byte[] staticLLength; private static short[] staticDCodes; private static byte[] staticDLength; - private class Tree + private Tree literalTree; + private Tree distTree; + private Tree blTree; + + // Buffer for distances + private short[] distanceBuffer; + + private byte[] literalBuffer; + private int lastLiteral; + private int extraBits; + + static DeflaterHuffman() + { + // See RFC 1951 3.2.6 + // Literal codes + staticLCodes = new short[LiteralNumber]; + staticLLength = new byte[LiteralNumber]; + + int i = 0; + while (i < 144) + { + staticLCodes[i] = BitReverse((0x030 + i) << 8); + staticLLength[i++] = 8; + } + + while (i < 256) + { + staticLCodes[i] = BitReverse((0x190 - 144 + i) << 7); + staticLLength[i++] = 9; + } + + while (i < 280) + { + staticLCodes[i] = BitReverse((0x000 - 256 + i) << 9); + staticLLength[i++] = 7; + } + + while (i < LiteralNumber) + { + staticLCodes[i] = BitReverse((0x0c0 - 280 + i) << 8); + staticLLength[i++] = 8; + } + + // Distance codes + staticDCodes = new short[DistanceNumber]; + staticDLength = new byte[DistanceNumber]; + for (i = 0; i < DistanceNumber; i++) + { + staticDCodes[i] = BitReverse(i << 11); + staticDLength[i] = 5; + } + } + + /// + /// Initializes a new instance of the class. + /// + /// Pending buffer to use + public DeflaterHuffman(DeflaterPendingBuffer pending) { - #region Instance Fields + this.Pending = pending; - public short[] freqs; + this.literalTree = new Tree(this, LiteralNumber, 257, 15); + this.distTree = new Tree(this, DistanceNumber, 1, 15); + this.blTree = new Tree(this, BitLengthNumber, 4, 7); - public byte[] length; + this.distanceBuffer = new short[BufferSize]; + this.literalBuffer = new byte[BufferSize]; + } - public int minNumCodes; + /// + /// Gets the pending buffer to use. + /// + public DeflaterPendingBuffer Pending { get; } - public int numCodes; + /// + /// Reset internal state + /// + public void Reset() + { + this.lastLiteral = 0; + this.extraBits = 0; + this.literalTree.Reset(); + this.distTree.Reset(); + this.blTree.Reset(); + } - private short[] codes; - private readonly int[] bl_counts; - private readonly int maxLength; - private DeflaterHuffman dh; + /// + /// Write all trees to pending buffer + /// + /// The number/rank of treecodes to send. + public void SendAllTrees(int blTreeCodes) + { + this.blTree.BuildCodes(); + this.literalTree.BuildCodes(); + this.distTree.BuildCodes(); + this.Pending.WriteBits(this.literalTree.NumCodes - 257, 5); + this.Pending.WriteBits(this.distTree.NumCodes - 1, 5); + this.Pending.WriteBits(blTreeCodes - 4, 4); + for (int rank = 0; rank < blTreeCodes; rank++) + { + this.Pending.WriteBits(this.blTree.Length[BitLengthOrder[rank]], 3); + } + + this.literalTree.WriteTree(this.blTree); + this.distTree.WriteTree(this.blTree); + } + + /// + /// Compress current buffer writing data to pending buffer + /// + public void CompressBlock() + { + for (int i = 0; i < this.lastLiteral; i++) + { + int litlen = this.literalBuffer[i] & 0xff; + int dist = this.distanceBuffer[i]; + if (dist-- != 0) + { + int lc = Lcode(litlen); + this.literalTree.WriteSymbol(lc); + + int bits = (lc - 261) / 4; + if (bits > 0 && bits <= 5) + { + this.Pending.WriteBits(litlen & ((1 << bits) - 1), bits); + } + + int dc = Dcode(dist); + this.distTree.WriteSymbol(dc); + + bits = (dc / 2) - 1; + if (bits > 0) + { + this.Pending.WriteBits(dist & ((1 << bits) - 1), bits); + } + } + else + { + this.literalTree.WriteSymbol(litlen); + } + } + + this.literalTree.WriteSymbol(EofSymbol); + } + + /// + /// Flush block to output with no compression + /// + /// Data to write + /// Index of first byte to write + /// Count of bytes to write + /// True if this is the last block + public void FlushStoredBlock(byte[] stored, int storedOffset, int storedLength, bool lastBlock) + { + this.Pending.WriteBits((DeflaterConstants.STORED_BLOCK << 1) + (lastBlock ? 1 : 0), 3); + this.Pending.AlignToByte(); + this.Pending.WriteShort(storedLength); + this.Pending.WriteShort(~storedLength); + this.Pending.WriteBlock(stored, storedOffset, storedLength); + this.Reset(); + } + + /// + /// Flush block to output with compression + /// + /// Data to flush + /// Index of first byte to flush + /// Count of bytes to flush + /// True if this is the last block + public void FlushBlock(byte[] stored, int storedOffset, int storedLength, bool lastBlock) + { + this.literalTree.Freqs[EofSymbol]++; + + // Build trees + this.literalTree.BuildTree(); + this.distTree.BuildTree(); + + // Calculate bitlen frequency + this.literalTree.CalcBLFreq(this.blTree); + this.distTree.CalcBLFreq(this.blTree); + + // Build bitlen tree + this.blTree.BuildTree(); + + int blTreeCodes = 4; + for (int i = 18; i > blTreeCodes; i--) + { + if (this.blTree.Length[BitLengthOrder[i]] > 0) + { + blTreeCodes = i + 1; + } + } + + int opt_len = 14 + (blTreeCodes * 3) + this.blTree.GetEncodedLength() + + this.literalTree.GetEncodedLength() + this.distTree.GetEncodedLength() + + this.extraBits; + + int static_len = this.extraBits; + for (int i = 0; i < LiteralNumber; i++) + { + static_len += this.literalTree.Freqs[i] * staticLLength[i]; + } + + for (int i = 0; i < DistanceNumber; i++) + { + static_len += this.distTree.Freqs[i] * staticDLength[i]; + } + + if (opt_len >= static_len) + { + // Force static trees + opt_len = static_len; + } + + if (storedOffset >= 0 && storedLength + 4 < opt_len >> 3) + { + // Store Block + this.FlushStoredBlock(stored, storedOffset, storedLength, lastBlock); + } + else if (opt_len == static_len) + { + // Encode with static tree + this.Pending.WriteBits((DeflaterConstants.STATIC_TREES << 1) + (lastBlock ? 1 : 0), 3); + this.literalTree.SetStaticCodes(staticLCodes, staticLLength); + this.distTree.SetStaticCodes(staticDCodes, staticDLength); + this.CompressBlock(); + this.Reset(); + } + else + { + // Encode with dynamic tree + this.Pending.WriteBits((DeflaterConstants.DYN_TREES << 1) + (lastBlock ? 1 : 0), 3); + this.SendAllTrees(blTreeCodes); + this.CompressBlock(); + this.Reset(); + } + } + + /// + /// Get value indicating if internal buffer is full + /// + /// true if buffer is full + public bool IsFull() + { + return this.lastLiteral >= BufferSize; + } + + /// + /// Add literal to buffer + /// + /// Literal value to add to buffer. + /// Value indicating internal buffer is full + public bool TallyLit(int literal) + { + this.distanceBuffer[this.lastLiteral] = 0; + this.literalBuffer[this.lastLiteral++] = (byte)literal; + this.literalTree.Freqs[literal]++; + return this.IsFull(); + } + + /// + /// Add distance code and length to literal and distance trees + /// + /// Distance code + /// Length + /// Value indicating if internal buffer is full + public bool TallyDist(int distance, int length) + { + this.distanceBuffer[this.lastLiteral] = (short)distance; + this.literalBuffer[this.lastLiteral++] = (byte)(length - 3); + + int lc = Lcode(length - 3); + this.literalTree.Freqs[lc]++; + if (lc >= 265 && lc < 285) + { + this.extraBits += (lc - 261) / 4; + } + + int dc = Dcode(distance - 1); + this.distTree.Freqs[dc]++; + if (dc >= 4) + { + this.extraBits += (dc / 2) - 1; + } + + return this.IsFull(); + } + + /// + /// Reverse the bits of a 16 bit value. + /// + /// Value to reverse bits + /// Value with bits reversed + public static short BitReverse(int toReverse) + { + return (short)(Bit4Reverse[toReverse & 0xF] << 12 | + Bit4Reverse[(toReverse >> 4) & 0xF] << 8 | + Bit4Reverse[(toReverse >> 8) & 0xF] << 4 | + Bit4Reverse[toReverse >> 12]); + } + + private static int Lcode(int length) + { + if (length == 255) + { + return 285; + } + + int code = 257; + while (length >= 8) + { + code += 4; + length >>= 1; + } - #endregion Instance Fields + return code + length; + } - #region Constructors + private static int Dcode(int distance) + { + int code = 0; + while (distance >= 4) + { + code += 2; + distance >>= 1; + } + + return code + distance; + } + + private class Tree + { + private readonly int minNumCodes; + private short[] codes; + private readonly int[] bitLengthCounts; + private readonly int maxLength; + private readonly DeflaterHuffman dh; public Tree(DeflaterHuffman dh, int elems, int minCodes, int maxLength) { this.dh = dh; this.minNumCodes = minCodes; this.maxLength = maxLength; - freqs = new short[elems]; - bl_counts = new int[maxLength]; + this.Freqs = new short[elems]; + this.bitLengthCounts = new int[maxLength]; } - #endregion Constructors + public int NumCodes { get; private set; } + + public short[] Freqs { get; } + + public byte[] Length { get; set; } /// /// Resets the internal state of the tree /// public void Reset() { - for (int i = 0; i < freqs.Length; i++) + for (int i = 0; i < this.Freqs.Length; i++) { - freqs[i] = 0; + this.Freqs[i] = 0; } - codes = null; - length = null; + + this.codes = null; + this.Length = null; } public void WriteSymbol(int code) { - // if (DeflaterConstants.DEBUGGING) { - // freqs[code]--; - // // Console.Write("writeSymbol("+freqs.length+","+code+"): "); - // } - dh.pending.WriteBits(codes[code] & 0xffff, length[code]); + this.dh.Pending.WriteBits(this.codes[code] & 0xffff, this.Length[code]); } /// @@ -129,9 +431,9 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib public void CheckEmpty() { bool empty = true; - for (int i = 0; i < freqs.Length; i++) + for (int i = 0; i < this.Freqs.Length; i++) { - empty &= freqs[i] == 0; + empty &= this.Freqs[i] == 0; } if (!empty) @@ -147,8 +449,8 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib /// length for new codes public void SetStaticCodes(short[] staticCodes, byte[] staticLengths) { - codes = staticCodes; - length = staticLengths; + this.codes = staticCodes; + this.Length = staticLengths; } /// @@ -156,44 +458,24 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib /// public void BuildCodes() { - int numSymbols = freqs.Length; - int[] nextCode = new int[maxLength]; + int numSymbols = this.Freqs.Length; + int[] nextCode = new int[this.maxLength]; int code = 0; - codes = new short[freqs.Length]; - - // if (DeflaterConstants.DEBUGGING) { - // //Console.WriteLine("buildCodes: "+freqs.Length); - // } + this.codes = new short[this.Freqs.Length]; - for (int bits = 0; bits < maxLength; bits++) + for (int bits = 0; bits < this.maxLength; bits++) { nextCode[bits] = code; - code += bl_counts[bits] << (15 - bits); - - // if (DeflaterConstants.DEBUGGING) { - // //Console.WriteLine("bits: " + ( bits + 1) + " count: " + bl_counts[bits] - // +" nextCode: "+code); - // } + code += this.bitLengthCounts[bits] << (15 - bits); } -#if DebugDeflation - if ( DeflaterConstants.DEBUGGING && (code != 65536) ) - { - throw new ImageFormatException("Inconsistent bl_counts!"); - } -#endif - for (int i = 0; i < numCodes; i++) + for (int i = 0; i < this.NumCodes; i++) { - int bits = length[i]; + int bits = this.Length[i]; if (bits > 0) { - // if (DeflaterConstants.DEBUGGING) { - // //Console.WriteLine("codes["+i+"] = rev(" + nextCode[bits-1]+"), - // +bits); - // } - - codes[i] = BitReverse(nextCode[bits - 1]); + this.codes[i] = BitReverse(nextCode[bits - 1]); nextCode[bits - 1] += 1 << (16 - bits); } } @@ -201,67 +483,65 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib public void BuildTree() { - int numSymbols = freqs.Length; - - /* heap is a priority queue, sorted by frequency, least frequent - * nodes first. The heap is a binary tree, with the property, that - * the parent node is smaller than both child nodes. This assures - * that the smallest node is the first parent. - * - * The binary tree is encoded in an array: 0 is root node and - * the nodes 2*n+1, 2*n+2 are the child nodes of node n. - */ + int numSymbols = this.Freqs.Length; + + // heap is a priority queue, sorted by frequency, least frequent + // nodes first. The heap is a binary tree, with the property, that + // the parent node is smaller than both child nodes. This assures + // that the smallest node is the first parent. + // + // The binary tree is encoded in an array: 0 is root node and + // the nodes 2*n+1, 2*n+2 are the child nodes of node n. int[] heap = new int[numSymbols]; int heapLen = 0; int maxCode = 0; for (int n = 0; n < numSymbols; n++) { - int freq = freqs[n]; + int freq = this.Freqs[n]; if (freq != 0) { // Insert n into heap int pos = heapLen++; int ppos; - while (pos > 0 && freqs[heap[ppos = (pos - 1) / 2]] > freq) + while (pos > 0 && this.Freqs[heap[ppos = (pos - 1) / 2]] > freq) { heap[pos] = heap[ppos]; pos = ppos; } + heap[pos] = n; maxCode = n; } } - /* We could encode a single literal with 0 bits but then we - * don't see the literals. Therefore we force at least two - * literals to avoid this case. We don't care about order in - * this case, both literals get a 1 bit code. - */ + // We could encode a single literal with 0 bits but then we + // don't see the literals. Therefore we force at least two + // literals to avoid this case. We don't care about order in + // this case, both literals get a 1 bit code. while (heapLen < 2) { int node = maxCode < 2 ? ++maxCode : 0; heap[heapLen++] = node; } - numCodes = Math.Max(maxCode + 1, minNumCodes); + this.NumCodes = Math.Max(maxCode + 1, this.minNumCodes); int numLeafs = heapLen; - int[] childs = new int[4 * heapLen - 2]; - int[] values = new int[2 * heapLen - 1]; + int[] childs = new int[(4 * heapLen) - 2]; + int[] values = new int[(2 * heapLen) - 1]; int numNodes = numLeafs; for (int i = 0; i < heapLen; i++) { int node = heap[i]; childs[2 * i] = node; - childs[2 * i + 1] = -1; - values[i] = freqs[node] << 8; + childs[(2 * i) + 1] = -1; + values[i] = this.Freqs[node] << 8; heap[i] = i; } - /* Construct the Huffman tree by repeatedly combining the least two - * frequent nodes. - */ + // Construct the Huffman tree by repeatedly combining the least two + // frequent nodes. do { int first = heap[0]; @@ -280,17 +560,17 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib heap[ppos] = heap[path]; ppos = path; - path = path * 2 + 1; + path = (path * 2) + 1; } - /* Now propagate the last element down along path. Normally - * it shouldn't go too deep. - */ + // Now propagate the last element down along path. Normally + // it shouldn't go too deep. int lastVal = values[last]; while ((path = ppos) > 0 && values[heap[ppos = (path - 1) / 2]] > lastVal) { heap[path] = heap[ppos]; } + heap[path] = last; int second = heap[0]; @@ -298,7 +578,7 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib // Create a new node father of first and second last = numNodes++; childs[2 * last] = first; - childs[2 * last + 1] = second; + childs[(2 * last) + 1] = second; int mindepth = Math.Min(values[first] & 0xff, values[second] & 0xff); values[last] = lastVal = values[first] + values[second] - mindepth + 1; @@ -315,7 +595,7 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib heap[ppos] = heap[path]; ppos = path; - path = ppos * 2 + 1; + path = (ppos * 2) + 1; } // Now propagate the new element down along path @@ -323,15 +603,17 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib { heap[path] = heap[ppos]; } + heap[path] = last; - } while (heapLen > 1); + } + while (heapLen > 1); - if (heap[0] != childs.Length / 2 - 1) + if (heap[0] != (childs.Length / 2) - 1) { throw new ImageFormatException("Heap invariant violated"); } - BuildLength(childs); + this.BuildLength(childs); } /// @@ -341,10 +623,11 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib public int GetEncodedLength() { int len = 0; - for (int i = 0; i < freqs.Length; i++) + for (int i = 0; i < this.Freqs.Length; i++) { - len += freqs[i] * length[i]; + len += this.Freqs[i] * this.Length[i]; } + return len; } @@ -360,10 +643,10 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib int curlen = -1; /* length of current code */ int i = 0; - while (i < numCodes) + while (i < this.NumCodes) { count = 1; - int nextlen = length[i]; + int nextlen = this.Length[i]; if (nextlen == 0) { max_count = 138; @@ -375,14 +658,15 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib min_count = 3; if (curlen != nextlen) { - blTree.freqs[nextlen]++; + blTree.Freqs[nextlen]++; count = 0; } } + curlen = nextlen; i++; - while (i < numCodes && curlen == length[i]) + while (i < this.NumCodes && curlen == this.Length[i]) { i++; if (++count >= max_count) @@ -393,19 +677,19 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib if (count < min_count) { - blTree.freqs[curlen] += (short)count; + blTree.Freqs[curlen] += (short)count; } else if (curlen != 0) { - blTree.freqs[REP_3_6]++; + blTree.Freqs[Repeat3To6]++; } else if (count <= 10) { - blTree.freqs[REP_3_10]++; + blTree.Freqs[Repeat3To10]++; } else { - blTree.freqs[REP_11_138]++; + blTree.Freqs[Repeat11To138]++; } } } @@ -422,10 +706,10 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib int curlen = -1; // length of current code int i = 0; - while (i < numCodes) + while (i < this.NumCodes) { count = 1; - int nextlen = length[i]; + int nextlen = this.Length[i]; if (nextlen == 0) { max_count = 138; @@ -441,10 +725,11 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib count = 0; } } + curlen = nextlen; i++; - while (i < numCodes && curlen == length[i]) + while (i < this.NumCodes && curlen == this.Length[i]) { i++; if (++count >= max_count) @@ -462,32 +747,32 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib } else if (curlen != 0) { - blTree.WriteSymbol(REP_3_6); - dh.pending.WriteBits(count - 3, 2); + blTree.WriteSymbol(Repeat3To6); + this.dh.Pending.WriteBits(count - 3, 2); } else if (count <= 10) { - blTree.WriteSymbol(REP_3_10); - dh.pending.WriteBits(count - 3, 3); + blTree.WriteSymbol(Repeat3To10); + this.dh.Pending.WriteBits(count - 3, 3); } else { - blTree.WriteSymbol(REP_11_138); - dh.pending.WriteBits(count - 11, 7); + blTree.WriteSymbol(Repeat11To138); + this.dh.Pending.WriteBits(count - 11, 7); } } } private void BuildLength(int[] childs) { - this.length = new byte[freqs.Length]; + this.Length = new byte[this.Freqs.Length]; int numNodes = childs.Length / 2; int numLeafs = (numNodes + 1) / 2; int overflow = 0; - for (int i = 0; i < maxLength; i++) + for (int i = 0; i < this.maxLength; i++) { - bl_counts[i] = 0; + this.bitLengthCounts[i] = 0; } // First calculate optimal bit lengths @@ -496,43 +781,36 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib for (int i = numNodes - 1; i >= 0; i--) { - if (childs[2 * i + 1] != -1) + if (childs[(2 * i) + 1] != -1) { int bitLength = lengths[i] + 1; - if (bitLength > maxLength) + if (bitLength > this.maxLength) { - bitLength = maxLength; + bitLength = this.maxLength; overflow++; } - lengths[childs[2 * i]] = lengths[childs[2 * i + 1]] = bitLength; + + lengths[childs[2 * i]] = lengths[childs[(2 * i) + 1]] = bitLength; } else { // A leaf node int bitLength = lengths[i]; - bl_counts[bitLength - 1]++; - this.length[childs[2 * i]] = (byte)lengths[i]; + this.bitLengthCounts[bitLength - 1]++; + this.Length[childs[2 * i]] = (byte)lengths[i]; } } - // if (DeflaterConstants.DEBUGGING) { - // //Console.WriteLine("Tree "+freqs.Length+" lengths:"); - // for (int i=0; i < numLeafs; i++) { - // //Console.WriteLine("Node "+childs[2*i]+" freq: "+freqs[childs[2*i]] - // + " len: "+length[childs[2*i]]); - // } - // } - if (overflow == 0) { return; } - int incrBitLen = maxLength - 1; + int incrBitLen = this.maxLength - 1; do { // Find the first bit length which could increase: - while (bl_counts[--incrBitLen] == 0) + while (this.bitLengthCounts[--incrBitLen] == 0) { } @@ -540,426 +818,42 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib // number of overflow nodes. do { - bl_counts[incrBitLen]--; - bl_counts[++incrBitLen]++; - overflow -= 1 << (maxLength - 1 - incrBitLen); - } while (overflow > 0 && incrBitLen < maxLength - 1); - } while (overflow > 0); - - /* We may have overshot above. Move some nodes from maxLength to - * maxLength-1 in that case. - */ - bl_counts[maxLength - 1] += overflow; - bl_counts[maxLength - 2] -= overflow; - - /* Now recompute all bit lengths, scanning in increasing - * frequency. It is simpler to reconstruct all lengths instead of - * fixing only the wrong ones. This idea is taken from 'ar' - * written by Haruhiko Okumura. - * - * The nodes were inserted with decreasing frequency into the childs - * array. - */ + this.bitLengthCounts[incrBitLen]--; + this.bitLengthCounts[++incrBitLen]++; + overflow -= 1 << (this.maxLength - 1 - incrBitLen); + } + while (overflow > 0 && incrBitLen < this.maxLength - 1); + } + while (overflow > 0); + + // We may have overshot above. Move some nodes from maxLength to + // maxLength-1 in that case. + this.bitLengthCounts[this.maxLength - 1] += overflow; + this.bitLengthCounts[this.maxLength - 2] -= overflow; + + // Now recompute all bit lengths, scanning in increasing + // frequency. It is simpler to reconstruct all lengths instead of + // fixing only the wrong ones. This idea is taken from 'ar' + // written by Haruhiko Okumura. + // + // The nodes were inserted with decreasing frequency into the childs + // array. int nodePtr = 2 * numLeafs; - for (int bits = maxLength; bits != 0; bits--) + for (int bits = this.maxLength; bits != 0; bits--) { - int n = bl_counts[bits - 1]; + int n = this.bitLengthCounts[bits - 1]; while (n > 0) { int childPtr = 2 * childs[nodePtr++]; if (childs[childPtr + 1] == -1) { // We found another leaf - length[childs[childPtr]] = (byte)bits; + this.Length[childs[childPtr]] = (byte)bits; n--; } } } - // if (DeflaterConstants.DEBUGGING) { - // //Console.WriteLine("*** After overflow elimination. ***"); - // for (int i=0; i < numLeafs; i++) { - // //Console.WriteLine("Node "+childs[2*i]+" freq: "+freqs[childs[2*i]] - // + " len: "+length[childs[2*i]]); - // } - // } - } - } - - #region Instance Fields - - /// - /// Pending buffer to use - /// - public DeflaterPendingBuffer pending; - - private Tree literalTree; - private Tree distTree; - private Tree blTree; - - // Buffer for distances - private short[] d_buf; - - private byte[] l_buf; - private int last_lit; - private int extra_bits; - - #endregion Instance Fields - - static DeflaterHuffman() - { - // See RFC 1951 3.2.6 - // Literal codes - staticLCodes = new short[LITERAL_NUM]; - staticLLength = new byte[LITERAL_NUM]; - - int i = 0; - while (i < 144) - { - staticLCodes[i] = BitReverse((0x030 + i) << 8); - staticLLength[i++] = 8; - } - - while (i < 256) - { - staticLCodes[i] = BitReverse((0x190 - 144 + i) << 7); - staticLLength[i++] = 9; - } - - while (i < 280) - { - staticLCodes[i] = BitReverse((0x000 - 256 + i) << 9); - staticLLength[i++] = 7; - } - - while (i < LITERAL_NUM) - { - staticLCodes[i] = BitReverse((0x0c0 - 280 + i) << 8); - staticLLength[i++] = 8; - } - - // Distance codes - staticDCodes = new short[DIST_NUM]; - staticDLength = new byte[DIST_NUM]; - for (i = 0; i < DIST_NUM; i++) - { - staticDCodes[i] = BitReverse(i << 11); - staticDLength[i] = 5; - } - } - - /// - /// Construct instance with pending buffer - /// - /// Pending buffer to use - public DeflaterHuffman(DeflaterPendingBuffer pending) - { - this.pending = pending; - - literalTree = new Tree(this, LITERAL_NUM, 257, 15); - distTree = new Tree(this, DIST_NUM, 1, 15); - blTree = new Tree(this, BITLEN_NUM, 4, 7); - - d_buf = new short[BUFSIZE]; - l_buf = new byte[BUFSIZE]; - } - - /// - /// Reset internal state - /// - public void Reset() - { - last_lit = 0; - extra_bits = 0; - literalTree.Reset(); - distTree.Reset(); - blTree.Reset(); - } - - /// - /// Write all trees to pending buffer - /// - /// The number/rank of treecodes to send. - public void SendAllTrees(int blTreeCodes) - { - blTree.BuildCodes(); - literalTree.BuildCodes(); - distTree.BuildCodes(); - pending.WriteBits(literalTree.numCodes - 257, 5); - pending.WriteBits(distTree.numCodes - 1, 5); - pending.WriteBits(blTreeCodes - 4, 4); - for (int rank = 0; rank < blTreeCodes; rank++) - { - pending.WriteBits(blTree.length[BL_ORDER[rank]], 3); - } - literalTree.WriteTree(blTree); - distTree.WriteTree(blTree); - -#if DebugDeflation - if (DeflaterConstants.DEBUGGING) { - blTree.CheckEmpty(); - } -#endif - } - - /// - /// Compress current buffer writing data to pending buffer - /// - public void CompressBlock() - { - for (int i = 0; i < last_lit; i++) - { - int litlen = l_buf[i] & 0xff; - int dist = d_buf[i]; - if (dist-- != 0) - { - // if (DeflaterConstants.DEBUGGING) { - // Console.Write("["+(dist+1)+","+(litlen+3)+"]: "); - // } - - int lc = Lcode(litlen); - literalTree.WriteSymbol(lc); - - int bits = (lc - 261) / 4; - if (bits > 0 && bits <= 5) - { - pending.WriteBits(litlen & ((1 << bits) - 1), bits); - } - - int dc = Dcode(dist); - distTree.WriteSymbol(dc); - - bits = dc / 2 - 1; - if (bits > 0) - { - pending.WriteBits(dist & ((1 << bits) - 1), bits); - } - } - else - { - // if (DeflaterConstants.DEBUGGING) { - // if (litlen > 32 && litlen < 127) { - // Console.Write("("+(char)litlen+"): "); - // } else { - // Console.Write("{"+litlen+"}: "); - // } - // } - literalTree.WriteSymbol(litlen); - } - } - -#if DebugDeflation - if (DeflaterConstants.DEBUGGING) { - Console.Write("EOF: "); - } -#endif - literalTree.WriteSymbol(EOF_SYMBOL); - -#if DebugDeflation - if (DeflaterConstants.DEBUGGING) { - literalTree.CheckEmpty(); - distTree.CheckEmpty(); - } -#endif - } - - /// - /// Flush block to output with no compression - /// - /// Data to write - /// Index of first byte to write - /// Count of bytes to write - /// True if this is the last block - public void FlushStoredBlock(byte[] stored, int storedOffset, int storedLength, bool lastBlock) - { -#if DebugDeflation - // if (DeflaterConstants.DEBUGGING) { - // //Console.WriteLine("Flushing stored block "+ storedLength); - // } -#endif - pending.WriteBits((DeflaterConstants.STORED_BLOCK << 1) + (lastBlock ? 1 : 0), 3); - pending.AlignToByte(); - pending.WriteShort(storedLength); - pending.WriteShort(~storedLength); - pending.WriteBlock(stored, storedOffset, storedLength); - Reset(); - } - - /// - /// Flush block to output with compression - /// - /// Data to flush - /// Index of first byte to flush - /// Count of bytes to flush - /// True if this is the last block - public void FlushBlock(byte[] stored, int storedOffset, int storedLength, bool lastBlock) - { - literalTree.freqs[EOF_SYMBOL]++; - - // Build trees - literalTree.BuildTree(); - distTree.BuildTree(); - - // Calculate bitlen frequency - literalTree.CalcBLFreq(blTree); - distTree.CalcBLFreq(blTree); - - // Build bitlen tree - blTree.BuildTree(); - - int blTreeCodes = 4; - for (int i = 18; i > blTreeCodes; i--) - { - if (blTree.length[BL_ORDER[i]] > 0) - { - blTreeCodes = i + 1; - } - } - int opt_len = 14 + blTreeCodes * 3 + blTree.GetEncodedLength() + - literalTree.GetEncodedLength() + distTree.GetEncodedLength() + - extra_bits; - - int static_len = extra_bits; - for (int i = 0; i < LITERAL_NUM; i++) - { - static_len += literalTree.freqs[i] * staticLLength[i]; - } - for (int i = 0; i < DIST_NUM; i++) - { - static_len += distTree.freqs[i] * staticDLength[i]; - } - if (opt_len >= static_len) - { - // Force static trees - opt_len = static_len; - } - - if (storedOffset >= 0 && storedLength + 4 < opt_len >> 3) - { - // Store Block - - // if (DeflaterConstants.DEBUGGING) { - // //Console.WriteLine("Storing, since " + storedLength + " < " + opt_len - // + " <= " + static_len); - // } - FlushStoredBlock(stored, storedOffset, storedLength, lastBlock); - } - else if (opt_len == static_len) - { - // Encode with static tree - pending.WriteBits((DeflaterConstants.STATIC_TREES << 1) + (lastBlock ? 1 : 0), 3); - literalTree.SetStaticCodes(staticLCodes, staticLLength); - distTree.SetStaticCodes(staticDCodes, staticDLength); - CompressBlock(); - Reset(); - } - else - { - // Encode with dynamic tree - pending.WriteBits((DeflaterConstants.DYN_TREES << 1) + (lastBlock ? 1 : 0), 3); - SendAllTrees(blTreeCodes); - CompressBlock(); - Reset(); - } - } - - /// - /// Get value indicating if internal buffer is full - /// - /// true if buffer is full - public bool IsFull() - { - return last_lit >= BUFSIZE; - } - - /// - /// Add literal to buffer - /// - /// Literal value to add to buffer. - /// Value indicating internal buffer is full - public bool TallyLit(int literal) - { - // if (DeflaterConstants.DEBUGGING) { - // if (lit > 32 && lit < 127) { - // //Console.WriteLine("("+(char)lit+")"); - // } else { - // //Console.WriteLine("{"+lit+"}"); - // } - // } - d_buf[last_lit] = 0; - l_buf[last_lit++] = (byte)literal; - literalTree.freqs[literal]++; - return IsFull(); - } - - /// - /// Add distance code and length to literal and distance trees - /// - /// Distance code - /// Length - /// Value indicating if internal buffer is full - public bool TallyDist(int distance, int length) - { - // if (DeflaterConstants.DEBUGGING) { - // //Console.WriteLine("[" + distance + "," + length + "]"); - // } - - d_buf[last_lit] = (short)distance; - l_buf[last_lit++] = (byte)(length - 3); - - int lc = Lcode(length - 3); - literalTree.freqs[lc]++; - if (lc >= 265 && lc < 285) - { - extra_bits += (lc - 261) / 4; - } - - int dc = Dcode(distance - 1); - distTree.freqs[dc]++; - if (dc >= 4) - { - extra_bits += dc / 2 - 1; - } - return IsFull(); - } - - /// - /// Reverse the bits of a 16 bit value. - /// - /// Value to reverse bits - /// Value with bits reversed - public static short BitReverse(int toReverse) - { - return (short)(bit4Reverse[toReverse & 0xF] << 12 | - bit4Reverse[(toReverse >> 4) & 0xF] << 8 | - bit4Reverse[(toReverse >> 8) & 0xF] << 4 | - bit4Reverse[toReverse >> 12]); - } - - private static int Lcode(int length) - { - if (length == 255) - { - return 285; } - - int code = 257; - while (length >= 8) - { - code += 4; - length >>= 1; - } - return code + length; - } - - private static int Dcode(int distance) - { - int code = 0; - while (distance >= 4) - { - code += 2; - distance >>= 1; - } - return code + distance; } } } From 0afece3af13e86a5f53be332b27353ebb9dd46df Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Wed, 13 Nov 2019 21:44:21 +1100 Subject: [PATCH 65/75] Move pending buffer --- src/ImageSharp/Formats/Png/Zlib/Deflater.cs | 19 ++++------- .../Formats/Png/Zlib/DeflaterEngine.cs | 19 +++++++---- .../Formats/Png/Zlib/DeflaterHuffman.cs | 33 ++++++++++++++++--- 3 files changed, 48 insertions(+), 23 deletions(-) diff --git a/src/ImageSharp/Formats/Png/Zlib/Deflater.cs b/src/ImageSharp/Formats/Png/Zlib/Deflater.cs index ef1d0e116d..d1560eb4b0 100644 --- a/src/ImageSharp/Formats/Png/Zlib/Deflater.cs +++ b/src/ImageSharp/Formats/Png/Zlib/Deflater.cs @@ -50,7 +50,6 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib /// private int state; - private DeflaterPendingBuffer pending; private DeflaterEngine engine; private bool isDisposed; @@ -80,10 +79,8 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib throw new ArgumentOutOfRangeException(nameof(level)); } - this.pending = new DeflaterPendingBuffer(memoryAllocator); - // TODO: Possibly provide DeflateStrategy as an option. - this.engine = new DeflaterEngine(memoryAllocator, this.pending, DeflateStrategy.Default); + this.engine = new DeflaterEngine(memoryAllocator, DeflateStrategy.Default); this.SetLevel(level); this.Reset(); @@ -126,7 +123,7 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib /// Gets a value indicating whetherthe stream was finished and no more output bytes /// are available. /// - public bool IsFinished => (this.state == FinishedState) && this.pending.IsFlushed; + public bool IsFinished => (this.state == FinishedState) && this.engine.Pending.IsFlushed; /// /// Gets a value indicating whether the input buffer is empty. @@ -145,7 +142,7 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib public void Reset() { this.state = BusyState; - this.pending.Reset(); + this.engine.Pending.Reset(); this.engine.Reset(); } @@ -236,7 +233,7 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib while (true) { - int count = this.pending.Flush(output, offset, length); + int count = this.engine.Pending.Flush(output, offset, length); offset += count; length -= count; @@ -259,11 +256,11 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib // We have to supply some lookahead. 8 bit lookahead // is needed by the zlib inflater, and we must fill // the next byte, so that all bits are flushed. - int neededbits = 8 + ((-this.pending.BitCount) & 7); + int neededbits = 8 + ((-this.engine.Pending.BitCount) & 7); while (neededbits > 0) { // Write a static tree block consisting solely of an EOF: - this.pending.WriteBits(2, 10); + this.engine.Pending.WriteBits(2, 10); neededbits -= 10; } } @@ -272,7 +269,7 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib break; case FinishingState: - this.pending.AlignToByte(); + this.engine.Pending.AlignToByte(); this.state = FinishedState; break; } @@ -296,10 +293,8 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib if (disposing) { this.engine.Dispose(); - this.pending.Dispose(); } - this.pending = null; this.engine = null; this.isDisposed = true; } diff --git a/src/ImageSharp/Formats/Png/Zlib/DeflaterEngine.cs b/src/ImageSharp/Formats/Png/Zlib/DeflaterEngine.cs index b146825015..8f57f51f9f 100644 --- a/src/ImageSharp/Formats/Png/Zlib/DeflaterEngine.cs +++ b/src/ImageSharp/Formats/Png/Zlib/DeflaterEngine.cs @@ -100,7 +100,6 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib private int inputEnd; private readonly DeflateStrategy strategy; - private readonly DeflaterPendingBuffer pending; private DeflaterHuffman huffman; private bool isDisposed; @@ -145,12 +144,11 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib /// Initializes a new instance of the class. /// /// The memory allocator to use for buffer allocations. - /// The pending buffer to use. /// The deflate strategy to use. - public DeflaterEngine(MemoryAllocator memoryAllocator, DeflaterPendingBuffer pending, DeflateStrategy strategy) + public DeflaterEngine(MemoryAllocator memoryAllocator, DeflateStrategy strategy) { - this.pending = pending; - this.huffman = new DeflaterHuffman(pending); + this.huffman = new DeflaterHuffman(memoryAllocator); + this.Pending = this.huffman.Pending; this.strategy = strategy; // Create pinned pointers to the various buffers to allow indexing @@ -175,6 +173,11 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib this.blockStart = this.strstart = 1; } + /// + /// Gets the pending buffer to use. + /// + public DeflaterPendingBuffer Pending { get; } + /// /// Deflate drives actual compression of data /// @@ -208,7 +211,7 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib break; } } - while (this.pending.IsFlushed && progress); // repeat while we have no pending output and progress was made + while (this.Pending.IsFlushed && progress); // repeat while we have no pending output and progress was made return progress; } @@ -834,6 +837,8 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib { if (disposing) { + this.huffman.Dispose(); + this.windowBufferHandle.Dispose(); this.windowBuffer.Dispose(); @@ -844,6 +849,8 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib this.prevBuffer.Dispose(); } + this.huffman = null; + this.isDisposed = true; } } diff --git a/src/ImageSharp/Formats/Png/Zlib/DeflaterHuffman.cs b/src/ImageSharp/Formats/Png/Zlib/DeflaterHuffman.cs index bc7ad7a38b..1a2d661c11 100644 --- a/src/ImageSharp/Formats/Png/Zlib/DeflaterHuffman.cs +++ b/src/ImageSharp/Formats/Png/Zlib/DeflaterHuffman.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Text; +using SixLabors.Memory; namespace SixLabors.ImageSharp.Formats.Png.Zlib { @@ -15,7 +16,7 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib /// /// author of the original java version : Jochen Hoenicke ///
- public class DeflaterHuffman + public sealed class DeflaterHuffman : IDisposable { private const int BufferSize = 1 << (DeflaterConstants.DEFAULT_MEM_LEVEL + 6); @@ -60,6 +61,7 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib private byte[] literalBuffer; private int lastLiteral; private int extraBits; + private bool isDisposed; static DeflaterHuffman() { @@ -106,10 +108,10 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib /// /// Initializes a new instance of the class. /// - /// Pending buffer to use - public DeflaterHuffman(DeflaterPendingBuffer pending) + /// The memory allocator to use for buffer allocations. + public DeflaterHuffman(MemoryAllocator memoryAllocator) { - this.Pending = pending; + this.Pending = new DeflaterPendingBuffer(memoryAllocator); this.literalTree = new Tree(this, LiteralNumber, 257, 15); this.distTree = new Tree(this, DistanceNumber, 1, 15); @@ -122,7 +124,7 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib /// /// Gets the pending buffer to use. /// - public DeflaterPendingBuffer Pending { get; } + public DeflaterPendingBuffer Pending { get; private set; } /// /// Reset internal state @@ -351,6 +353,13 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib Bit4Reverse[toReverse >> 12]); } + /// + public void Dispose() + { + this.Dispose(true); + GC.SuppressFinalize(this); + } + private static int Lcode(int length) { if (length == 255) @@ -380,6 +389,20 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib return code + distance; } + private void Dispose(bool disposing) + { + if (!this.isDisposed) + { + if (disposing) + { + this.Pending.Dispose(); + } + + this.Pending = null; + this.isDisposed = true; + } + } + private class Tree { private readonly int minNumCodes; From b66a95bd5ef3991f2c378991522969807f25e3f0 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 14 Nov 2019 00:01:03 +1100 Subject: [PATCH 66/75] Use pinned buffers for distances --- .../Formats/Png/Zlib/DeflateThrowHelper.cs | 7 + .../Formats/Png/Zlib/DeflaterHuffman.cs | 205 ++++++++++-------- .../Formats/Png/Zlib/DeflaterPendingBuffer.cs | 1 - 3 files changed, 119 insertions(+), 94 deletions(-) diff --git a/src/ImageSharp/Formats/Png/Zlib/DeflateThrowHelper.cs b/src/ImageSharp/Formats/Png/Zlib/DeflateThrowHelper.cs index 2698c9b99b..d7198c4ee9 100644 --- a/src/ImageSharp/Formats/Png/Zlib/DeflateThrowHelper.cs +++ b/src/ImageSharp/Formats/Png/Zlib/DeflateThrowHelper.cs @@ -17,6 +17,7 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib [MethodImpl(InliningOptions.ColdPath)] public static void ThrowUnknownCompression() => throw new InvalidOperationException("Unknown compression function."); + [MethodImpl(InliningOptions.ColdPath)] public static void ThrowNotProcessed() => throw new InvalidOperationException("Old input was not completely processed."); [MethodImpl(InliningOptions.ColdPath)] @@ -24,5 +25,11 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib [MethodImpl(InliningOptions.ColdPath)] public static void ThrowOutOfRange(string name) => throw new ArgumentOutOfRangeException(name); + + [MethodImpl(InliningOptions.ColdPath)] + public static void ThrowFrequencyNotEmpty() => throw new InvalidOperationException("Huffman frequency entry non empty."); + + [MethodImpl(InliningOptions.ColdPath)] + public static void ThrowHeapViolated() => throw new InvalidOperationException("Huffman heap invariant violated."); } } diff --git a/src/ImageSharp/Formats/Png/Zlib/DeflaterHuffman.cs b/src/ImageSharp/Formats/Png/Zlib/DeflaterHuffman.cs index 1a2d661c11..36cf0d5594 100644 --- a/src/ImageSharp/Formats/Png/Zlib/DeflaterHuffman.cs +++ b/src/ImageSharp/Formats/Png/Zlib/DeflaterHuffman.cs @@ -2,8 +2,8 @@ // Licensed under the Apache License, Version 2.0. using System; -using System.Collections.Generic; -using System.Text; +using System.Buffers; +using System.Runtime.CompilerServices; using SixLabors.Memory; namespace SixLabors.ImageSharp.Formats.Png.Zlib @@ -16,7 +16,7 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib /// /// author of the original java version : Jochen Hoenicke /// - public sealed class DeflaterHuffman : IDisposable + public sealed unsafe class DeflaterHuffman : IDisposable { private const int BufferSize = 1 << (DeflaterConstants.DEFAULT_MEM_LEVEL + 6); @@ -29,13 +29,13 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib // Number of codes used to transfer bit lengths private const int BitLengthNumber = 19; - // repeat previous bit length 3-6 times (2 bits of repeat count) + // Repeat previous bit length 3-6 times (2 bits of repeat count) private const int Repeat3To6 = 16; - // repeat a zero length 3-10 times (3 bits of repeat count) + // Repeat a zero length 3-10 times (3 bits of repeat count) private const int Repeat3To10 = 17; - // repeat a zero length 11-138 times (7 bits of repeat count) + // Repeat a zero length 11-138 times (7 bits of repeat count) private const int Repeat11To138 = 18; private const int EofSymbol = 256; @@ -46,19 +46,24 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib private static readonly byte[] Bit4Reverse = { 0, 8, 4, 12, 2, 10, 6, 14, 1, 9, 5, 13, 3, 11, 7, 15 }; - private static short[] staticLCodes; - private static byte[] staticLLength; - private static short[] staticDCodes; - private static byte[] staticDLength; + private static readonly short[] StaticLCodes; + private static readonly byte[] StaticLLength; + private static readonly short[] StaticDCodes; + private static readonly byte[] StaticDLength; private Tree literalTree; private Tree distTree; private Tree blTree; // Buffer for distances - private short[] distanceBuffer; + private readonly IMemoryOwner distanceManagedBuffer; + private readonly short* pinnedDistanceBuffer; + private MemoryHandle distanceBufferHandle; + + private readonly IMemoryOwner literalManagedBuffer; + private readonly short* pinnedLiteralBuffer; + private MemoryHandle literalBufferHandle; - private byte[] literalBuffer; private int lastLiteral; private int extraBits; private bool isDisposed; @@ -67,41 +72,41 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib { // See RFC 1951 3.2.6 // Literal codes - staticLCodes = new short[LiteralNumber]; - staticLLength = new byte[LiteralNumber]; + StaticLCodes = new short[LiteralNumber]; + StaticLLength = new byte[LiteralNumber]; int i = 0; while (i < 144) { - staticLCodes[i] = BitReverse((0x030 + i) << 8); - staticLLength[i++] = 8; + StaticLCodes[i] = BitReverse((0x030 + i) << 8); + StaticLLength[i++] = 8; } while (i < 256) { - staticLCodes[i] = BitReverse((0x190 - 144 + i) << 7); - staticLLength[i++] = 9; + StaticLCodes[i] = BitReverse((0x190 - 144 + i) << 7); + StaticLLength[i++] = 9; } while (i < 280) { - staticLCodes[i] = BitReverse((0x000 - 256 + i) << 9); - staticLLength[i++] = 7; + StaticLCodes[i] = BitReverse((0x000 - 256 + i) << 9); + StaticLLength[i++] = 7; } while (i < LiteralNumber) { - staticLCodes[i] = BitReverse((0x0c0 - 280 + i) << 8); - staticLLength[i++] = 8; + StaticLCodes[i] = BitReverse((0x0c0 - 280 + i) << 8); + StaticLLength[i++] = 8; } // Distance codes - staticDCodes = new short[DistanceNumber]; - staticDLength = new byte[DistanceNumber]; + StaticDCodes = new short[DistanceNumber]; + StaticDLength = new byte[DistanceNumber]; for (i = 0; i < DistanceNumber; i++) { - staticDCodes[i] = BitReverse(i << 11); - staticDLength[i] = 5; + StaticDCodes[i] = BitReverse(i << 11); + StaticDLength[i] = 5; } } @@ -113,12 +118,17 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib { this.Pending = new DeflaterPendingBuffer(memoryAllocator); - this.literalTree = new Tree(this, LiteralNumber, 257, 15); - this.distTree = new Tree(this, DistanceNumber, 1, 15); - this.blTree = new Tree(this, BitLengthNumber, 4, 7); + this.literalTree = new Tree(LiteralNumber, 257, 15); + this.distTree = new Tree(DistanceNumber, 1, 15); + this.blTree = new Tree(BitLengthNumber, 4, 7); + + this.distanceManagedBuffer = memoryAllocator.Allocate(BufferSize); + this.distanceBufferHandle = this.distanceManagedBuffer.Memory.Pin(); + this.pinnedDistanceBuffer = (short*)this.distanceBufferHandle.Pointer; - this.distanceBuffer = new short[BufferSize]; - this.literalBuffer = new byte[BufferSize]; + this.literalManagedBuffer = memoryAllocator.Allocate(BufferSize); + this.literalBufferHandle = this.literalManagedBuffer.Memory.Pin(); + this.pinnedLiteralBuffer = (short*)this.literalBufferHandle.Pointer; } /// @@ -155,8 +165,8 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib this.Pending.WriteBits(this.blTree.Length[BitLengthOrder[rank]], 3); } - this.literalTree.WriteTree(this.blTree); - this.distTree.WriteTree(this.blTree); + this.literalTree.WriteTree(this.Pending, this.blTree); + this.distTree.WriteTree(this.Pending, this.blTree); } /// @@ -164,14 +174,18 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib /// public void CompressBlock() { + DeflaterPendingBuffer pendingBuffer = this.Pending; + short* pinnedDistance = this.pinnedDistanceBuffer; + short* pinnedLiteral = this.pinnedLiteralBuffer; + for (int i = 0; i < this.lastLiteral; i++) { - int litlen = this.literalBuffer[i] & 0xff; - int dist = this.distanceBuffer[i]; + int litlen = pinnedLiteral[i] & 0xFF; + int dist = pinnedDistance[i]; if (dist-- != 0) { int lc = Lcode(litlen); - this.literalTree.WriteSymbol(lc); + this.literalTree.WriteSymbol(pendingBuffer, lc); int bits = (lc - 261) / 4; if (bits > 0 && bits <= 5) @@ -180,7 +194,7 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib } int dc = Dcode(dist); - this.distTree.WriteSymbol(dc); + this.distTree.WriteSymbol(pendingBuffer, dc); bits = (dc / 2) - 1; if (bits > 0) @@ -190,11 +204,11 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib } else { - this.literalTree.WriteSymbol(litlen); + this.literalTree.WriteSymbol(pendingBuffer, litlen); } } - this.literalTree.WriteSymbol(EofSymbol); + this.literalTree.WriteSymbol(pendingBuffer, EofSymbol); } /// @@ -245,19 +259,19 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib } } - int opt_len = 14 + (blTreeCodes * 3) + this.blTree.GetEncodedLength() + - this.literalTree.GetEncodedLength() + this.distTree.GetEncodedLength() + - this.extraBits; + int opt_len = 14 + (blTreeCodes * 3) + this.blTree.GetEncodedLength() + + this.literalTree.GetEncodedLength() + this.distTree.GetEncodedLength() + + this.extraBits; int static_len = this.extraBits; for (int i = 0; i < LiteralNumber; i++) { - static_len += this.literalTree.Freqs[i] * staticLLength[i]; + static_len += this.literalTree.Freqs[i] * StaticLLength[i]; } for (int i = 0; i < DistanceNumber; i++) { - static_len += this.distTree.Freqs[i] * staticDLength[i]; + static_len += this.distTree.Freqs[i] * StaticDLength[i]; } if (opt_len >= static_len) @@ -275,8 +289,8 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib { // Encode with static tree this.Pending.WriteBits((DeflaterConstants.STATIC_TREES << 1) + (lastBlock ? 1 : 0), 3); - this.literalTree.SetStaticCodes(staticLCodes, staticLLength); - this.distTree.SetStaticCodes(staticDCodes, staticDLength); + this.literalTree.SetStaticCodes(StaticLCodes, StaticLLength); + this.distTree.SetStaticCodes(StaticDCodes, StaticDLength); this.CompressBlock(); this.Reset(); } @@ -294,20 +308,19 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib /// Get value indicating if internal buffer is full /// /// true if buffer is full - public bool IsFull() - { - return this.lastLiteral >= BufferSize; - } + [MethodImpl(InliningOptions.ShortMethod)] + public bool IsFull() => this.lastLiteral >= BufferSize; /// /// Add literal to buffer /// /// Literal value to add to buffer. /// Value indicating internal buffer is full + [MethodImpl(InliningOptions.ShortMethod)] public bool TallyLit(int literal) { - this.distanceBuffer[this.lastLiteral] = 0; - this.literalBuffer[this.lastLiteral++] = (byte)literal; + this.pinnedDistanceBuffer[this.lastLiteral] = 0; + this.pinnedLiteralBuffer[this.lastLiteral++] = (byte)literal; this.literalTree.Freqs[literal]++; return this.IsFull(); } @@ -318,10 +331,11 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib /// Distance code /// Length /// Value indicating if internal buffer is full + [MethodImpl(InliningOptions.ShortMethod)] public bool TallyDist(int distance, int length) { - this.distanceBuffer[this.lastLiteral] = (short)distance; - this.literalBuffer[this.lastLiteral++] = (byte)(length - 3); + this.pinnedDistanceBuffer[this.lastLiteral] = (short)distance; + this.pinnedLiteralBuffer[this.lastLiteral++] = (byte)(length - 3); int lc = Lcode(length - 3); this.literalTree.Freqs[lc]++; @@ -345,12 +359,13 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib /// /// Value to reverse bits /// Value with bits reversed + [MethodImpl(InliningOptions.ShortMethod)] public static short BitReverse(int toReverse) { - return (short)(Bit4Reverse[toReverse & 0xF] << 12 | - Bit4Reverse[(toReverse >> 4) & 0xF] << 8 | - Bit4Reverse[(toReverse >> 8) & 0xF] << 4 | - Bit4Reverse[toReverse >> 12]); + return (short)(Bit4Reverse[toReverse & 0xF] << 12 + | Bit4Reverse[(toReverse >> 4) & 0xF] << 8 + | Bit4Reverse[(toReverse >> 8) & 0xF] << 4 + | Bit4Reverse[toReverse >> 12]); } /// @@ -360,6 +375,7 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib GC.SuppressFinalize(this); } + [MethodImpl(InliningOptions.ShortMethod)] private static int Lcode(int length) { if (length == 255) @@ -377,6 +393,7 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib return code + length; } + [MethodImpl(InliningOptions.ShortMethod)] private static int Dcode(int distance) { int code = 0; @@ -396,6 +413,10 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib if (disposing) { this.Pending.Dispose(); + this.distanceBufferHandle.Dispose(); + this.distanceManagedBuffer.Dispose(); + this.literalBufferHandle.Dispose(); + this.literalManagedBuffer.Dispose(); } this.Pending = null; @@ -403,20 +424,18 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib } } - private class Tree + private sealed class Tree { private readonly int minNumCodes; private short[] codes; private readonly int[] bitLengthCounts; private readonly int maxLength; - private readonly DeflaterHuffman dh; - public Tree(DeflaterHuffman dh, int elems, int minCodes, int maxLength) + public Tree(int elements, int minCodes, int maxLength) { - this.dh = dh; this.minNumCodes = minCodes; this.maxLength = maxLength; - this.Freqs = new short[elems]; + this.Freqs = new short[elements]; this.bitLengthCounts = new int[maxLength]; } @@ -429,6 +448,7 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib /// /// Resets the internal state of the tree /// + [MethodImpl(InliningOptions.ShortMethod)] public void Reset() { for (int i = 0; i < this.Freqs.Length; i++) @@ -440,17 +460,17 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib this.Length = null; } - public void WriteSymbol(int code) - { - this.dh.Pending.WriteBits(this.codes[code] & 0xffff, this.Length[code]); - } + [MethodImpl(InliningOptions.ShortMethod)] + public void WriteSymbol(DeflaterPendingBuffer pendingBuffer, int code) + => pendingBuffer.WriteBits(this.codes[code] & 0xFFFF, this.Length[code]); /// /// Check that all frequencies are zero /// - /// + /// /// At least one frequency is non-zero /// + [MethodImpl(InliningOptions.ShortMethod)] public void CheckEmpty() { bool empty = true; @@ -461,7 +481,7 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib if (!empty) { - throw new ImageFormatException("!Empty"); + DeflateThrowHelper.ThrowFrequencyNotEmpty(); } } @@ -481,7 +501,6 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib ///
public void BuildCodes() { - int numSymbols = this.Freqs.Length; int[] nextCode = new int[this.maxLength]; int code = 0; @@ -544,8 +563,7 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib // this case, both literals get a 1 bit code. while (heapLen < 2) { - int node = maxCode < 2 ? ++maxCode : 0; - heap[heapLen++] = node; + heap[heapLen++] = maxCode < 2 ? ++maxCode : 0; } this.NumCodes = Math.Max(maxCode + 1, this.minNumCodes); @@ -602,7 +620,7 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib last = numNodes++; childs[2 * last] = first; childs[(2 * last) + 1] = second; - int mindepth = Math.Min(values[first] & 0xff, values[second] & 0xff); + int mindepth = Math.Min(values[first] & 0xFF, values[second] & 0xFF); values[last] = lastVal = values[first] + values[second] - mindepth + 1; // Again, propagate the hole to the leafs @@ -633,7 +651,7 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib if (heap[0] != (childs.Length / 2) - 1) { - throw new ImageFormatException("Heap invariant violated"); + DeflateThrowHelper.ThrowHeapViolated(); } this.BuildLength(childs); @@ -718,10 +736,11 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib } /// - /// Write tree values + /// Write the tree values. /// - /// Tree to write - public void WriteTree(Tree blTree) + /// The pending buffer. + /// The tree to write. + public void WriteTree(DeflaterPendingBuffer pendingBuffer, Tree bitLengthTree) { int max_count; // max repeat count int min_count; // min repeat count @@ -744,7 +763,7 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib min_count = 3; if (curlen != nextlen) { - blTree.WriteSymbol(nextlen); + bitLengthTree.WriteSymbol(pendingBuffer, nextlen); count = 0; } } @@ -765,31 +784,31 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib { while (count-- > 0) { - blTree.WriteSymbol(curlen); + bitLengthTree.WriteSymbol(pendingBuffer, curlen); } } else if (curlen != 0) { - blTree.WriteSymbol(Repeat3To6); - this.dh.Pending.WriteBits(count - 3, 2); + bitLengthTree.WriteSymbol(pendingBuffer, Repeat3To6); + pendingBuffer.WriteBits(count - 3, 2); } else if (count <= 10) { - blTree.WriteSymbol(Repeat3To10); - this.dh.Pending.WriteBits(count - 3, 3); + bitLengthTree.WriteSymbol(pendingBuffer, Repeat3To10); + pendingBuffer.WriteBits(count - 3, 3); } else { - blTree.WriteSymbol(Repeat11To138); - this.dh.Pending.WriteBits(count - 11, 7); + bitLengthTree.WriteSymbol(pendingBuffer, Repeat11To138); + pendingBuffer.WriteBits(count - 11, 7); } } } - private void BuildLength(int[] childs) + private void BuildLength(int[] children) { this.Length = new byte[this.Freqs.Length]; - int numNodes = childs.Length / 2; + int numNodes = children.Length / 2; int numLeafs = (numNodes + 1) / 2; int overflow = 0; @@ -804,7 +823,7 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib for (int i = numNodes - 1; i >= 0; i--) { - if (childs[(2 * i) + 1] != -1) + if (children[(2 * i) + 1] != -1) { int bitLength = lengths[i] + 1; if (bitLength > this.maxLength) @@ -813,14 +832,14 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib overflow++; } - lengths[childs[2 * i]] = lengths[childs[(2 * i) + 1]] = bitLength; + lengths[children[2 * i]] = lengths[children[(2 * i) + 1]] = bitLength; } else { // A leaf node int bitLength = lengths[i]; this.bitLengthCounts[bitLength - 1]++; - this.Length[childs[2 * i]] = (byte)lengths[i]; + this.Length[children[2 * i]] = (byte)lengths[i]; } } @@ -867,11 +886,11 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib int n = this.bitLengthCounts[bits - 1]; while (n > 0) { - int childPtr = 2 * childs[nodePtr++]; - if (childs[childPtr + 1] == -1) + int childPtr = 2 * children[nodePtr++]; + if (children[childPtr + 1] == -1) { // We found another leaf - this.Length[childs[childPtr]] = (byte)bits; + this.Length[children[childPtr]] = (byte)bits; n--; } } diff --git a/src/ImageSharp/Formats/Png/Zlib/DeflaterPendingBuffer.cs b/src/ImageSharp/Formats/Png/Zlib/DeflaterPendingBuffer.cs index 64214b47e6..da4d890402 100644 --- a/src/ImageSharp/Formats/Png/Zlib/DeflaterPendingBuffer.cs +++ b/src/ImageSharp/Formats/Png/Zlib/DeflaterPendingBuffer.cs @@ -29,7 +29,6 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib /// The memory allocator to use for buffer allocations. public DeflaterPendingBuffer(MemoryAllocator memoryAllocator) { - this.buffer = new byte[DeflaterConstants.PENDING_BUF_SIZE]; this.managedBuffer = memoryAllocator.AllocateManagedByteBuffer(DeflaterConstants.PENDING_BUF_SIZE); this.buffer = this.managedBuffer.Array; this.handle = this.managedBuffer.Memory.Pin(); From c3d5fab94a6e682dcec1163ee245571fa2211b3a Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 14 Nov 2019 00:59:16 +1100 Subject: [PATCH 67/75] Remove a few more bounds checks. --- src/ImageSharp/Formats/Png/Zlib/DeflaterHuffman.cs | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/src/ImageSharp/Formats/Png/Zlib/DeflaterHuffman.cs b/src/ImageSharp/Formats/Png/Zlib/DeflaterHuffman.cs index 36cf0d5594..b2eddc37bb 100644 --- a/src/ImageSharp/Formats/Png/Zlib/DeflaterHuffman.cs +++ b/src/ImageSharp/Formats/Png/Zlib/DeflaterHuffman.cs @@ -4,17 +4,13 @@ using System; using System.Buffers; using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; using SixLabors.Memory; namespace SixLabors.ImageSharp.Formats.Png.Zlib { /// - /// This is the DeflaterHuffman class. - /// - /// This class is not thread safe. This is inherent in the API, due - /// to the split of Deflate and SetInput. - /// - /// author of the original java version : Jochen Hoenicke + /// Performs Deflate Huffman encoding. /// public sealed unsafe class DeflaterHuffman : IDisposable { @@ -264,14 +260,16 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib + this.extraBits; int static_len = this.extraBits; + ref byte staticLLengthRef = ref MemoryMarshal.GetReference(StaticLLength); for (int i = 0; i < LiteralNumber; i++) { - static_len += this.literalTree.Freqs[i] * StaticLLength[i]; + static_len += this.literalTree.Freqs[i] * Unsafe.Add(ref staticLLengthRef, i); } + ref byte staticDLengthRef = ref MemoryMarshal.GetReference(StaticDLength); for (int i = 0; i < DistanceNumber; i++) { - static_len += this.distTree.Freqs[i] * StaticDLength[i]; + static_len += this.distTree.Freqs[i] * Unsafe.Add(ref staticDLengthRef, i); } if (opt_len >= static_len) From 6d56192970a394259b554badf35bee992b582f5a Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 14 Nov 2019 15:49:39 +1100 Subject: [PATCH 68/75] More speedup and bounds checks removal. --- .../Formats/Png/Zlib/DeflaterHuffman.cs | 198 ++++++++++++------ .../Formats/Png/Zlib/DeflaterPendingBuffer.cs | 18 +- 2 files changed, 144 insertions(+), 72 deletions(-) diff --git a/src/ImageSharp/Formats/Png/Zlib/DeflaterHuffman.cs b/src/ImageSharp/Formats/Png/Zlib/DeflaterHuffman.cs index b2eddc37bb..50aa1c0956 100644 --- a/src/ImageSharp/Formats/Png/Zlib/DeflaterHuffman.cs +++ b/src/ImageSharp/Formats/Png/Zlib/DeflaterHuffman.cs @@ -64,6 +64,7 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib private int extraBits; private bool isDisposed; + // TODO: These should be pre-generated array/readonlyspans. static DeflaterHuffman() { // See RFC 1951 3.2.6 @@ -114,9 +115,9 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib { this.Pending = new DeflaterPendingBuffer(memoryAllocator); - this.literalTree = new Tree(LiteralNumber, 257, 15); - this.distTree = new Tree(DistanceNumber, 1, 15); - this.blTree = new Tree(BitLengthNumber, 4, 7); + this.literalTree = new Tree(memoryAllocator, LiteralNumber, 257, 15); + this.distTree = new Tree(memoryAllocator, DistanceNumber, 1, 15); + this.blTree = new Tree(memoryAllocator, BitLengthNumber, 4, 7); this.distanceManagedBuffer = memoryAllocator.Allocate(BufferSize); this.distanceBufferHandle = this.distanceManagedBuffer.Memory.Pin(); @@ -135,6 +136,7 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib /// /// Reset internal state /// + [MethodImpl(InliningOptions.ShortMethod)] public void Reset() { this.lastLiteral = 0; @@ -214,6 +216,7 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib /// Index of first byte to write /// Count of bytes to write /// True if this is the last block + [MethodImpl(InliningOptions.ShortMethod)] public void FlushStoredBlock(byte[] stored, int storedOffset, int storedLength, bool lastBlock) { this.Pending.WriteBits((DeflaterConstants.STORED_BLOCK << 1) + (lastBlock ? 1 : 0), 3); @@ -233,7 +236,7 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib /// True if this is the last block public void FlushBlock(byte[] stored, int storedOffset, int storedLength, bool lastBlock) { - this.literalTree.Freqs[EofSymbol]++; + this.literalTree.Frequencies[EofSymbol]++; // Build trees this.literalTree.BuildTree(); @@ -263,13 +266,13 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib ref byte staticLLengthRef = ref MemoryMarshal.GetReference(StaticLLength); for (int i = 0; i < LiteralNumber; i++) { - static_len += this.literalTree.Freqs[i] * Unsafe.Add(ref staticLLengthRef, i); + static_len += this.literalTree.Frequencies[i] * Unsafe.Add(ref staticLLengthRef, i); } ref byte staticDLengthRef = ref MemoryMarshal.GetReference(StaticDLength); for (int i = 0; i < DistanceNumber; i++) { - static_len += this.distTree.Freqs[i] * Unsafe.Add(ref staticDLengthRef, i); + static_len += this.distTree.Frequencies[i] * Unsafe.Add(ref staticDLengthRef, i); } if (opt_len >= static_len) @@ -319,7 +322,7 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib { this.pinnedDistanceBuffer[this.lastLiteral] = 0; this.pinnedLiteralBuffer[this.lastLiteral++] = (byte)literal; - this.literalTree.Freqs[literal]++; + this.literalTree.Frequencies[literal]++; return this.IsFull(); } @@ -336,14 +339,14 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib this.pinnedLiteralBuffer[this.lastLiteral++] = (byte)(length - 3); int lc = Lcode(length - 3); - this.literalTree.Freqs[lc]++; + this.literalTree.Frequencies[lc]++; if (lc >= 265 && lc < 285) { this.extraBits += (lc - 261) / 4; } int dc = Dcode(distance - 1); - this.distTree.Freqs[dc]++; + this.distTree.Frequencies[dc]++; if (dc >= 4) { this.extraBits += (dc / 2) - 1; @@ -415,33 +418,69 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib this.distanceManagedBuffer.Dispose(); this.literalBufferHandle.Dispose(); this.literalManagedBuffer.Dispose(); + + this.literalTree.Dispose(); + this.blTree.Dispose(); + this.distTree.Dispose(); } this.Pending = null; + + this.literalTree = null; + this.blTree = null; + this.distTree = null; + this.isDisposed = true; } } - private sealed class Tree + private sealed class Tree : IDisposable { private readonly int minNumCodes; - private short[] codes; private readonly int[] bitLengthCounts; private readonly int maxLength; + private bool isDisposed; - public Tree(int elements, int minCodes, int maxLength) + private readonly int elementCount; + + private IMemoryOwner codesMemoryOwner; + private MemoryHandle codesMemoryHandle; + private short* codes; + + private IMemoryOwner frequenciesMemoryOwner; + private MemoryHandle frequenciesMemoryHandle; + + private IManagedByteBuffer lengthsMemoryOwner; + private MemoryHandle lengthsMemoryHandle; + + public Tree(MemoryAllocator memoryAllocator, int elements, int minCodes, int maxLength) { + this.elementCount = elements; + this.minNumCodes = minCodes; this.maxLength = maxLength; - this.Freqs = new short[elements]; + + this.frequenciesMemoryOwner = memoryAllocator.Allocate(elements); + this.frequenciesMemoryHandle = this.frequenciesMemoryOwner.Memory.Pin(); + this.Frequencies = (short*)this.frequenciesMemoryHandle.Pointer; + + this.lengthsMemoryOwner = memoryAllocator.AllocateManagedByteBuffer(elements); + this.lengthsMemoryHandle = this.lengthsMemoryOwner.Memory.Pin(); + this.Length = (byte*)this.lengthsMemoryHandle.Pointer; + + this.codesMemoryOwner = memoryAllocator.Allocate(elements); + this.codesMemoryHandle = this.codesMemoryOwner.Memory.Pin(); + this.codes = (short*)this.codesMemoryHandle.Pointer; + + // Maxes out at 15. this.bitLengthCounts = new int[maxLength]; } public int NumCodes { get; private set; } - public short[] Freqs { get; } + public short* Frequencies { get; } - public byte[] Length { get; set; } + public byte* Length { get; } /// /// Resets the internal state of the tree @@ -449,13 +488,9 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib [MethodImpl(InliningOptions.ShortMethod)] public void Reset() { - for (int i = 0; i < this.Freqs.Length; i++) - { - this.Freqs[i] = 0; - } - - this.codes = null; - this.Length = null; + this.frequenciesMemoryOwner.Memory.Span.Clear(); + this.lengthsMemoryOwner.Memory.Span.Clear(); + this.codesMemoryOwner.Memory.Span.Clear(); } [MethodImpl(InliningOptions.ShortMethod)] @@ -472,9 +507,9 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib public void CheckEmpty() { bool empty = true; - for (int i = 0; i < this.Freqs.Length; i++) + for (int i = 0; i < this.elementCount; i++) { - empty &= this.Freqs[i] == 0; + empty &= this.Frequencies[i] == 0; } if (!empty) @@ -488,10 +523,11 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib /// /// new codes /// length for new codes - public void SetStaticCodes(short[] staticCodes, byte[] staticLengths) + [MethodImpl(InliningOptions.ShortMethod)] + public void SetStaticCodes(ReadOnlySpan staticCodes, ReadOnlySpan staticLengths) { - this.codes = staticCodes; - this.Length = staticLengths; + staticCodes.CopyTo(this.codesMemoryOwner.Memory.Span); + staticLengths.CopyTo(this.lengthsMemoryOwner.Memory.Span); } /// @@ -499,15 +535,16 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib /// public void BuildCodes() { - int[] nextCode = new int[this.maxLength]; - int code = 0; - - this.codes = new short[this.Freqs.Length]; + // Maxes out at 15 * 4 + Span nextCode = stackalloc int[this.maxLength]; + ref int nextCodeRef = ref MemoryMarshal.GetReference(nextCode); + ref int bitLengthCountsRef = ref MemoryMarshal.GetReference(this.bitLengthCounts); + int code = 0; for (int bits = 0; bits < this.maxLength; bits++) { - nextCode[bits] = code; - code += this.bitLengthCounts[bits] << (15 - bits); + Unsafe.Add(ref nextCodeRef, bits) = code; + code += Unsafe.Add(ref bitLengthCountsRef, bits) << (15 - bits); } for (int i = 0; i < this.NumCodes; i++) @@ -515,15 +552,15 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib int bits = this.Length[i]; if (bits > 0) { - this.codes[i] = BitReverse(nextCode[bits - 1]); - nextCode[bits - 1] += 1 << (16 - bits); + this.codes[i] = BitReverse(Unsafe.Add(ref nextCodeRef, bits - 1)); + Unsafe.Add(ref nextCodeRef, bits - 1) += 1 << (16 - bits); } } } public void BuildTree() { - int numSymbols = this.Freqs.Length; + int numSymbols = this.elementCount; // heap is a priority queue, sorted by frequency, least frequent // nodes first. The heap is a binary tree, with the property, that @@ -532,20 +569,23 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib // // The binary tree is encoded in an array: 0 is root node and // the nodes 2*n+1, 2*n+2 are the child nodes of node n. - int[] heap = new int[numSymbols]; + // Maxes out at 286 * 4 + Span heap = stackalloc int[numSymbols]; + ref int heapRef = ref MemoryMarshal.GetReference(heap); + int heapLen = 0; int maxCode = 0; for (int n = 0; n < numSymbols; n++) { - int freq = this.Freqs[n]; + int freq = this.Frequencies[n]; if (freq != 0) { // Insert n into heap int pos = heapLen++; int ppos; - while (pos > 0 && this.Freqs[heap[ppos = (pos - 1) / 2]] > freq) + while (pos > 0 && this.Frequencies[Unsafe.Add(ref heapRef, ppos = (pos - 1) / 2)] > freq) { - heap[pos] = heap[ppos]; + Unsafe.Add(ref heapRef, pos) = Unsafe.Add(ref heapRef, ppos); pos = ppos; } @@ -561,7 +601,7 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib // this case, both literals get a 1 bit code. while (heapLen < 2) { - heap[heapLen++] = maxCode < 2 ? ++maxCode : 0; + Unsafe.Add(ref heapRef, heapLen++) = maxCode < 2 ? ++maxCode : 0; } this.NumCodes = Math.Max(maxCode + 1, this.minNumCodes); @@ -572,10 +612,10 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib int numNodes = numLeafs; for (int i = 0; i < heapLen; i++) { - int node = heap[i]; + int node = Unsafe.Add(ref heapRef, i); childs[2 * i] = node; childs[(2 * i) + 1] = -1; - values[i] = this.Freqs[node] << 8; + values[i] = this.Frequencies[node] << 8; heap[i] = i; } @@ -584,7 +624,7 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib do { int first = heap[0]; - int last = heap[--heapLen]; + int last = Unsafe.Add(ref heapRef, --heapLen); // Propagate the hole to the leafs of the heap int ppos = 0; @@ -592,12 +632,12 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib while (path < heapLen) { - if (path + 1 < heapLen && values[heap[path]] > values[heap[path + 1]]) + if (path + 1 < heapLen && values[Unsafe.Add(ref heapRef, path)] > values[Unsafe.Add(ref heapRef, path + 1)]) { path++; } - heap[ppos] = heap[path]; + Unsafe.Add(ref heapRef, ppos) = Unsafe.Add(ref heapRef, path); ppos = path; path = (path * 2) + 1; } @@ -605,14 +645,14 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib // Now propagate the last element down along path. Normally // it shouldn't go too deep. int lastVal = values[last]; - while ((path = ppos) > 0 && values[heap[ppos = (path - 1) / 2]] > lastVal) + while ((path = ppos) > 0 && values[Unsafe.Add(ref heapRef, ppos = (path - 1) / 2)] > lastVal) { - heap[path] = heap[ppos]; + Unsafe.Add(ref heapRef, path) = Unsafe.Add(ref heapRef, ppos); } - heap[path] = last; + Unsafe.Add(ref heapRef, path) = last; - int second = heap[0]; + int second = Unsafe.Add(ref heapRef, 0); // Create a new node father of first and second last = numNodes++; @@ -627,27 +667,27 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib while (path < heapLen) { - if (path + 1 < heapLen && values[heap[path]] > values[heap[path + 1]]) + if (path + 1 < heapLen && values[Unsafe.Add(ref heapRef, path)] > values[Unsafe.Add(ref heapRef, path + 1)]) { path++; } - heap[ppos] = heap[path]; + Unsafe.Add(ref heapRef, ppos) = Unsafe.Add(ref heapRef, path); ppos = path; path = (ppos * 2) + 1; } // Now propagate the new element down along path - while ((path = ppos) > 0 && values[heap[ppos = (path - 1) / 2]] > lastVal) + while ((path = ppos) > 0 && values[Unsafe.Add(ref heapRef, ppos = (path - 1) / 2)] > lastVal) { - heap[path] = heap[ppos]; + Unsafe.Add(ref heapRef, path) = Unsafe.Add(ref heapRef, ppos); } - heap[path] = last; + Unsafe.Add(ref heapRef, path) = last; } while (heapLen > 1); - if (heap[0] != (childs.Length / 2) - 1) + if (Unsafe.Add(ref heapRef, 0) != (childs.Length / 2) - 1) { DeflateThrowHelper.ThrowHeapViolated(); } @@ -659,12 +699,13 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib /// Get encoded length ///
/// Encoded length, the sum of frequencies * lengths + [MethodImpl(InliningOptions.ShortMethod)] public int GetEncodedLength() { int len = 0; - for (int i = 0; i < this.Freqs.Length; i++) + for (int i = 0; i < this.elementCount; i++) { - len += this.Freqs[i] * this.Length[i]; + len += this.Frequencies[i] * this.Length[i]; } return len; @@ -697,7 +738,7 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib min_count = 3; if (curlen != nextlen) { - blTree.Freqs[nextlen]++; + blTree.Frequencies[nextlen]++; count = 0; } } @@ -716,19 +757,19 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib if (count < min_count) { - blTree.Freqs[curlen] += (short)count; + blTree.Frequencies[curlen] += (short)count; } else if (curlen != 0) { - blTree.Freqs[Repeat3To6]++; + blTree.Frequencies[Repeat3To6]++; } else if (count <= 10) { - blTree.Freqs[Repeat3To10]++; + blTree.Frequencies[Repeat3To10]++; } else { - blTree.Freqs[Repeat11To138]++; + blTree.Frequencies[Repeat11To138]++; } } } @@ -805,7 +846,6 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib private void BuildLength(int[] children) { - this.Length = new byte[this.Freqs.Length]; int numNodes = children.Length / 2; int numLeafs = (numNodes + 1) / 2; int overflow = 0; @@ -894,6 +934,36 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib } } } + + public void Dispose() + { + this.Dispose(true); + GC.SuppressFinalize(this); + } + + private void Dispose(bool disposing) + { + if (!this.isDisposed) + { + if (disposing) + { + this.frequenciesMemoryHandle.Dispose(); + this.frequenciesMemoryOwner.Dispose(); + + this.lengthsMemoryHandle.Dispose(); + this.lengthsMemoryOwner.Dispose(); + + this.codesMemoryHandle.Dispose(); + this.codesMemoryOwner.Dispose(); + } + + this.frequenciesMemoryOwner = null; + this.lengthsMemoryOwner = null; + this.codesMemoryOwner = null; + + this.isDisposed = true; + } + } } } } diff --git a/src/ImageSharp/Formats/Png/Zlib/DeflaterPendingBuffer.cs b/src/ImageSharp/Formats/Png/Zlib/DeflaterPendingBuffer.cs index da4d890402..d4af8cb5a2 100644 --- a/src/ImageSharp/Formats/Png/Zlib/DeflaterPendingBuffer.cs +++ b/src/ImageSharp/Formats/Png/Zlib/DeflaterPendingBuffer.cs @@ -15,8 +15,8 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib { private readonly byte[] buffer; private readonly byte* pinnedBuffer; - private readonly IManagedByteBuffer managedBuffer; - private MemoryHandle handle; + private IManagedByteBuffer bufferMemoryOwner; + private MemoryHandle bufferMemoryHandle; private int start; private int end; @@ -29,10 +29,10 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib /// The memory allocator to use for buffer allocations. public DeflaterPendingBuffer(MemoryAllocator memoryAllocator) { - this.managedBuffer = memoryAllocator.AllocateManagedByteBuffer(DeflaterConstants.PENDING_BUF_SIZE); - this.buffer = this.managedBuffer.Array; - this.handle = this.managedBuffer.Memory.Pin(); - this.pinnedBuffer = (byte*)this.handle.Pointer; + this.bufferMemoryOwner = memoryAllocator.AllocateManagedByteBuffer(DeflaterConstants.PENDING_BUF_SIZE); + this.buffer = this.bufferMemoryOwner.Array; + this.bufferMemoryHandle = this.bufferMemoryOwner.Memory.Pin(); + this.pinnedBuffer = (byte*)this.bufferMemoryHandle.Pointer; } /// @@ -175,10 +175,12 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib { if (disposing) { - this.handle.Dispose(); - this.managedBuffer.Dispose(); + this.bufferMemoryHandle.Dispose(); + this.bufferMemoryOwner.Dispose(); } + this.bufferMemoryOwner = null; + this.isDisposed = true; } } From 23fae72391687860deebdd3b66d96a1f7a62cf4e Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 14 Nov 2019 23:53:19 +1100 Subject: [PATCH 69/75] Optimize huffman and make all internal --- src/ImageSharp/Formats/Png/Zlib/Crc32.cs | 6 +- .../Formats/Png/Zlib/DeflateThrowHelper.cs | 4 +- src/ImageSharp/Formats/Png/Zlib/Deflater.cs | 2 +- .../Formats/Png/Zlib/DeflaterEngine.cs | 87 ++-- .../Formats/Png/Zlib/DeflaterHuffman.cs | 343 ++++++------- .../Formats/Png/Zlib/DeflaterOutputStream.cs | 477 +++--------------- .../Formats/Png/Zlib/DeflaterPendingBuffer.cs | 19 +- src/ImageSharp/Formats/Png/Zlib/README.md | 7 +- .../Formats/Png/Zlib/ZlibDeflateStream.cs | 41 +- 9 files changed, 317 insertions(+), 669 deletions(-) diff --git a/src/ImageSharp/Formats/Png/Zlib/Crc32.cs b/src/ImageSharp/Formats/Png/Zlib/Crc32.cs index d1588c384f..77355e908c 100644 --- a/src/ImageSharp/Formats/Png/Zlib/Crc32.cs +++ b/src/ImageSharp/Formats/Png/Zlib/Crc32.cs @@ -1,8 +1,9 @@ -// 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; +using System.Runtime.InteropServices; namespace SixLabors.ImageSharp.Formats.Png.Zlib { @@ -141,9 +142,10 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib { this.crc ^= CrcSeed; + ref uint crcTableRef = ref MemoryMarshal.GetReference(CrcTable.AsSpan()); for (int i = 0; i < data.Length; i++) { - this.crc = CrcTable[(this.crc ^ data[i]) & 0xFF] ^ (this.crc >> 8); + this.crc = Unsafe.Add(ref crcTableRef, (int)((this.crc ^ data[i]) & 0xFF)) ^ (this.crc >> 8); } this.crc ^= CrcSeed; diff --git a/src/ImageSharp/Formats/Png/Zlib/DeflateThrowHelper.cs b/src/ImageSharp/Formats/Png/Zlib/DeflateThrowHelper.cs index d7198c4ee9..5f62b13c7f 100644 --- a/src/ImageSharp/Formats/Png/Zlib/DeflateThrowHelper.cs +++ b/src/ImageSharp/Formats/Png/Zlib/DeflateThrowHelper.cs @@ -27,9 +27,9 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib public static void ThrowOutOfRange(string name) => throw new ArgumentOutOfRangeException(name); [MethodImpl(InliningOptions.ColdPath)] - public static void ThrowFrequencyNotEmpty() => throw new InvalidOperationException("Huffman frequency entry non empty."); + public static void ThrowHeapViolated() => throw new InvalidOperationException("Huffman heap invariant violated."); [MethodImpl(InliningOptions.ColdPath)] - public static void ThrowHeapViolated() => throw new InvalidOperationException("Huffman heap invariant violated."); + public static void ThrowNoDeflate() => throw new ImageFormatException("Cannot deflate all input."); } } diff --git a/src/ImageSharp/Formats/Png/Zlib/Deflater.cs b/src/ImageSharp/Formats/Png/Zlib/Deflater.cs index d1560eb4b0..fb2538f8c8 100644 --- a/src/ImageSharp/Formats/Png/Zlib/Deflater.cs +++ b/src/ImageSharp/Formats/Png/Zlib/Deflater.cs @@ -11,7 +11,7 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib /// This class compresses input with the deflate algorithm described in RFC 1951. /// It has several compression levels and three different strategies described below. /// - public sealed class Deflater : IDisposable + internal sealed class Deflater : IDisposable { /// /// The best and slowest compression level. This tries to find very diff --git a/src/ImageSharp/Formats/Png/Zlib/DeflaterEngine.cs b/src/ImageSharp/Formats/Png/Zlib/DeflaterEngine.cs index 8f57f51f9f..327279e723 100644 --- a/src/ImageSharp/Formats/Png/Zlib/DeflaterEngine.cs +++ b/src/ImageSharp/Formats/Png/Zlib/DeflaterEngine.cs @@ -49,7 +49,7 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib /// Low level compression engine for deflate algorithm which uses a 32K sliding window /// with secondary compression from Huffman/Shannon-Fano codes. /// - public sealed unsafe class DeflaterEngine : IDisposable + internal sealed unsafe class DeflaterEngine : IDisposable { private const int TooFar = 4096; @@ -109,8 +109,8 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib /// Note that the array should really be unsigned short, so you need /// to and the values with 0xFFFF. ///
- private readonly IMemoryOwner headBuffer; - private MemoryHandle headBufferHandle; + private IMemoryOwner headMemoryOwner; + private MemoryHandle headMemoryHandle; private readonly Memory head; private readonly short* pinnedHeadPointer; @@ -121,17 +121,17 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib /// Note that the array should really be unsigned short, so you need /// to and the values with 0xFFFF. ///
- private readonly IMemoryOwner prevBuffer; - private MemoryHandle prevBufferHandle; + private IMemoryOwner prevMemoryOwner; + private MemoryHandle prevMemoryHandle; private readonly Memory prev; private readonly short* pinnedPrevPointer; /// /// This array contains the part of the uncompressed stream that - /// is of relevance. The current character is indexed by strstart. + /// is of relevance. The current character is indexed by strstart. /// - private readonly IManagedByteBuffer windowBuffer; - private MemoryHandle windowBufferHandle; + private IManagedByteBuffer windowMemoryOwner; + private MemoryHandle windowMemoryHandle; private readonly byte[] window; private readonly byte* pinnedWindowPointer; @@ -153,20 +153,20 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib // Create pinned pointers to the various buffers to allow indexing // without bounds checks. - this.windowBuffer = memoryAllocator.AllocateManagedByteBuffer(2 * DeflaterConstants.WSIZE); - this.window = this.windowBuffer.Array; - this.windowBufferHandle = this.windowBuffer.Memory.Pin(); - this.pinnedWindowPointer = (byte*)this.windowBufferHandle.Pointer; + this.windowMemoryOwner = memoryAllocator.AllocateManagedByteBuffer(2 * DeflaterConstants.WSIZE); + this.window = this.windowMemoryOwner.Array; + this.windowMemoryHandle = this.windowMemoryOwner.Memory.Pin(); + this.pinnedWindowPointer = (byte*)this.windowMemoryHandle.Pointer; - this.headBuffer = memoryAllocator.Allocate(DeflaterConstants.HASH_SIZE); - this.head = this.headBuffer.Memory; - this.headBufferHandle = this.headBuffer.Memory.Pin(); - this.pinnedHeadPointer = (short*)this.headBufferHandle.Pointer; + this.headMemoryOwner = memoryAllocator.Allocate(DeflaterConstants.HASH_SIZE); + this.head = this.headMemoryOwner.Memory; + this.headMemoryHandle = this.headMemoryOwner.Memory.Pin(); + this.pinnedHeadPointer = (short*)this.headMemoryHandle.Pointer; - this.prevBuffer = memoryAllocator.Allocate(DeflaterConstants.WSIZE); - this.prev = this.prevBuffer.Memory; - this.prevBufferHandle = this.prevBuffer.Memory.Pin(); - this.pinnedPrevPointer = (short*)this.prevBufferHandle.Pointer; + this.prevMemoryOwner = memoryAllocator.Allocate(DeflaterConstants.WSIZE); + this.prev = this.prevMemoryOwner.Memory; + this.prevMemoryHandle = this.prevMemoryOwner.Memory.Pin(); + this.pinnedPrevPointer = (short*)this.prevMemoryHandle.Pointer; // We start at index 1, to avoid an implementation deficiency, that // we cannot build a repeat pattern at index 0. @@ -377,8 +377,27 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib /// public void Dispose() { - // Do not change this code. Put cleanup code in Dispose(bool disposing) above. - this.Dispose(true); + if (!this.isDisposed) + { + this.huffman.Dispose(); + + this.windowMemoryHandle.Dispose(); + this.windowMemoryOwner.Dispose(); + + this.headMemoryHandle.Dispose(); + this.headMemoryOwner.Dispose(); + + this.prevMemoryHandle.Dispose(); + this.prevMemoryOwner.Dispose(); + + this.windowMemoryOwner = null; + this.headMemoryOwner = null; + this.prevMemoryOwner = null; + this.huffman = null; + + this.isDisposed = true; + } + GC.SuppressFinalize(this); } @@ -830,29 +849,5 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib return true; } - - private void Dispose(bool disposing) - { - if (!this.isDisposed) - { - if (disposing) - { - this.huffman.Dispose(); - - this.windowBufferHandle.Dispose(); - this.windowBuffer.Dispose(); - - this.headBufferHandle.Dispose(); - this.headBuffer.Dispose(); - - this.prevBufferHandle.Dispose(); - this.prevBuffer.Dispose(); - } - - this.huffman = null; - - this.isDisposed = true; - } - } } } diff --git a/src/ImageSharp/Formats/Png/Zlib/DeflaterHuffman.cs b/src/ImageSharp/Formats/Png/Zlib/DeflaterHuffman.cs index 50aa1c0956..7118703d08 100644 --- a/src/ImageSharp/Formats/Png/Zlib/DeflaterHuffman.cs +++ b/src/ImageSharp/Formats/Png/Zlib/DeflaterHuffman.cs @@ -12,7 +12,7 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib /// /// Performs Deflate Huffman encoding. /// - public sealed unsafe class DeflaterHuffman : IDisposable + internal sealed unsafe class DeflaterHuffman : IDisposable { private const int BufferSize = 1 << (DeflaterConstants.DEFAULT_MEM_LEVEL + 6); @@ -194,7 +194,7 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib int dc = Dcode(dist); this.distTree.WriteSymbol(pendingBuffer, dc); - bits = (dc / 2) - 1; + bits = (dc >> 1) - 1; if (bits > 0) { this.Pending.WriteBits(dist & ((1 << bits) - 1), bits); @@ -349,7 +349,7 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib this.distTree.Frequencies[dc]++; if (dc >= 4) { - this.extraBits += (dc / 2) - 1; + this.extraBits += (dc >> 1) - 1; } return this.IsFull(); @@ -443,9 +443,11 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib private readonly int elementCount; + private readonly MemoryAllocator memoryAllocator; + private IMemoryOwner codesMemoryOwner; private MemoryHandle codesMemoryHandle; - private short* codes; + private readonly short* codes; private IMemoryOwner frequenciesMemoryOwner; private MemoryHandle frequenciesMemoryHandle; @@ -455,8 +457,8 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib public Tree(MemoryAllocator memoryAllocator, int elements, int minCodes, int maxLength) { + this.memoryAllocator = memoryAllocator; this.elementCount = elements; - this.minNumCodes = minCodes; this.maxLength = maxLength; @@ -497,27 +499,6 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib public void WriteSymbol(DeflaterPendingBuffer pendingBuffer, int code) => pendingBuffer.WriteBits(this.codes[code] & 0xFFFF, this.Length[code]); - /// - /// Check that all frequencies are zero - /// - /// - /// At least one frequency is non-zero - /// - [MethodImpl(InliningOptions.ShortMethod)] - public void CheckEmpty() - { - bool empty = true; - for (int i = 0; i < this.elementCount; i++) - { - empty &= this.Frequencies[i] == 0; - } - - if (!empty) - { - DeflateThrowHelper.ThrowFrequencyNotEmpty(); - } - } - /// /// Set static codes and length /// @@ -569,130 +550,141 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib // // The binary tree is encoded in an array: 0 is root node and // the nodes 2*n+1, 2*n+2 are the child nodes of node n. - // Maxes out at 286 * 4 - Span heap = stackalloc int[numSymbols]; - ref int heapRef = ref MemoryMarshal.GetReference(heap); - - int heapLen = 0; - int maxCode = 0; - for (int n = 0; n < numSymbols; n++) - { - int freq = this.Frequencies[n]; - if (freq != 0) - { - // Insert n into heap - int pos = heapLen++; - int ppos; - while (pos > 0 && this.Frequencies[Unsafe.Add(ref heapRef, ppos = (pos - 1) / 2)] > freq) - { - Unsafe.Add(ref heapRef, pos) = Unsafe.Add(ref heapRef, ppos); - pos = ppos; - } - - heap[pos] = n; - - maxCode = n; - } - } - - // We could encode a single literal with 0 bits but then we - // don't see the literals. Therefore we force at least two - // literals to avoid this case. We don't care about order in - // this case, both literals get a 1 bit code. - while (heapLen < 2) - { - Unsafe.Add(ref heapRef, heapLen++) = maxCode < 2 ? ++maxCode : 0; - } - - this.NumCodes = Math.Max(maxCode + 1, this.minNumCodes); - - int numLeafs = heapLen; - int[] childs = new int[(4 * heapLen) - 2]; - int[] values = new int[(2 * heapLen) - 1]; - int numNodes = numLeafs; - for (int i = 0; i < heapLen; i++) - { - int node = Unsafe.Add(ref heapRef, i); - childs[2 * i] = node; - childs[(2 * i) + 1] = -1; - values[i] = this.Frequencies[node] << 8; - heap[i] = i; - } - - // Construct the Huffman tree by repeatedly combining the least two - // frequent nodes. - do + // Maxes out at 286 * 4 so too large for the stack. + using (IMemoryOwner heapMemoryOwner = this.memoryAllocator.Allocate(numSymbols)) { - int first = heap[0]; - int last = Unsafe.Add(ref heapRef, --heapLen); - - // Propagate the hole to the leafs of the heap - int ppos = 0; - int path = 1; + ref int heapRef = ref MemoryMarshal.GetReference(heapMemoryOwner.Memory.Span); - while (path < heapLen) + int heapLen = 0; + int maxCode = 0; + for (int n = 0; n < numSymbols; n++) { - if (path + 1 < heapLen && values[Unsafe.Add(ref heapRef, path)] > values[Unsafe.Add(ref heapRef, path + 1)]) + int freq = this.Frequencies[n]; + if (freq != 0) { - path++; + // Insert n into heap + int pos = heapLen++; + int ppos; + while (pos > 0 && this.Frequencies[Unsafe.Add(ref heapRef, ppos = (pos - 1) >> 1)] > freq) + { + Unsafe.Add(ref heapRef, pos) = Unsafe.Add(ref heapRef, ppos); + pos = ppos; + } + + Unsafe.Add(ref heapRef, pos) = n; + + maxCode = n; } - - Unsafe.Add(ref heapRef, ppos) = Unsafe.Add(ref heapRef, path); - ppos = path; - path = (path * 2) + 1; } - // Now propagate the last element down along path. Normally - // it shouldn't go too deep. - int lastVal = values[last]; - while ((path = ppos) > 0 && values[Unsafe.Add(ref heapRef, ppos = (path - 1) / 2)] > lastVal) + // We could encode a single literal with 0 bits but then we + // don't see the literals. Therefore we force at least two + // literals to avoid this case. We don't care about order in + // this case, both literals get a 1 bit code. + while (heapLen < 2) { - Unsafe.Add(ref heapRef, path) = Unsafe.Add(ref heapRef, ppos); + Unsafe.Add(ref heapRef, heapLen++) = maxCode < 2 ? ++maxCode : 0; } - Unsafe.Add(ref heapRef, path) = last; - - int second = Unsafe.Add(ref heapRef, 0); + this.NumCodes = Math.Max(maxCode + 1, this.minNumCodes); - // Create a new node father of first and second - last = numNodes++; - childs[2 * last] = first; - childs[(2 * last) + 1] = second; - int mindepth = Math.Min(values[first] & 0xFF, values[second] & 0xFF); - values[last] = lastVal = values[first] + values[second] - mindepth + 1; + int numLeafs = heapLen; + int childrenLength = (4 * heapLen) - 2; + using (IMemoryOwner childrenMemoryOwner = this.memoryAllocator.Allocate(childrenLength)) + using (IMemoryOwner valuesMemoryOwner = this.memoryAllocator.Allocate((2 * heapLen) - 1)) + { + ref int childrenRef = ref MemoryMarshal.GetReference(childrenMemoryOwner.Memory.Span); + ref int valuesRef = ref MemoryMarshal.GetReference(valuesMemoryOwner.Memory.Span); + int numNodes = numLeafs; - // Again, propagate the hole to the leafs - ppos = 0; - path = 1; + for (int i = 0; i < heapLen; i++) + { + int node = Unsafe.Add(ref heapRef, i); + int i2 = 2 * i; + Unsafe.Add(ref childrenRef, i2) = node; + Unsafe.Add(ref childrenRef, i2 + 1) = -1; + Unsafe.Add(ref valuesRef, i) = this.Frequencies[node] << 8; + Unsafe.Add(ref heapRef, i) = i; + } - while (path < heapLen) - { - if (path + 1 < heapLen && values[Unsafe.Add(ref heapRef, path)] > values[Unsafe.Add(ref heapRef, path + 1)]) + // Construct the Huffman tree by repeatedly combining the least two + // frequent nodes. + do { - path++; + int first = Unsafe.Add(ref heapRef, 0); + int last = Unsafe.Add(ref heapRef, --heapLen); + + // Propagate the hole to the leafs of the heap + int ppos = 0; + int path = 1; + + while (path < heapLen) + { + if (path + 1 < heapLen && Unsafe.Add(ref valuesRef, Unsafe.Add(ref heapRef, path)) > Unsafe.Add(ref valuesRef, Unsafe.Add(ref heapRef, path + 1))) + { + path++; + } + + Unsafe.Add(ref heapRef, ppos) = Unsafe.Add(ref heapRef, path); + ppos = path; + path = (path * 2) + 1; + } + + // Now propagate the last element down along path. Normally + // it shouldn't go too deep. + int lastVal = Unsafe.Add(ref valuesRef, last); + while ((path = ppos) > 0 + && Unsafe.Add(ref valuesRef, Unsafe.Add(ref heapRef, ppos = (path - 1) >> 1)) > lastVal) + { + Unsafe.Add(ref heapRef, path) = Unsafe.Add(ref heapRef, ppos); + } + + Unsafe.Add(ref heapRef, path) = last; + + int second = Unsafe.Add(ref heapRef, 0); + + // Create a new node father of first and second + last = numNodes++; + Unsafe.Add(ref childrenRef, 2 * last) = first; + Unsafe.Add(ref childrenRef, (2 * last) + 1) = second; + int mindepth = Math.Min(Unsafe.Add(ref valuesRef, first) & 0xFF, Unsafe.Add(ref valuesRef, second) & 0xFF); + Unsafe.Add(ref valuesRef, last) = lastVal = Unsafe.Add(ref valuesRef, first) + Unsafe.Add(ref valuesRef, second) - mindepth + 1; + + // Again, propagate the hole to the leafs + ppos = 0; + path = 1; + + while (path < heapLen) + { + if (path + 1 < heapLen + && Unsafe.Add(ref valuesRef, Unsafe.Add(ref heapRef, path)) > Unsafe.Add(ref valuesRef, Unsafe.Add(ref heapRef, path + 1))) + { + path++; + } + + Unsafe.Add(ref heapRef, ppos) = Unsafe.Add(ref heapRef, path); + ppos = path; + path = (ppos * 2) + 1; + } + + // Now propagate the new element down along path + while ((path = ppos) > 0 && Unsafe.Add(ref valuesRef, Unsafe.Add(ref heapRef, ppos = (path - 1) >> 1)) > lastVal) + { + Unsafe.Add(ref heapRef, path) = Unsafe.Add(ref heapRef, ppos); + } + + Unsafe.Add(ref heapRef, path) = last; } + while (heapLen > 1); - Unsafe.Add(ref heapRef, ppos) = Unsafe.Add(ref heapRef, path); - ppos = path; - path = (ppos * 2) + 1; - } + if (Unsafe.Add(ref heapRef, 0) != (childrenLength >> 1) - 1) + { + DeflateThrowHelper.ThrowHeapViolated(); + } - // Now propagate the new element down along path - while ((path = ppos) > 0 && values[Unsafe.Add(ref heapRef, ppos = (path - 1) / 2)] > lastVal) - { - Unsafe.Add(ref heapRef, path) = Unsafe.Add(ref heapRef, ppos); + this.BuildLength(childrenMemoryOwner.Memory.Span); } - - Unsafe.Add(ref heapRef, path) = last; } - while (heapLen > 1); - - if (Unsafe.Add(ref heapRef, 0) != (childs.Length / 2) - 1) - { - DeflateThrowHelper.ThrowHeapViolated(); - } - - this.BuildLength(childs); } /// @@ -717,10 +709,10 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib /// public void CalcBLFreq(Tree blTree) { - int max_count; /* max repeat count */ - int min_count; /* min repeat count */ - int count; /* repeat count of the current code */ - int curlen = -1; /* length of current code */ + int maxCount; // max repeat count + int minCount; // min repeat count + int count; // repeat count of the current code + int curLen = -1; // length of current code int i = 0; while (i < this.NumCodes) @@ -729,37 +721,37 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib int nextlen = this.Length[i]; if (nextlen == 0) { - max_count = 138; - min_count = 3; + maxCount = 138; + minCount = 3; } else { - max_count = 6; - min_count = 3; - if (curlen != nextlen) + maxCount = 6; + minCount = 3; + if (curLen != nextlen) { blTree.Frequencies[nextlen]++; count = 0; } } - curlen = nextlen; + curLen = nextlen; i++; - while (i < this.NumCodes && curlen == this.Length[i]) + while (i < this.NumCodes && curLen == this.Length[i]) { i++; - if (++count >= max_count) + if (++count >= maxCount) { break; } } - if (count < min_count) + if (count < minCount) { - blTree.Frequencies[curlen] += (short)count; + blTree.Frequencies[curLen] += (short)count; } - else if (curlen != 0) + else if (curLen != 0) { blTree.Frequencies[Repeat3To6]++; } @@ -781,10 +773,10 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib /// The tree to write. public void WriteTree(DeflaterPendingBuffer pendingBuffer, Tree bitLengthTree) { - int max_count; // max repeat count - int min_count; // min repeat count - int count; // repeat count of the current code - int curlen = -1; // length of current code + int maxCount; // max repeat count + int minCount; // min repeat count + int count; // repeat count of the current code + int curLen = -1; // length of current code int i = 0; while (i < this.NumCodes) @@ -793,40 +785,40 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib int nextlen = this.Length[i]; if (nextlen == 0) { - max_count = 138; - min_count = 3; + maxCount = 138; + minCount = 3; } else { - max_count = 6; - min_count = 3; - if (curlen != nextlen) + maxCount = 6; + minCount = 3; + if (curLen != nextlen) { bitLengthTree.WriteSymbol(pendingBuffer, nextlen); count = 0; } } - curlen = nextlen; + curLen = nextlen; i++; - while (i < this.NumCodes && curlen == this.Length[i]) + while (i < this.NumCodes && curLen == this.Length[i]) { i++; - if (++count >= max_count) + if (++count >= maxCount) { break; } } - if (count < min_count) + if (count < minCount) { while (count-- > 0) { - bitLengthTree.WriteSymbol(pendingBuffer, curlen); + bitLengthTree.WriteSymbol(pendingBuffer, curLen); } } - else if (curlen != 0) + else if (curLen != 0) { bitLengthTree.WriteSymbol(pendingBuffer, Repeat3To6); pendingBuffer.WriteBits(count - 3, 2); @@ -844,10 +836,10 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib } } - private void BuildLength(int[] children) + private void BuildLength(ReadOnlySpan children) { - int numNodes = children.Length / 2; - int numLeafs = (numNodes + 1) / 2; + int numNodes = children.Length >> 1; + int numLeafs = (numNodes + 1) >> 1; int overflow = 0; for (int i = 0; i < this.maxLength; i++) @@ -936,26 +928,17 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib } public void Dispose() - { - this.Dispose(true); - GC.SuppressFinalize(this); - } - - private void Dispose(bool disposing) { if (!this.isDisposed) { - if (disposing) - { - this.frequenciesMemoryHandle.Dispose(); - this.frequenciesMemoryOwner.Dispose(); + this.frequenciesMemoryHandle.Dispose(); + this.frequenciesMemoryOwner.Dispose(); - this.lengthsMemoryHandle.Dispose(); - this.lengthsMemoryOwner.Dispose(); + this.lengthsMemoryHandle.Dispose(); + this.lengthsMemoryOwner.Dispose(); - this.codesMemoryHandle.Dispose(); - this.codesMemoryOwner.Dispose(); - } + this.codesMemoryHandle.Dispose(); + this.codesMemoryOwner.Dispose(); this.frequenciesMemoryOwner = null; this.lengthsMemoryOwner = null; @@ -963,6 +946,8 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib this.isDisposed = true; } + + GC.SuppressFinalize(this); } } } diff --git a/src/ImageSharp/Formats/Png/Zlib/DeflaterOutputStream.cs b/src/ImageSharp/Formats/Png/Zlib/DeflaterOutputStream.cs index 837e8b7958..eb214aae2c 100644 --- a/src/ImageSharp/Formats/Png/Zlib/DeflaterOutputStream.cs +++ b/src/ImageSharp/Formats/Png/Zlib/DeflaterOutputStream.cs @@ -1,466 +1,155 @@ // 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.Text; +using SixLabors.Memory; namespace SixLabors.ImageSharp.Formats.Png.Zlib { /// /// A special stream deflating or compressing the bytes that are - /// written to it. It uses a Deflater to perform actual deflating.
- /// Authors of the original java version : Tom Tromey, Jochen Hoenicke + /// written to it. It uses a Deflater to perform actual deflating. ///
- public class DeflaterOutputStream : Stream + internal sealed class DeflaterOutputStream : Stream { - #region Constructors - /// - /// Creates a new DeflaterOutputStream with the given Deflater and - /// default buffer size. - /// - /// - /// the output stream where deflated output should be written. - /// - /// - /// the underlying deflater. - /// - public DeflaterOutputStream(Stream baseOutputStream, Deflater deflater) - : this(baseOutputStream, deflater, 512) - { - } + private const int BufferLength = 512; + private IManagedByteBuffer memoryOwner; + private readonly byte[] buffer; + private Deflater deflater; + private readonly Stream rawStream; + private bool isDisposed; /// - /// Creates a new DeflaterOutputStream with the given Deflater and - /// buffer size. + /// Initializes a new instance of the class. /// - /// - /// The output stream where deflated output is written. - /// - /// - /// The underlying deflater to use - /// - /// - /// The buffer size in bytes to use when deflating (minimum value 512) - /// - /// - /// bufsize is less than or equal to zero. - /// - /// - /// baseOutputStream does not support writing - /// - /// - /// deflater instance is null - /// - public DeflaterOutputStream(Stream baseOutputStream, Deflater deflater, int bufferSize) + /// The memory allocator to use for buffer allocations. + /// The output stream where deflated output is written. + /// The compression level. + public DeflaterOutputStream(MemoryAllocator memoryAllocator, Stream rawStream, int compressionLevel) { - if (baseOutputStream == null) - { - throw new ArgumentNullException(nameof(baseOutputStream)); - } - - if (baseOutputStream.CanWrite == false) - { - throw new ArgumentException("Must support writing", nameof(baseOutputStream)); - } - - if (bufferSize < 512) - { - throw new ArgumentOutOfRangeException(nameof(bufferSize)); - } - - baseOutputStream_ = baseOutputStream; - buffer_ = new byte[bufferSize]; - deflater_ = deflater ?? throw new ArgumentNullException(nameof(deflater)); + this.rawStream = rawStream; + this.memoryOwner = memoryAllocator.AllocateManagedByteBuffer(BufferLength); + this.buffer = this.memoryOwner.Array; + this.deflater = new Deflater(memoryAllocator, compressionLevel); } - #endregion Constructors + /// + public override bool CanRead => false; - #region Public API + /// + public override bool CanSeek => false; - /// - /// Finishes the stream by calling finish() on the deflater. - /// - /// - /// Not all input is deflated - /// - public virtual void Finish() - { - deflater_.Finish(); - while (!deflater_.IsFinished) - { - int len = deflater_.Deflate(buffer_, 0, buffer_.Length); - if (len <= 0) - { - break; - } + /// + public override bool CanWrite => this.rawStream.CanWrite; - baseOutputStream_.Write(buffer_, 0, len); - } + /// + public override long Length => this.rawStream.Length; - if (!deflater_.IsFinished) + /// + public override long Position + { + get { - throw new ImageFormatException("Can't deflate all input?"); + return this.rawStream.Position; } - baseOutputStream_.Flush(); - } - - /// - /// Gets or sets a flag indicating ownership of underlying stream. - /// When the flag is true will close the underlying stream also. - /// - /// The default value is true. - public bool IsStreamOwner { get; set; } = true; - - /// - /// Allows client to determine if an entry can be patched after its added - /// - public bool CanPatchEntries - { - get + set { - return baseOutputStream_.CanSeek; + throw new NotSupportedException(); } } - #endregion Public API - - //#region Encryption - - //private string password; - - //private ICryptoTransform cryptoTransform_; + /// + public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException(); - ///// - ///// Returns the 10 byte AUTH CODE to be appended immediately following the AES data stream. - ///// - //protected byte[] AESAuthCode; + /// + public override void SetLength(long value) => throw new NotSupportedException(); - ///// - ///// Get/set the password used for encryption. - ///// - ///// When set to null or if the password is empty no encryption is performed - //public string Password - //{ - // get - // { - // return password; - // } - // set - // { - // if ((value != null) && (value.Length == 0)) - // { - // password = null; - // } - // else - // { - // password = value; - // } - // } - //} + /// + public override int ReadByte() => throw new NotSupportedException(); - ///// - ///// Encrypt a block of data - ///// - ///// - ///// Data to encrypt. NOTE the original contents of the buffer are lost - ///// - ///// - ///// Offset of first byte in buffer to encrypt - ///// - ///// - ///// Number of bytes in buffer to encrypt - ///// - //protected void EncryptBlock(byte[] buffer, int offset, int length) - //{ - // cryptoTransform_.TransformBlock(buffer, 0, length, buffer, 0); - //} + /// + public override int Read(byte[] buffer, int offset, int count) => throw new NotSupportedException(); - ///// - ///// Initializes encryption keys based on given . - ///// - ///// The password. - //protected void InitializePassword(string password) - //{ - // var pkManaged = new PkzipClassicManaged(); - // byte[] key = PkzipClassic.GenerateKeys(ZipStrings.ConvertToArray(password)); - // cryptoTransform_ = pkManaged.CreateEncryptor(key, null); - //} - - ///// - ///// Initializes encryption keys based on given password. - ///// - //protected void InitializeAESPassword(ZipEntry entry, string rawPassword, - // out byte[] salt, out byte[] pwdVerifier) - //{ - // salt = new byte[entry.AESSaltLen]; - // // Salt needs to be cryptographically random, and unique per file - // if (_aesRnd == null) - // _aesRnd = RandomNumberGenerator.Create(); - // _aesRnd.GetBytes(salt); - // int blockSize = entry.AESKeySize / 8; // bits to bytes - - // cryptoTransform_ = new ZipAESTransform(rawPassword, salt, blockSize, true); - // pwdVerifier = ((ZipAESTransform)cryptoTransform_).PwdVerifier; - //} - - //#endregion Encryption - - #region Deflation Support + /// + public override void Flush() + { + this.deflater.Flush(); + this.Deflate(true); + this.rawStream.Flush(); + } - /// - /// Deflates everything in the input buffers. This will call - /// def.deflate() until all bytes from the input buffers - /// are processed. - /// - protected void Deflate() + /// + public override void Write(byte[] buffer, int offset, int count) { - Deflate(false); + this.deflater.SetInput(buffer, offset, count); + this.Deflate(); } + private void Deflate() => this.Deflate(false); + private void Deflate(bool flushing) { - while (flushing || !deflater_.IsNeedingInput) + while (flushing || !this.deflater.IsNeedingInput) { - int deflateCount = deflater_.Deflate(buffer_, 0, buffer_.Length); + int deflateCount = this.deflater.Deflate(this.buffer, 0, BufferLength); if (deflateCount <= 0) { break; } - //if (cryptoTransform_ != null) - //{ - // EncryptBlock(buffer_, 0, deflateCount); - //} - baseOutputStream_.Write(buffer_, 0, deflateCount); + this.rawStream.Write(this.buffer, 0, deflateCount); } - if (!deflater_.IsNeedingInput) + if (!this.deflater.IsNeedingInput) { - throw new ImageFormatException("DeflaterOutputStream can't deflate all input?"); + DeflateThrowHelper.ThrowNoDeflate(); } } - #endregion Deflation Support - - #region Stream Overrides - - /// - /// Gets value indicating stream can be read from - /// - public override bool CanRead + private void Finish() { - get + this.deflater.Finish(); + while (!this.deflater.IsFinished) { - return false; - } - } + int len = this.deflater.Deflate(this.buffer, 0, BufferLength); + if (len <= 0) + { + break; + } - /// - /// Gets a value indicating if seeking is supported for this stream - /// This property always returns false - /// - public override bool CanSeek - { - get - { - return false; + this.rawStream.Write(this.buffer, 0, len); } - } - /// - /// Get value indicating if this stream supports writing - /// - public override bool CanWrite - { - get + if (!this.deflater.IsFinished) { - return baseOutputStream_.CanWrite; + DeflateThrowHelper.ThrowNoDeflate(); } - } - /// - /// Get current length of stream - /// - public override long Length - { - get - { - return baseOutputStream_.Length; - } + this.rawStream.Flush(); } - /// - /// Gets the current position within the stream. - /// - /// Any attempt to set position - public override long Position + /// + protected override void Dispose(bool disposing) { - get - { - return baseOutputStream_.Position; - } - set + if (this.isDisposed) { - throw new NotSupportedException("Position property not supported"); + return; } - } - - /// - /// Sets the current position of this stream to the given value. Not supported by this class! - /// - /// The offset relative to the to seek. - /// The to seek from. - /// The new position in the stream. - /// Any access - public override long Seek(long offset, SeekOrigin origin) - { - throw new NotSupportedException("DeflaterOutputStream Seek not supported"); - } - - /// - /// Sets the length of this stream to the given value. Not supported by this class! - /// - /// The new stream length. - /// Any access - public override void SetLength(long value) - { - throw new NotSupportedException("DeflaterOutputStream SetLength not supported"); - } - - /// - /// Read a byte from stream advancing position by one - /// - /// The byte read cast to an int. THe value is -1 if at the end of the stream. - /// Any access - public override int ReadByte() - { - throw new NotSupportedException("DeflaterOutputStream ReadByte not supported"); - } - - /// - /// Read a block of bytes from stream - /// - /// The buffer to store read data in. - /// The offset to start storing at. - /// The maximum number of bytes to read. - /// The actual number of bytes read. Zero if end of stream is detected. - /// Any access - public override int Read(byte[] buffer, int offset, int count) - { - throw new NotSupportedException("DeflaterOutputStream Read not supported"); - } - - /// - /// Flushes the stream by calling Flush on the deflater and then - /// on the underlying stream. This ensures that all bytes are flushed. - /// - public override void Flush() - { - deflater_.Flush(); - Deflate(true); - baseOutputStream_.Flush(); - } - /// - /// Calls and closes the underlying - /// stream when is true. - /// - protected override void Dispose(bool disposing) - { - if (!isClosed_) + if (disposing) { - isClosed_ = true; - - try - { - Finish(); - } - finally - { - if (IsStreamOwner) - { - baseOutputStream_.Dispose(); - } - } + this.Finish(); + this.deflater.Dispose(); + this.memoryOwner.Dispose(); } - } - - ///// - ///// Get the Auth code for AES encrypted entries - ///// - //protected void GetAuthCodeIfAES() - //{ - // if (cryptoTransform_ is ZipAESTransform) - // { - // AESAuthCode = ((ZipAESTransform)cryptoTransform_).GetAuthCode(); - // } - //} - /// - /// Writes a single byte to the compressed output stream. - /// - /// - /// The byte value. - /// - public override void WriteByte(byte value) - { - byte[] b = new byte[1]; - b[0] = value; - Write(b, 0, 1); - } - - /// - /// Writes bytes from an array to the compressed stream. - /// - /// - /// The byte array - /// - /// - /// The offset into the byte array where to start. - /// - /// - /// The number of bytes to write. - /// - public override void Write(byte[] buffer, int offset, int count) - { - deflater_.SetInput(buffer, offset, count); - Deflate(); + this.deflater = null; + this.memoryOwner = null; + this.isDisposed = true; + base.Dispose(disposing); } - - #endregion Stream Overrides - - #region Instance Fields - - /// - /// This buffer is used temporarily to retrieve the bytes from the - /// deflater and write them to the underlying output stream. - /// - private byte[] buffer_; - - /// - /// The deflater which is used to deflate the stream. - /// - protected Deflater deflater_; - - /// - /// Base stream the deflater depends on. - /// - protected Stream baseOutputStream_; - - private bool isClosed_; - - #endregion Instance Fields - - #region Static Fields - - // Static to help ensure that multiple files within a zip will get different random salt - //private static RandomNumberGenerator _aesRnd = RandomNumberGenerator.Create(); - - #endregion Static Fields } } diff --git a/src/ImageSharp/Formats/Png/Zlib/DeflaterPendingBuffer.cs b/src/ImageSharp/Formats/Png/Zlib/DeflaterPendingBuffer.cs index d4af8cb5a2..a5f00f03ca 100644 --- a/src/ImageSharp/Formats/Png/Zlib/DeflaterPendingBuffer.cs +++ b/src/ImageSharp/Formats/Png/Zlib/DeflaterPendingBuffer.cs @@ -11,7 +11,7 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib /// /// Stores pending data for writing data to the Deflater. /// - public sealed unsafe class DeflaterPendingBuffer : IDisposable + internal sealed unsafe class DeflaterPendingBuffer : IDisposable { private readonly byte[] buffer; private readonly byte* pinnedBuffer; @@ -164,25 +164,16 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib /// public void Dispose() - { - this.Dispose(true); - GC.SuppressFinalize(this); - } - - private void Dispose(bool disposing) { if (!this.isDisposed) { - if (disposing) - { - this.bufferMemoryHandle.Dispose(); - this.bufferMemoryOwner.Dispose(); - } - + this.bufferMemoryHandle.Dispose(); + this.bufferMemoryOwner.Dispose(); this.bufferMemoryOwner = null; - this.isDisposed = true; } + + GC.SuppressFinalize(this); } } } diff --git a/src/ImageSharp/Formats/Png/Zlib/README.md b/src/ImageSharp/Formats/Png/Zlib/README.md index c297a91d5e..59f75d05f6 100644 --- a/src/ImageSharp/Formats/Png/Zlib/README.md +++ b/src/ImageSharp/Formats/Png/Zlib/README.md @@ -1,2 +1,5 @@ -Adler32.cs and Crc32.cs have been copied from -https://github.com/ygrenier/SharpZipLib.Portable +Deflatestream implementation adapted from + +https://github.com/icsharpcode/SharpZipLib + +LIcensed under MIT diff --git a/src/ImageSharp/Formats/Png/Zlib/ZlibDeflateStream.cs b/src/ImageSharp/Formats/Png/Zlib/ZlibDeflateStream.cs index 5724e027d2..36bacc5ecd 100644 --- a/src/ImageSharp/Formats/Png/Zlib/ZlibDeflateStream.cs +++ b/src/ImageSharp/Formats/Png/Zlib/ZlibDeflateStream.cs @@ -41,8 +41,6 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib // private DeflateStream deflateStream; private DeflaterOutputStream deflateStream; - private Deflater deflater; - /// /// Initializes a new instance of the class. /// @@ -92,21 +90,7 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib this.rawStream.WriteByte(Cmf); this.rawStream.WriteByte((byte)flg); - // Initialize the deflate Stream. - // CompressionLevel level = CompressionLevel.Optimal; - // - // if (compressionLevel >= 1 && compressionLevel <= 5) - // { - // level = CompressionLevel.Fastest; - // } - // else if (compressionLevel == 0) - // { - // level = CompressionLevel.NoCompression; - // } - this.deflater = new Deflater(memoryAllocator, compressionLevel); - this.deflateStream = new DeflaterOutputStream(this.rawStream, this.deflater) { IsStreamOwner = false }; - - // this.deflateStream = new DeflateStream(this.rawStream, level, true); + this.deflateStream = new DeflaterOutputStream(memoryAllocator, this.rawStream, compressionLevel); } /// @@ -116,16 +100,23 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib public override bool CanSeek => false; /// - public override bool CanWrite => true; + public override bool CanWrite => this.rawStream.CanWrite; /// - public override long Length => throw new NotSupportedException(); + public override long Length => this.rawStream.Length; /// public override long Position { - get => throw new NotSupportedException(); - set => throw new NotSupportedException(); + get + { + return this.rawStream.Position; + } + + set + { + throw new NotSupportedException(); + } } /// @@ -174,10 +165,6 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib { this.deflateStream.Dispose(); this.deflateStream = null; - - // TODO: Remove temporal coupling here. - this.deflater.Dispose(); - this.deflater = null; } else { @@ -195,10 +182,6 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib } base.Dispose(disposing); - - // Call the appropriate methods to clean up - // unmanaged resources here. - // Note disposing is done. this.isDisposed = true; } } From 74fd3cb6d89c190fd869986955692473c27750c9 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Tue, 19 Nov 2019 13:53:35 +1100 Subject: [PATCH 70/75] Minor perf tweaks. --- src/ImageSharp/Formats/Png/Zlib/Adler32.cs | 5 +-- .../Formats/Png/Zlib/DeflaterEngine.cs | 36 +++++++++++-------- 2 files changed, 24 insertions(+), 17 deletions(-) diff --git a/src/ImageSharp/Formats/Png/Zlib/Adler32.cs b/src/ImageSharp/Formats/Png/Zlib/Adler32.cs index f6f6edd124..c4dc82a4dc 100644 --- a/src/ImageSharp/Formats/Png/Zlib/Adler32.cs +++ b/src/ImageSharp/Formats/Png/Zlib/Adler32.cs @@ -3,6 +3,7 @@ using System; using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; namespace SixLabors.ImageSharp.Formats.Png.Zlib { @@ -112,7 +113,7 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Update(ReadOnlySpan data) { - // (By Per Bothner) + ref byte dataRef = ref MemoryMarshal.GetReference(data); uint s1 = this.checksum & 0xFFFF; uint s2 = this.checksum >> 16; @@ -133,7 +134,7 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib count -= n; while (--n >= 0) { - s1 += (uint)(data[offset++] & 0xff); + s1 += Unsafe.Add(ref dataRef, offset++); s2 += s1; } diff --git a/src/ImageSharp/Formats/Png/Zlib/DeflaterEngine.cs b/src/ImageSharp/Formats/Png/Zlib/DeflaterEngine.cs index 327279e723..0163eec0b7 100644 --- a/src/ImageSharp/Formats/Png/Zlib/DeflaterEngine.cs +++ b/src/ImageSharp/Formats/Png/Zlib/DeflaterEngine.cs @@ -476,19 +476,22 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib int chainLength = this.maxChain; int niceLength = Math.Min(this.niceLength, this.lookahead); + int matchStrt = this.matchStart; this.matchLen = Math.Max(this.matchLen, DeflaterConstants.MIN_MATCH - 1); + int matchLength = this.matchLen; - if (scan + this.matchLen > scanMax) + if (scan + matchLength > scanMax) { return false; } byte* pinnedWindow = this.pinnedWindowPointer; - byte scan_end1 = pinnedWindow[scan + this.matchLen - 1]; - byte scan_end = pinnedWindow[scan + this.matchLen]; + int scanStart = this.strstart; + byte scanEnd1 = pinnedWindow[scan + matchLength - 1]; + byte scanEnd = pinnedWindow[scan + matchLength]; // Do not waste too much time if we already have a good match: - if (this.matchLen >= this.goodLength) + if (matchLength >= this.goodLength) { chainLength >>= 2; } @@ -497,10 +500,10 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib do { match = curMatch; - scan = this.strstart; + scan = scanStart; - if (pinnedWindow[match + this.matchLen] != scan_end - || pinnedWindow[match + this.matchLen - 1] != scan_end1 + if (pinnedWindow[match + matchLength] != scanEnd + || pinnedWindow[match + matchLength - 1] != scanEnd1 || pinnedWindow[match] != pinnedWindow[scan] || pinnedWindow[++match] != pinnedWindow[++scan]) { @@ -511,7 +514,8 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib // scanMax - scan is the maximum number of bytes we can compare. // below we compare 8 bytes at a time, so first we compare // (scanMax - scan) % 8 bytes, so the remainder is a multiple of 8 - switch ((scanMax - scan) % 8) + // n & (8 - 1) == n % 8. + switch ((scanMax - scan) & 7) { case 1: if (pinnedWindow[++scan] == pinnedWindow[++match]) @@ -616,23 +620,25 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib && pinnedWindow[++scan] == pinnedWindow[++match]); } - if (scan - this.strstart > this.matchLen) + if (scan - scanStart > matchLength) { - this.matchStart = curMatch; - this.matchLen = scan - this.strstart; + matchStrt = curMatch; + matchLength = scan - scanStart; - if (this.matchLen >= niceLength) + if (matchLength >= niceLength) { break; } - scan_end1 = pinnedWindow[scan - 1]; - scan_end = pinnedWindow[scan]; + scanEnd1 = pinnedWindow[scan - 1]; + scanEnd = pinnedWindow[scan]; } } while ((curMatch = pinnedPrev[curMatch & DeflaterConstants.WMASK] & 0xFFFF) > limit && --chainLength != 0); - return this.matchLen >= DeflaterConstants.MIN_MATCH; + this.matchStart = matchStrt; + this.matchLen = matchLength; + return matchLength >= DeflaterConstants.MIN_MATCH; } private bool DeflateStored(bool flush, bool finish) From 9e232fe57a4e43b7e1c64de83a60680c322125d3 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Tue, 19 Nov 2019 14:29:28 +1100 Subject: [PATCH 71/75] Improve code coverage. --- src/ImageSharp/Formats/Png/Zlib/Deflater.cs | 14 ++---- .../Formats/Png/Zlib/DeflaterHuffman.cs | 47 ++++++++----------- .../Formats/Png/Zlib/DeflaterOutputStream.cs | 24 +++++----- .../Formats/Png/Zlib/ZlibDeflateStream.cs | 34 +++----------- .../Formats/Png/PngEncoderTests.cs | 2 +- 5 files changed, 41 insertions(+), 80 deletions(-) diff --git a/src/ImageSharp/Formats/Png/Zlib/Deflater.cs b/src/ImageSharp/Formats/Png/Zlib/Deflater.cs index fb2538f8c8..6c4ea44d1d 100644 --- a/src/ImageSharp/Formats/Png/Zlib/Deflater.cs +++ b/src/ImageSharp/Formats/Png/Zlib/Deflater.cs @@ -281,23 +281,15 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib /// public void Dispose() - { - this.Dispose(true); - GC.SuppressFinalize(this); - } - - private void Dispose(bool disposing) { if (!this.isDisposed) { - if (disposing) - { - this.engine.Dispose(); - } - + this.engine.Dispose(); this.engine = null; this.isDisposed = true; } + + GC.SuppressFinalize(this); } } } diff --git a/src/ImageSharp/Formats/Png/Zlib/DeflaterHuffman.cs b/src/ImageSharp/Formats/Png/Zlib/DeflaterHuffman.cs index 7118703d08..003e4fbb71 100644 --- a/src/ImageSharp/Formats/Png/Zlib/DeflaterHuffman.cs +++ b/src/ImageSharp/Formats/Png/Zlib/DeflaterHuffman.cs @@ -372,7 +372,25 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib /// public void Dispose() { - this.Dispose(true); + if (!this.isDisposed) + { + this.Pending.Dispose(); + this.distanceBufferHandle.Dispose(); + this.distanceManagedBuffer.Dispose(); + this.literalBufferHandle.Dispose(); + this.literalManagedBuffer.Dispose(); + + this.literalTree.Dispose(); + this.blTree.Dispose(); + this.distTree.Dispose(); + + this.Pending = null; + this.literalTree = null; + this.blTree = null; + this.distTree = null; + this.isDisposed = true; + } + GC.SuppressFinalize(this); } @@ -407,33 +425,6 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib return code + distance; } - private void Dispose(bool disposing) - { - if (!this.isDisposed) - { - if (disposing) - { - this.Pending.Dispose(); - this.distanceBufferHandle.Dispose(); - this.distanceManagedBuffer.Dispose(); - this.literalBufferHandle.Dispose(); - this.literalManagedBuffer.Dispose(); - - this.literalTree.Dispose(); - this.blTree.Dispose(); - this.distTree.Dispose(); - } - - this.Pending = null; - - this.literalTree = null; - this.blTree = null; - this.distTree = null; - - this.isDisposed = true; - } - } - private sealed class Tree : IDisposable { private readonly int minNumCodes; diff --git a/src/ImageSharp/Formats/Png/Zlib/DeflaterOutputStream.cs b/src/ImageSharp/Formats/Png/Zlib/DeflaterOutputStream.cs index eb214aae2c..9eeb12cb08 100644 --- a/src/ImageSharp/Formats/Png/Zlib/DeflaterOutputStream.cs +++ b/src/ImageSharp/Formats/Png/Zlib/DeflaterOutputStream.cs @@ -134,22 +134,20 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib /// protected override void Dispose(bool disposing) { - if (this.isDisposed) + if (!this.isDisposed) { - return; - } + if (disposing) + { + this.Finish(); + this.deflater.Dispose(); + this.memoryOwner.Dispose(); + } - if (disposing) - { - this.Finish(); - this.deflater.Dispose(); - this.memoryOwner.Dispose(); + this.deflater = null; + this.memoryOwner = null; + this.isDisposed = true; + base.Dispose(disposing); } - - this.deflater = null; - this.memoryOwner = null; - this.isDisposed = true; - base.Dispose(disposing); } } } diff --git a/src/ImageSharp/Formats/Png/Zlib/ZlibDeflateStream.cs b/src/ImageSharp/Formats/Png/Zlib/ZlibDeflateStream.cs index 36bacc5ecd..3c52d306f9 100644 --- a/src/ImageSharp/Formats/Png/Zlib/ZlibDeflateStream.cs +++ b/src/ImageSharp/Formats/Png/Zlib/ZlibDeflateStream.cs @@ -120,28 +120,16 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib } /// - public override void Flush() - { - this.deflateStream?.Flush(); - } + public override void Flush() => this.deflateStream.Flush(); /// - public override int Read(byte[] buffer, int offset, int count) - { - throw new NotSupportedException(); - } + public override int Read(byte[] buffer, int offset, int count) => throw new NotSupportedException(); /// - public override long Seek(long offset, SeekOrigin origin) - { - throw new NotSupportedException(); - } + public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException(); /// - public override void SetLength(long value) - { - throw new NotSupportedException(); - } + public override void SetLength(long value) => throw new NotSupportedException(); /// public override void Write(byte[] buffer, int offset, int count) @@ -161,17 +149,7 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib if (disposing) { // dispose managed resources - if (this.deflateStream != null) - { - this.deflateStream.Dispose(); - this.deflateStream = null; - } - else - { - // Hack: empty input? - this.rawStream.WriteByte(3); - this.rawStream.WriteByte(0); - } + this.deflateStream.Dispose(); // Add the crc uint crc = (uint)this.adler32.Value; @@ -181,6 +159,8 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib this.rawStream.WriteByte((byte)(crc & 0xFF)); } + this.deflateStream = null; + base.Dispose(disposing); this.isDisposed = true; } diff --git a/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs index 2584391bb7..8a0cdbfbaf 100644 --- a/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs @@ -63,7 +63,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Png ///
public static readonly TheoryData CompressionLevels = new TheoryData { - 1, 2, 3, 4, 5, 6, 7, 8, 9 + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 }; public static readonly TheoryData PaletteSizes = new TheoryData From 98f802f617b7b9baca21e32334200227167224a5 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Wed, 20 Nov 2019 14:59:42 +1100 Subject: [PATCH 72/75] Only read the crc of critical chunks. --- src/ImageSharp/Formats/Png/PngChunk.cs | 17 ++---- src/ImageSharp/Formats/Png/PngDecoderCore.cs | 59 ++++++++++++-------- src/ImageSharp/Formats/Png/PngThrowHelper.cs | 29 ++++++++++ 3 files changed, 69 insertions(+), 36 deletions(-) create mode 100644 src/ImageSharp/Formats/Png/PngThrowHelper.cs diff --git a/src/ImageSharp/Formats/Png/PngChunk.cs b/src/ImageSharp/Formats/Png/PngChunk.cs index c75f9465af..1fee4a8371 100644 --- a/src/ImageSharp/Formats/Png/PngChunk.cs +++ b/src/ImageSharp/Formats/Png/PngChunk.cs @@ -1,4 +1,4 @@ -// Copyright (c) Six Labors and contributors. +// Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. using SixLabors.Memory; @@ -10,12 +10,11 @@ namespace SixLabors.ImageSharp.Formats.Png ///
internal readonly struct PngChunk { - public PngChunk(int length, PngChunkType type, IManagedByteBuffer data = null, uint crc = 0) + public PngChunk(int length, PngChunkType type, IManagedByteBuffer data = null) { this.Length = length; this.Type = type; this.Data = data; - this.Crc = crc; } /// @@ -38,20 +37,12 @@ namespace SixLabors.ImageSharp.Formats.Png /// public IManagedByteBuffer Data { get; } - /// - /// Gets a CRC (Cyclic Redundancy Check) calculated on the preceding bytes in the chunk, - /// including the chunk type code and chunk data fields, but not including the length field. - /// The CRC is always present, even for chunks containing no data - /// - public uint Crc { get; } - /// /// Gets a value indicating whether the given chunk is critical to decoding /// public bool IsCritical => this.Type == PngChunkType.Header || this.Type == PngChunkType.Palette || - this.Type == PngChunkType.Data || - this.Type == PngChunkType.End; + this.Type == PngChunkType.Data; } -} \ No newline at end of file +} diff --git a/src/ImageSharp/Formats/Png/PngDecoderCore.cs b/src/ImageSharp/Formats/Png/PngDecoderCore.cs index 037f648f0a..19e8b62fed 100644 --- a/src/ImageSharp/Formats/Png/PngDecoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngDecoderCore.cs @@ -221,7 +221,7 @@ namespace SixLabors.ImageSharp.Formats.Png if (image is null) { - throw new ImageFormatException("PNG Image does not contain a data chunk"); + PngThrowHelper.ThrowNoData(); } return image; @@ -285,7 +285,7 @@ namespace SixLabors.ImageSharp.Formats.Png if (this.header.Width == 0 && this.header.Height == 0) { - throw new ImageFormatException("PNG Image does not contain a header chunk"); + PngThrowHelper.ThrowNoHeader(); } return new ImageInfo(new PixelTypeInfo(this.CalculateBitsPerPixel()), this.header.Width, this.header.Height, metadata); @@ -407,7 +407,8 @@ namespace SixLabors.ImageSharp.Formats.Png case PngColorType.RgbWithAlpha: return this.header.BitDepth * 4; default: - throw new NotSupportedException("Unsupported PNG color type"); + PngThrowHelper.ThrowNotSupportedColor(); + return -1; } } @@ -528,7 +529,8 @@ namespace SixLabors.ImageSharp.Formats.Png break; default: - throw new ImageFormatException("Unknown filter type."); + PngThrowHelper.ThrowUnknownFilter(); + break; } this.ProcessDefilteredScanline(scanlineSpan, image, pngMetadata); @@ -601,7 +603,8 @@ namespace SixLabors.ImageSharp.Formats.Png break; default: - throw new ImageFormatException("Unknown filter type."); + PngThrowHelper.ThrowUnknownFilter(); + break; } Span rowSpan = image.GetPixelRowSpan(this.currentRow); @@ -1119,13 +1122,9 @@ namespace SixLabors.ImageSharp.Formats.Png chunk = new PngChunk( length: length, type: type, - data: this.ReadChunkData(length), - crc: this.ReadChunkCrc()); + data: this.ReadChunkData(length)); - if (chunk.IsCritical) - { - this.ValidateChunk(chunk); - } + this.ValidateChunk(chunk); return true; } @@ -1136,6 +1135,11 @@ namespace SixLabors.ImageSharp.Formats.Png /// The . private void ValidateChunk(in PngChunk chunk) { + if (!chunk.IsCritical) + { + return; + } + Span chunkType = stackalloc byte[4]; BinaryPrimitives.WriteUInt32BigEndian(chunkType, (uint)chunk.Type); @@ -1144,25 +1148,26 @@ namespace SixLabors.ImageSharp.Formats.Png this.crc.Update(chunkType); this.crc.Update(chunk.Data.GetSpan()); - if (this.crc.Value != chunk.Crc) + uint crc = this.ReadChunkCrc(); + if (this.crc.Value != crc) { string chunkTypeName = Encoding.ASCII.GetString(chunkType); - - throw new ImageFormatException($"CRC Error. PNG {chunkTypeName} chunk is corrupt!"); + PngThrowHelper.ThrowInvalidChunkCrc(chunkTypeName); } } /// /// Reads the cycle redundancy chunk from the data. /// - /// - /// Thrown if the input stream is not valid or corrupt. - /// private uint ReadChunkCrc() { - return this.currentStream.Read(this.buffer, 0, 4) == 4 - ? BinaryPrimitives.ReadUInt32BigEndian(this.buffer) - : throw new ImageFormatException("Image stream is not valid!"); + uint crc = 0; + if (this.currentStream.Read(this.buffer, 0, 4) == 4) + { + crc = BinaryPrimitives.ReadUInt32BigEndian(this.buffer); + } + + return crc; } /// @@ -1197,9 +1202,17 @@ namespace SixLabors.ImageSharp.Formats.Png /// private PngChunkType ReadChunkType() { - return this.currentStream.Read(this.buffer, 0, 4) == 4 - ? (PngChunkType)BinaryPrimitives.ReadUInt32BigEndian(this.buffer) - : throw new ImageFormatException("Invalid PNG data."); + if (this.currentStream.Read(this.buffer, 0, 4) == 4) + { + return (PngChunkType)BinaryPrimitives.ReadUInt32BigEndian(this.buffer); + } + else + { + PngThrowHelper.ThrowInvalidChunkType(); + + // The IDE cannot detect the throw here. + return default; + } } /// diff --git a/src/ImageSharp/Formats/Png/PngThrowHelper.cs b/src/ImageSharp/Formats/Png/PngThrowHelper.cs new file mode 100644 index 0000000000..bc6093948b --- /dev/null +++ b/src/ImageSharp/Formats/Png/PngThrowHelper.cs @@ -0,0 +1,29 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Runtime.CompilerServices; + +namespace SixLabors.ImageSharp.Formats.Png +{ + /// + /// Cold path optimizations for throwing png format based exceptions. + /// + internal static class PngThrowHelper + { + [MethodImpl(InliningOptions.ColdPath)] + public static void ThrowNoHeader() => throw new ImageFormatException("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 ThrowInvalidChunkType() => throw new ImageFormatException("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 ThrowNotSupportedColor() => new NotSupportedException("Unsupported PNG color type"); + + public static void ThrowUnknownFilter() => throw new ImageFormatException("Unknown filter type."); + } +} From dc090c23ec3c0d46e5e332b5cf52a884eafe6285 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Wed, 20 Nov 2019 22:02:04 +1100 Subject: [PATCH 73/75] Fix #1047 --- src/ImageSharp/Formats/Png/PngDecoderCore.cs | 5 +++++ src/ImageSharp/Formats/Png/PngThrowHelper.cs | 4 ++++ .../Formats/Png/PngDecoderTests.Chunks.cs | 21 ------------------- .../Formats/Png/PngDecoderTests.cs | 15 ++++++++++--- tests/ImageSharp.Tests/TestImages.cs | 1 + tests/Images/Input/Png/issues/Issue_1047.png | 3 +++ 6 files changed, 25 insertions(+), 24 deletions(-) create mode 100644 tests/Images/Input/Png/issues/Issue_1047.png diff --git a/src/ImageSharp/Formats/Png/PngDecoderCore.cs b/src/ImageSharp/Formats/Png/PngDecoderCore.cs index 19e8b62fed..b24a5eabda 100644 --- a/src/ImageSharp/Formats/Png/PngDecoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngDecoderCore.cs @@ -1159,6 +1159,7 @@ namespace SixLabors.ImageSharp.Formats.Png /// /// Reads the cycle redundancy chunk from the data. /// + [MethodImpl(InliningOptions.ShortMethod)] private uint ReadChunkCrc() { uint crc = 0; @@ -1174,6 +1175,7 @@ namespace SixLabors.ImageSharp.Formats.Png /// Skips the chunk data and the cycle redundancy chunk read from the data. /// /// The image format chunk. + [MethodImpl(InliningOptions.ShortMethod)] private void SkipChunkDataAndCrc(in PngChunk chunk) { this.currentStream.Skip(chunk.Length); @@ -1184,6 +1186,7 @@ namespace SixLabors.ImageSharp.Formats.Png /// Reads the chunk data from the stream. /// /// The length of the chunk data to read. + [MethodImpl(InliningOptions.ShortMethod)] private IManagedByteBuffer ReadChunkData(int length) { // We rent the buffer here to return it afterwards in Decode() @@ -1200,6 +1203,7 @@ namespace SixLabors.ImageSharp.Formats.Png /// /// Thrown if the input stream is not valid. /// + [MethodImpl(InliningOptions.ShortMethod)] private PngChunkType ReadChunkType() { if (this.currentStream.Read(this.buffer, 0, 4) == 4) @@ -1221,6 +1225,7 @@ namespace SixLabors.ImageSharp.Formats.Png /// /// Whether the length was read. /// + [MethodImpl(InliningOptions.ShortMethod)] private bool TryReadChunkLength(out int result) { if (this.currentStream.Read(this.buffer, 0, 4) == 4) diff --git a/src/ImageSharp/Formats/Png/PngThrowHelper.cs b/src/ImageSharp/Formats/Png/PngThrowHelper.cs index bc6093948b..00b40c50b4 100644 --- a/src/ImageSharp/Formats/Png/PngThrowHelper.cs +++ b/src/ImageSharp/Formats/Png/PngThrowHelper.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. using System; +using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; namespace SixLabors.ImageSharp.Formats.Png @@ -17,13 +18,16 @@ namespace SixLabors.ImageSharp.Formats.Png [MethodImpl(InliningOptions.ColdPath)] public static void ThrowNoData() => throw new ImageFormatException("PNG Image does not contain a data chunk"); + [MethodImpl(InliningOptions.ColdPath)] public static void ThrowInvalidChunkType() => throw new ImageFormatException("Invalid PNG data."); [MethodImpl(InliningOptions.ColdPath)] public static void ThrowInvalidChunkCrc(string chunkTypeName) => throw new ImageFormatException($"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."); } } diff --git a/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.Chunks.cs b/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.Chunks.cs index e976d5a768..660d5b7246 100644 --- a/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.Chunks.cs +++ b/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.Chunks.cs @@ -54,7 +54,6 @@ namespace SixLabors.ImageSharp.Tests.Formats.Png [InlineData((uint)PngChunkType.Header)] // IHDR [InlineData((uint)PngChunkType.Palette)] // PLTE // [InlineData(PngChunkTypes.Data)] //TODO: Figure out how to test this - [InlineData((uint)PngChunkType.End)] // IEND public void Decode_IncorrectCRCForCriticalChunk_ExceptionIsThrown(uint chunkType) { string chunkName = GetChunkTypeName(chunkType); @@ -74,26 +73,6 @@ namespace SixLabors.ImageSharp.Tests.Formats.Png } } - [Theory] - [InlineData((uint)PngChunkType.Gamma)] // gAMA - [InlineData((uint)PngChunkType.Transparency)] // tRNS - [InlineData((uint)PngChunkType.Physical)] // pHYs: It's ok to test physical as we don't throw for duplicate chunks. - //[InlineData(PngChunkTypes.Text)] //TODO: Figure out how to test this - public void Decode_IncorrectCRCForNonCriticalChunk_ExceptionIsThrown(uint chunkType) - { - string chunkName = GetChunkTypeName(chunkType); - - using (var memStream = new MemoryStream()) - { - WriteHeaderChunk(memStream); - WriteChunk(memStream, chunkName); - WriteDataChunk(memStream); - - var decoder = new PngDecoder(); - decoder.Decode(null, memStream); - } - } - private static string GetChunkTypeName(uint value) { var data = new byte[4]; diff --git a/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs index e064c0fb06..1acb4fe2df 100644 --- a/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs @@ -4,11 +4,11 @@ // ReSharper disable InconsistentNaming using System.IO; - +using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.Formats.Png; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison; - +using SixLabors.ImageSharp.Tests.TestUtilities.ReferenceCodecs; using Xunit; namespace SixLabors.ImageSharp.Tests.Formats.Png @@ -42,6 +42,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Png TestImages.Png.Bad.ZlibOverflow, TestImages.Png.Bad.ZlibOverflow2, TestImages.Png.Bad.ZlibZtxtBadHeader, + TestImages.Png.Bad.Issue1047_BadEndChunk }; public static readonly string[] TestImages48Bpp = @@ -90,7 +91,15 @@ namespace SixLabors.ImageSharp.Tests.Formats.Png using (Image image = provider.GetImage(new PngDecoder())) { image.DebugSave(provider); - image.CompareToOriginal(provider, ImageComparer.Exact); + + if (provider.Utility.SourceFileOrDescription == TestImages.Png.Bad.Issue1047_BadEndChunk) + { + image.CompareToOriginal(provider, ImageComparer.Exact, (IImageDecoder)SystemDrawingReferenceDecoder.Instance); + } + else + { + image.CompareToOriginal(provider, ImageComparer.Exact); + } } } diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs index 1777498694..d19dbe8341 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/tests/ImageSharp.Tests/TestImages.cs @@ -99,6 +99,7 @@ namespace SixLabors.ImageSharp.Tests public const string ZlibOverflow = "Png/zlib-overflow.png"; public const string ZlibOverflow2 = "Png/zlib-overflow2.png"; public const string ZlibZtxtBadHeader = "Png/zlib-ztxt-bad-header.png"; + public const string Issue1047_BadEndChunk = "Png/issues/Issue_1047.png"; } public static readonly string[] All = diff --git a/tests/Images/Input/Png/issues/Issue_1047.png b/tests/Images/Input/Png/issues/Issue_1047.png new file mode 100644 index 0000000000..7d5a53a9e5 --- /dev/null +++ b/tests/Images/Input/Png/issues/Issue_1047.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4768d4bc3a4aaddb8e3e5cbff2beb706abacfd5448d658564f001811dafd320a +size 44638 From ff136cd8ad785cec869eed955fe9252c94242a6c Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Wed, 20 Nov 2019 23:02:16 +1100 Subject: [PATCH 74/75] Compare image on windows only for now. --- tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs index 1acb4fe2df..bdd84038e3 100644 --- a/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs @@ -92,9 +92,13 @@ namespace SixLabors.ImageSharp.Tests.Formats.Png { image.DebugSave(provider); + // We don't have another x-plat reference decoder that can be compared for this image. if (provider.Utility.SourceFileOrDescription == TestImages.Png.Bad.Issue1047_BadEndChunk) { - image.CompareToOriginal(provider, ImageComparer.Exact, (IImageDecoder)SystemDrawingReferenceDecoder.Instance); + if (TestEnvironment.IsWindows) + { + image.CompareToOriginal(provider, ImageComparer.Exact, (IImageDecoder)SystemDrawingReferenceDecoder.Instance); + } } else { From 8f75d8cbc9dde074a9d691a0cc332f4676d080ea Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Mon, 25 Nov 2019 15:20:16 +1100 Subject: [PATCH 75/75] Remove allocation and bounds checks --- .../Formats/Png/Zlib/DeflaterHuffman.cs | 76 ++++++++++--------- 1 file changed, 40 insertions(+), 36 deletions(-) diff --git a/src/ImageSharp/Formats/Png/Zlib/DeflaterHuffman.cs b/src/ImageSharp/Formats/Png/Zlib/DeflaterHuffman.cs index 003e4fbb71..96ff6b6576 100644 --- a/src/ImageSharp/Formats/Png/Zlib/DeflaterHuffman.cs +++ b/src/ImageSharp/Formats/Png/Zlib/DeflaterHuffman.cs @@ -829,38 +829,42 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib private void BuildLength(ReadOnlySpan children) { + byte* lengthPtr = this.Length; + ref int childrenRef = ref MemoryMarshal.GetReference(children); + ref int bitLengthCountsRef = ref MemoryMarshal.GetReference(this.bitLengthCounts); + + int maxLen = this.maxLength; int numNodes = children.Length >> 1; int numLeafs = (numNodes + 1) >> 1; int overflow = 0; - for (int i = 0; i < this.maxLength; i++) - { - this.bitLengthCounts[i] = 0; - } + Array.Clear(this.bitLengthCounts, 0, maxLen); // First calculate optimal bit lengths - int[] lengths = new int[numNodes]; - lengths[numNodes - 1] = 0; - - for (int i = numNodes - 1; i >= 0; i--) + using (IMemoryOwner lengthsMemoryOwner = this.memoryAllocator.Allocate(numNodes, AllocationOptions.Clean)) { - if (children[(2 * i) + 1] != -1) + ref int lengthsRef = ref MemoryMarshal.GetReference(lengthsMemoryOwner.Memory.Span); + + for (int i = numNodes - 1; i >= 0; i--) { - int bitLength = lengths[i] + 1; - if (bitLength > this.maxLength) + if (children[(2 * i) + 1] != -1) { - bitLength = this.maxLength; - overflow++; - } + int bitLength = Unsafe.Add(ref lengthsRef, i) + 1; + if (bitLength > maxLen) + { + bitLength = maxLen; + overflow++; + } - lengths[children[2 * i]] = lengths[children[(2 * i) + 1]] = bitLength; - } - else - { - // A leaf node - int bitLength = lengths[i]; - this.bitLengthCounts[bitLength - 1]++; - this.Length[children[2 * i]] = (byte)lengths[i]; + Unsafe.Add(ref lengthsRef, Unsafe.Add(ref childrenRef, 2 * i)) = Unsafe.Add(ref lengthsRef, Unsafe.Add(ref childrenRef, (2 * i) + 1)) = bitLength; + } + else + { + // A leaf node + int bitLength = Unsafe.Add(ref lengthsRef, i); + Unsafe.Add(ref bitLengthCountsRef, bitLength - 1)++; + lengthPtr[Unsafe.Add(ref childrenRef, 2 * i)] = (byte)Unsafe.Add(ref lengthsRef, i); + } } } @@ -869,11 +873,11 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib return; } - int incrBitLen = this.maxLength - 1; + int incrBitLen = maxLen - 1; do { // Find the first bit length which could increase: - while (this.bitLengthCounts[--incrBitLen] == 0) + while (Unsafe.Add(ref bitLengthCountsRef, --incrBitLen) == 0) { } @@ -881,18 +885,18 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib // number of overflow nodes. do { - this.bitLengthCounts[incrBitLen]--; - this.bitLengthCounts[++incrBitLen]++; - overflow -= 1 << (this.maxLength - 1 - incrBitLen); + Unsafe.Add(ref bitLengthCountsRef, incrBitLen)--; + Unsafe.Add(ref bitLengthCountsRef, ++incrBitLen)++; + overflow -= 1 << (maxLen - 1 - incrBitLen); } - while (overflow > 0 && incrBitLen < this.maxLength - 1); + while (overflow > 0 && incrBitLen < maxLen - 1); } while (overflow > 0); // We may have overshot above. Move some nodes from maxLength to // maxLength-1 in that case. - this.bitLengthCounts[this.maxLength - 1] += overflow; - this.bitLengthCounts[this.maxLength - 2] -= overflow; + Unsafe.Add(ref bitLengthCountsRef, maxLen - 1) += overflow; + Unsafe.Add(ref bitLengthCountsRef, maxLen - 2) -= overflow; // Now recompute all bit lengths, scanning in increasing // frequency. It is simpler to reconstruct all lengths instead of @@ -901,17 +905,17 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib // // The nodes were inserted with decreasing frequency into the childs // array. - int nodePtr = 2 * numLeafs; - for (int bits = this.maxLength; bits != 0; bits--) + int nodeIndex = 2 * numLeafs; + for (int bits = maxLen; bits != 0; bits--) { - int n = this.bitLengthCounts[bits - 1]; + int n = Unsafe.Add(ref bitLengthCountsRef, bits - 1); while (n > 0) { - int childPtr = 2 * children[nodePtr++]; - if (children[childPtr + 1] == -1) + int childIndex = 2 * Unsafe.Add(ref childrenRef, nodeIndex++); + if (Unsafe.Add(ref childrenRef, childIndex + 1) == -1) { // We found another leaf - this.Length[children[childPtr]] = (byte)bits; + lengthPtr[Unsafe.Add(ref childrenRef, childIndex)] = (byte)bits; n--; } }