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/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.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.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..a9df07ced3 100644 --- a/src/ImageSharp.Drawing/Processing/BrushApplicator.cs +++ b/src/ImageSharp.Drawing/Processing/BrushApplicator.cs @@ -1,68 +1,85 @@ -// Copyright (c) Six Labors and contributors. +// Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. using System; using System.Buffers; - using 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 graphics options. /// The target. - /// The options. - internal BrushApplicator(ImageFrame target, GraphicsOptions options) + internal BrushApplicator(Configuration configuration, GraphicsOptions options, ImageFrame target) { + 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) { @@ -71,8 +88,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++) { @@ -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..fbab3605d2 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,12 +47,14 @@ namespace SixLabors.ImageSharp.Processing /// public override BrushApplicator CreateApplicator( + Configuration configuration, + GraphicsOptions options, ImageFrame source, - RectangleF region, - GraphicsOptions options) => + RectangleF region) => new RadialGradientBrushApplicator( - source, + configuration, options, + source, this.center, this.referenceAxisEnd, this.axisRatio, @@ -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 graphics options. + /// 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. public RadialGradientBrushApplicator( - ImageFrame target, + Configuration configuration, GraphicsOptions options, + ImageFrame target, PointF center, PointF referenceAxisEnd, float axisRatio, ColorStop[] colorStops, GradientRepetitionMode repetitionMode) - : base(target, options, colorStops, repetitionMode) + : base(configuration, options, target, colorStops, repetitionMode) { 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/Extensions/DrawImageExtensions.cs b/src/ImageSharp.Drawing/Processing/Extensions/DrawImageExtensions.cs index 981cf1bef4..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, - GraphicsOptions.Default.ColorBlendingMode, - GraphicsOptions.Default.AlphaCompositionMode, - opacity)); + image, + Point.Empty, + options.ColorBlendingMode, + options.AlphaCompositionMode, + opacity)); + } /// /// Draws the given image together with the current one by blending their pixels. @@ -49,7 +52,7 @@ namespace SixLabors.ImageSharp.Processing image, Point.Empty, colorBlending, - GraphicsOptions.Default.AlphaCompositionMode, + new GraphicsOptions().AlphaCompositionMode, opacity)); /// @@ -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, - GraphicsOptions.Default.ColorBlendingMode, - GraphicsOptions.Default.AlphaCompositionMode, - opacity)); + image, + location, + options.ColorBlendingMode, + options.AlphaCompositionMode, + opacity)); + } /// /// Draws the given image together with the current one by blending their pixels. @@ -129,7 +135,7 @@ namespace SixLabors.ImageSharp.Processing image, location, colorBlending, - GraphicsOptions.Default.AlphaCompositionMode, + new GraphicsOptions().AlphaCompositionMode, opacity)); /// @@ -172,4 +178,4 @@ namespace SixLabors.ImageSharp.Processing options.AlphaCompositionMode, options.BlendPercentage)); } -} \ No newline at end of file +} 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..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; @@ -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. @@ -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 9826748c46..3be56c0424 100644 --- a/src/ImageSharp.Drawing/Processing/GradientBrush.cs +++ b/src/ImageSharp.Drawing/Processing/GradientBrush.cs @@ -1,11 +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.Numerics; - using SixLabors.ImageSharp.PixelFormats; -using SixLabors.ImageSharp.PixelFormats.PixelBlenders; using SixLabors.Primitives; namespace SixLabors.ImageSharp.Processing @@ -38,9 +36,10 @@ 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,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 graphics options. + /// The target image. /// An array of color stops sorted by their position. /// Defines if and how the gradient should be repeated. protected GradientBrushApplicator( - ImageFrame target, + Configuration configuration, GraphicsOptions options, + ImageFrame target, ColorStop[] colorStops, GradientRepetitionMode repetitionMode) - : base(target, options) + : base(configuration, options, target) { 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 @@ -92,10 +88,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 +117,8 @@ 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 new Color(Vector4.Lerp((Vector4)from.Color, (Vector4)to.Color, onLocalGradient)).ToPixel(); } } } @@ -142,8 +127,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. @@ -176,4 +161,4 @@ namespace SixLabors.ImageSharp.Processing } } } -} \ No newline at end of file +} diff --git a/src/ImageSharp.Drawing/Processing/IBrush.cs b/src/ImageSharp.Drawing/Processing/IBrush.cs index 0cd2e20fda..f4c7ef7cbb 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 graphic options. /// 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, + GraphicsOptions options, ImageFrame source, - RectangleF region, - GraphicsOptions options) + RectangleF region) 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 8485ddfd09..e38614070f 100644 --- a/src/ImageSharp.Drawing/Processing/ImageBrush.cs +++ b/src/ImageSharp.Drawing/Processing/ImageBrush.cs @@ -1,11 +1,10 @@ -// Copyright (c) Six Labors and contributors. +// Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. using System; using System.Buffers; using SixLabors.ImageSharp.Advanced; -using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; using SixLabors.Primitives; @@ -32,19 +31,20 @@ 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(source, specificImage, region, options, false); + return new ImageBrushApplicator(configuration, options, source, specificImage, region, false); } specificImage = this.image.CloneAs(); - return new ImageBrushApplicator(source, specificImage, region, options, true); + return new ImageBrushApplicator(configuration, options, source, specificImage, region, true); } /// @@ -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 graphics options. /// The target image. /// The image. /// The region. - /// The options /// Whether to dispose the image on disposal of the applicator. public ImageBrushApplicator( + Configuration configuration, + GraphicsOptions options, ImageFrame target, Image image, RectangleF region, - GraphicsOptions options, bool shouldDisposeImage) - : base(target, options) + : base(configuration, options, target) { 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; } /// @@ -140,8 +144,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; @@ -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, @@ -167,4 +170,4 @@ namespace SixLabors.ImageSharp.Processing } } } -} \ No newline at end of file +} diff --git a/src/ImageSharp.Drawing/Processing/LinearGradientBrush.cs b/src/ImageSharp.Drawing/Processing/LinearGradientBrush.cs index bb99eeb26a..044bee72c5 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,16 +39,18 @@ 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. @@ -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 - /// defines how the gradient colors are repeated. - /// the graphics options + /// The configuration instance to use when performing operations. + /// The graphics options. + /// The source image. + /// 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(source, options, colorStops, repetitionMode) + GradientRepetitionMode repetitionMode) + : base(configuration, options, source, colorStops, repetitionMode) { 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 7315dc5a3e..9e354120e6 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, + GraphicsOptions options, ImageFrame source, - RectangleF region, - GraphicsOptions options) + RectangleF region) where TPixel : struct, IPixel { - return new PathGradientBrushApplicator(source, this.edges, this.centerColor, options); + return new PathGradientBrushApplicator(configuration, options, source, this.edges, this.centerColor); } private static Color CalculateCenterColor(Color[] colors) @@ -105,7 +106,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 +142,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]; @@ -199,23 +200,25 @@ 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 options. public PathGradientBrushApplicator( + Configuration configuration, + GraphicsOptions options, ImageFrame source, IList edges, - Color centerColor, - GraphicsOptions options) - : base(source, options) + Color centerColor) + : base(configuration, options, source) { this.edges = edges; 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(); } @@ -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 1999af8a39..726df5a797 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++) @@ -92,14 +92,16 @@ 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(source.Configuration), - options); + this.pattern.ToPixelMatrix(configuration)); /// /// The pattern brush applicator. @@ -115,41 +117,33 @@ 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 options - public PatternBrushApplicator(ImageFrame source, in DenseMatrix pattern, GraphicsOptions options) - : base(source, options) + public PatternBrushApplicator( + Configuration configuration, + GraphicsOptions options, + ImageFrame source, + in DenseMatrix pattern) + : base(configuration, options, source) { 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) { @@ -159,12 +153,12 @@ 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++) { - 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]; @@ -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, @@ -181,4 +175,4 @@ namespace SixLabors.ImageSharp.Processing } } } -} \ No newline at end of file +} 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 4e052818da..fc94826187 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,11 +84,12 @@ 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.GetSpan().Fill(1f); + amount.Memory.Span.Fill(1f); ParallelHelper.IterateRows( workingRect, @@ -100,7 +101,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.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 45d5015ae0..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(source, rect, options)) + using (BrushApplicator applicator = brush.CreateApplicator(configuration, options, source, rect)) { int scanlineWidth = maxX - minX; using (IMemoryOwner bBuffer = source.MemoryAllocator.Allocate(maxIntersections)) @@ -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..64d32efb80 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); @@ -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, this.textRenderer.Options, source, this.SourceRectangle)) { foreach (DrawingOperation operation in operations) { @@ -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/RadialGradientBrush.cs b/src/ImageSharp.Drawing/Processing/RadialGradientBrush.cs index f4d2dd81f4..2b1b6913f8 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,12 +35,14 @@ namespace SixLabors.ImageSharp.Processing /// public override BrushApplicator CreateApplicator( + Configuration configuration, + GraphicsOptions options, ImageFrame source, - RectangleF region, - GraphicsOptions options) => + RectangleF region) => new RadialGradientBrushApplicator( - source, + configuration, options, + source, this.center, this.radius, this.ColorStops, @@ -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 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. public RadialGradientBrushApplicator( - ImageFrame target, + Configuration configuration, GraphicsOptions options, + ImageFrame target, PointF center, float radius, ColorStop[] colorStops, GradientRepetitionMode repetitionMode) - : base(target, options, colorStops, repetitionMode) + : base(configuration, options, target, colorStops, repetitionMode) { 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 fca95be327..e0e43cf780 100644 --- a/src/ImageSharp.Drawing/Processing/RecolorBrush.cs +++ b/src/ImageSharp.Drawing/Processing/RecolorBrush.cs @@ -1,11 +1,10 @@ -// Copyright (c) Six Labors and contributors. +// Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. using System; using System.Buffers; using System.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,17 +46,19 @@ 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); } /// @@ -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 options /// 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, + GraphicsOptions options, + ImageFrame source, + TPixel sourceColor, + TPixel targetColor, + float threshold) + : base(configuration, options, source) { 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) { @@ -151,8 +138,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++) { @@ -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, @@ -176,4 +163,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..c297ede211 100644 --- a/src/ImageSharp.Drawing/Processing/SolidBrush.cs +++ b/src/ImageSharp.Drawing/Processing/SolidBrush.cs @@ -1,11 +1,10 @@ -// Copyright (c) Six Labors and contributors. +// Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. using System; using System.Buffers; using 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, + GraphicsOptions options, + ImageFrame source, + RectangleF region) where TPixel : struct, IPixel { - return new SolidBrushApplicator(source, this.color.ToPixel(), options); + return new SolidBrushApplicator(configuration, options, source, this.Color.ToPixel()); } /// @@ -52,38 +47,49 @@ 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 graphics options. /// The source image. /// The color. - /// The options - public SolidBrushApplicator(ImageFrame source, TPixel color, GraphicsOptions options) - : base(source, options) + public SolidBrushApplicator( + Configuration configuration, + GraphicsOptions options, + ImageFrame source, + TPixel color) + : base(configuration, options, source) { this.Colors = source.MemoryAllocator.Allocate(source.Width); - this.Colors.GetSpan().Fill(color); + this.Colors.Memory.Span.Fill(color); } /// /// 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.GetSpan()[x]; + /// + 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,17 +108,17 @@ namespace SixLabors.ImageSharp.Processing } MemoryAllocator memoryAllocator = this.Target.MemoryAllocator; - Configuration configuration = this.Target.Configuration; + Configuration configuration = this.Configuration; 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 +129,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.Drawing/Processing/TextGraphicsOptions.cs b/src/ImageSharp.Drawing/Processing/TextGraphicsOptions.cs index 6c140be72e..63730d1bf7 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; @@ -9,120 +9,169 @@ namespace SixLabors.ImageSharp.Processing /// /// Options for influencing the drawing functions. /// - public struct TextGraphicsOptions + public class TextGraphicsOptions : IDeepCloneable { - private const int DefaultTextDpi = 72; + private int antialiasSubpixelDepth = 16; + private float blendPercentage = 1F; + private float tabWidth = 4F; + private float dpiX = 72F; + private float dpiY = 72F; /// - /// Represents the default . + /// Initializes a new instance of the class. /// - public static readonly TextGraphicsOptions Default = new TextGraphicsOptions(true); - - 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() + { + } - /// - /// Initializes a new instance of the struct. - /// - /// If set to true [enable antialiasing]. - public TextGraphicsOptions(bool enableAntialiasing) + private TextGraphicsOptions(TextGraphicsOptions source) { - this.applyKerning = true; - this.tabWidth = 4; - this.WrapTextWidth = 0; - this.horizontalAlignment = HorizontalAlignment.Left; - this.verticalAlignment = VerticalAlignment.Top; - - this.antialiasSubpixelDepth = 16; - this.ColorBlendingMode = PixelColorBlendingMode.Normal; - this.AlphaCompositionMode = PixelAlphaCompositionMode.SrcOver; - this.blendPercentage = 1; - this.antialias = enableAntialiasing; - this.dpiX = DefaultTextDpi; - this.dpiY = DefaultTextDpi; + 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 or sets a value indicating whether antialiasing should be applied. + /// Defaults to true. /// - public bool Antialias { get => this.antialias ?? true; set => this.antialias = value; } + public bool Antialias { get; set; } = true; /// /// 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 ?? 1).Clamp(0, 1); set => this.blendPercentage = value; } + public float BlendPercentage + { + get + { + return this.blendPercentage; + } - // 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. + set + { + Guard.MustBeBetweenOrEqualTo(value, 0, 1F, nameof(this.BlendPercentage)); + this.blendPercentage = value; + } + } /// - /// 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 => this.applyKerning ?? true; set => this.applyKerning = value; } + 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 { 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. + /// 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 { 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. + /// Gets or sets a value indicating the DPI (Dots Per Inch) to render text along the Y axis. + /// Defaults to 72. /// - 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. /// 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 => this.horizontalAlignment ?? HorizontalAlignment.Left; set => this.horizontalAlignment = value; } + 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 => this.verticalAlignment ?? VerticalAlignment.Top; set => this.verticalAlignment = value; } + public VerticalAlignment VerticalAlignment { get; set; } = VerticalAlignment.Top; /// /// Performs an implicit conversion from to . @@ -133,8 +182,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, @@ -151,13 +201,17 @@ 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 }; } + + /// + public TextGraphicsOptions DeepClone() => new TextGraphicsOptions(this); } -} \ 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; + } + } +} 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; 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); - } } } 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/Configuration.cs b/src/ImageSharp/Configuration.cs index ae20490c77..4dba7a7e8e 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()); } } } 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; } 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..b24a5eabda 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,31 +1148,34 @@ 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. - /// + [MethodImpl(InliningOptions.ShortMethod)] 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; } /// /// 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); @@ -1179,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() @@ -1195,11 +1203,20 @@ namespace SixLabors.ImageSharp.Formats.Png /// /// Thrown if the input stream is not valid. /// + [MethodImpl(InliningOptions.ShortMethod)] 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; + } } /// @@ -1208,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/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/PngThrowHelper.cs b/src/ImageSharp/Formats/Png/PngThrowHelper.cs new file mode 100644 index 0000000000..00b40c50b4 --- /dev/null +++ b/src/ImageSharp/Formats/Png/PngThrowHelper.cs @@ -0,0 +1,33 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Diagnostics.CodeAnalysis; +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"); + + [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/src/ImageSharp/Formats/Png/Zlib/Adler32.cs b/src/ImageSharp/Formats/Png/Zlib/Adler32.cs index a06983b9ed..c4dc82a4dc 100644 --- a/src/ImageSharp/Formats/Png/Zlib/Adler32.cs +++ b/src/ImageSharp/Formats/Png/Zlib/Adler32.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 { @@ -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,8 +134,8 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib count -= n; while (--n >= 0) { - s1 = s1 + (uint)(data[offset++] & 0xff); - s2 = s2 + s1; + s1 += Unsafe.Add(ref dataRef, offset++); + s2 += s1; } s1 %= Base; @@ -144,4 +145,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/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 new file mode 100644 index 0000000000..5f62b13c7f --- /dev/null +++ b/src/ImageSharp/Formats/Png/Zlib/DeflateThrowHelper.cs @@ -0,0 +1,35 @@ +// 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 DeflateThrowHelper + { + [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."); + + [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)] + public static void ThrowNull(string name) => throw new ArgumentNullException(name); + + [MethodImpl(InliningOptions.ColdPath)] + public static void ThrowOutOfRange(string name) => throw new ArgumentOutOfRangeException(name); + + [MethodImpl(InliningOptions.ColdPath)] + public static void ThrowHeapViolated() => throw new InvalidOperationException("Huffman heap invariant violated."); + + [MethodImpl(InliningOptions.ColdPath)] + 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 new file mode 100644 index 0000000000..6c4ea44d1d --- /dev/null +++ b/src/ImageSharp/Formats/Png/Zlib/Deflater.cs @@ -0,0 +1,295 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Runtime.CompilerServices; +using SixLabors.Memory; + +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. + /// + internal sealed class Deflater : IDisposable + { + /// + /// The best and slowest compression level. This tries to find very + /// long and distant string repetitions. + /// + public const int BestCompression = 9; + + /// + /// The worst but fastest compression level. + /// + public const int BestSpeed = 1; + + /// + /// The default compression level. + /// + public const int DefaultCompression = -1; + + /// + /// This level won't compress at all but output uncompressed blocks. + /// + 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; + + /// + /// Compression level. + /// + private int level; + + /// + /// The current state. + /// + private int state; + + private DeflaterEngine engine; + private bool isDisposed; + + 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)); + } + + // TODO: Possibly provide DeflateStrategy as an option. + this.engine = new DeflaterEngine(memoryAllocator, DeflateStrategy.Default); + + this.SetLevel(level); + this.Reset(); + } + + /// + /// 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. + /// + BestCompression = Deflater.BestCompression, + + /// + /// The worst but fastest compression level. + /// + BestSpeed = Deflater.BestSpeed, + + /// + /// The default compression level. + /// + DefaultCompression = Deflater.DefaultCompression, + + /// + /// This level won't compress at all but output uncompressed blocks. + /// + 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 + } + + /// + /// Gets a value indicating whetherthe stream was finished and no more output bytes + /// are available. + /// + public bool IsFinished => (this.state == FinishedState) && this.engine.Pending.IsFlushed; + + /// + /// 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() + { + this.state = BusyState; + this.engine.Pending.Reset(); + this.engine.Reset(); + } + + /// + /// Flushes the current input block. Further calls to Deflate() will + /// produce enough output to inflate everything in the current input + /// block. It is used by DeflaterOutputStream to implement Flush(). + /// + [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 + /// be called to force all bytes to be flushed. + /// + [MethodImpl(InliningOptions.ShortMethod)] + public void Finish() => this.state |= IsFlushing | IsFinishing; + + /// + /// 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 finished or if previous input is still pending. + /// + [MethodImpl(InliningOptions.ShortMethod)] + public void SetInput(byte[] input, int offset, int count) + { + if ((this.state & IsFinishing) != 0) + { + DeflateThrowHelper.ThrowAlreadyFinished(); + } + + this.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 == DefaultCompression) + { + level = 6; + } + else if (level < NoCompression || level > BestCompression) + { + throw new ArgumentOutOfRangeException(nameof(level)); + } + + if (this.level != level) + { + this.level = level; + this.engine.SetLevel(level); + } + } + + /// + /// 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 + /// or returns true or length is zero. + /// + public int Deflate(byte[] output, int offset, int length) + { + int origLength = length; + + if (this.state == ClosedState) + { + DeflateThrowHelper.ThrowAlreadyClosed(); + } + + while (true) + { + int count = this.engine.Pending.Flush(output, offset, length); + offset += count; + length -= count; + + if (length == 0 || this.state == FinishedState) + { + break; + } + + if (!this.engine.Deflate((this.state & IsFlushing) != 0, (this.state & IsFinishing) != 0)) + { + switch (this.state) + { + case BusyState: + // We need more input now + return origLength - length; + + 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 + ((-this.engine.Pending.BitCount) & 7); + while (neededbits > 0) + { + // Write a static tree block consisting solely of an EOF: + this.engine.Pending.WriteBits(2, 10); + neededbits -= 10; + } + } + + this.state = BusyState; + break; + + case FinishingState: + this.engine.Pending.AlignToByte(); + this.state = FinishedState; + break; + } + } + } + + return origLength - length; + } + + /// + public void Dispose() + { + if (!this.isDisposed) + { + this.engine.Dispose(); + this.engine = null; + this.isDisposed = true; + } + + GC.SuppressFinalize(this); + } + } +} 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..0163eec0b7 --- /dev/null +++ b/src/ImageSharp/Formats/Png/Zlib/DeflaterEngine.cs @@ -0,0 +1,859 @@ +// 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 +{ + /// + /// 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. + /// + internal sealed unsafe class DeflaterEngine : IDisposable + { + private const int TooFar = 4096; + + // Hash index of string to be inserted + private int insertHashIndex; + + 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; + + /// + /// The current compression function. + /// + private int compressionFunction; + + /// + /// The input data for compression. + /// + private byte[] inputBuf; + + /// + /// The offset into inputBuf, where input data starts. + /// + private int inputOff; + + /// + /// The end offset of the input data. + /// + private int inputEnd; + + private readonly DeflateStrategy strategy; + private DeflaterHuffman huffman; + private bool isDisposed; + + /// + /// 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 IMemoryOwner headMemoryOwner; + private MemoryHandle headMemoryHandle; + private readonly Memory head; + private readonly short* pinnedHeadPointer; + + /// + /// 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 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. + /// + private IManagedByteBuffer windowMemoryOwner; + private MemoryHandle windowMemoryHandle; + private readonly 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 deflate strategy to use. + public DeflaterEngine(MemoryAllocator memoryAllocator, DeflateStrategy strategy) + { + this.huffman = new DeflaterHuffman(memoryAllocator); + this.Pending = this.huffman.Pending; + this.strategy = strategy; + + // Create pinned pointers to the various buffers to allow indexing + // without bounds checks. + 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.headMemoryOwner = memoryAllocator.Allocate(DeflaterConstants.HASH_SIZE); + this.head = this.headMemoryOwner.Memory; + this.headMemoryHandle = this.headMemoryOwner.Memory.Pin(); + this.pinnedHeadPointer = (short*)this.headMemoryHandle.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. + this.blockStart = this.strstart = 1; + } + + /// + /// Gets the pending buffer to use. + /// + public DeflaterPendingBuffer Pending { get; } + + /// + /// 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 = false; + do + { + this.FillWindow(); + bool canFlush = flush && (this.inputOff == this.inputEnd); + + switch (this.compressionFunction) + { + case DeflaterConstants.DEFLATE_STORED: + progress = this.DeflateStored(canFlush, finish); + break; + + case DeflaterConstants.DEFLATE_FAST: + progress = this.DeflateFast(canFlush, finish); + break; + + case DeflaterConstants.DEFLATE_SLOW: + progress = this.DeflateSlow(canFlush, finish); + break; + + default: + DeflateThrowHelper.ThrowUnknownCompression(); + break; + } + } + while (this.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 + /// 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 is null) + { + DeflateThrowHelper.ThrowNull(nameof(buffer)); + } + + if (offset < 0) + { + DeflateThrowHelper.ThrowOutOfRange(nameof(offset)); + } + + if (count < 0) + { + DeflateThrowHelper.ThrowOutOfRange(nameof(count)); + } + + if (this.inputOff < this.inputEnd) + { + DeflateThrowHelper.ThrowNotProcessed(); + } + + int end = offset + count; + + // We want to throw an ArgumentOutOfRangeException early. + // The check is very tricky: it also handles integer wrap around. + if ((offset > end) || (end > buffer.Length)) + { + DeflateThrowHelper.ThrowOutOfRange(nameof(count)); + } + + this.inputBuf = buffer; + this.inputOff = offset; + this.inputEnd = end; + } + + /// + /// Determines if more input is needed. + /// + /// Return true if input is needed via SetInput + [MethodImpl(InliningOptions.ShortMethod)] + public bool NeedsInput() => this.inputEnd == this.inputOff; + + /// + /// Reset internal state + /// + [MethodImpl(InliningOptions.ShortMethod)] + public void Reset() + { + this.huffman.Reset(); + this.blockStart = this.strstart = 1; + this.lookahead = 0; + this.prevAvailable = false; + this.matchLen = DeflaterConstants.MIN_MATCH - 1; + this.head.Span.Slice(0, DeflaterConstants.HASH_SIZE).Clear(); + this.prev.Span.Slice(0, DeflaterConstants.WSIZE).Clear(); + } + + /// + /// Set the deflate level (0-9) + /// + /// The value to set the level to. + public void SetLevel(int level) + { + if ((level < 0) || (level > 9)) + { + DeflateThrowHelper.ThrowOutOfRange(nameof(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] != this.compressionFunction) + { + switch (this.compressionFunction) + { + case DeflaterConstants.DEFLATE_STORED: + if (this.strstart > this.blockStart) + { + this.huffman.FlushStoredBlock(this.window, this.blockStart, this.strstart - this.blockStart, false); + this.blockStart = this.strstart; + } + + this.UpdateHash(); + break; + + case DeflaterConstants.DEFLATE_FAST: + if (this.strstart > this.blockStart) + { + this.huffman.FlushBlock(this.window, this.blockStart, this.strstart - this.blockStart, false); + this.blockStart = this.strstart; + } + + break; + + case DeflaterConstants.DEFLATE_SLOW: + if (this.prevAvailable) + { + this.huffman.TallyLit(this.pinnedWindowPointer[this.strstart - 1] & 0xFF); + } + + if (this.strstart > this.blockStart) + { + this.huffman.FlushBlock(this.window, this.blockStart, this.strstart - this.blockStart, false); + this.blockStart = this.strstart; + } + + this.prevAvailable = false; + this.matchLen = DeflaterConstants.MIN_MATCH - 1; + break; + } + + this.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 (this.strstart >= DeflaterConstants.WSIZE + DeflaterConstants.MAX_DIST) + { + this.SlideWindow(); + } + + // 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) - this.lookahead - this.strstart; + + if (more > this.inputEnd - this.inputOff) + { + more = this.inputEnd - this.inputOff; + } + + Array.Copy(this.inputBuf, this.inputOff, this.window, this.strstart + this.lookahead, more); + + this.inputOff += more; + this.lookahead += more; + } + + if (this.lookahead >= DeflaterConstants.MIN_MATCH) + { + this.UpdateHash(); + } + } + + /// + public void Dispose() + { + 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); + } + + [MethodImpl(InliningOptions.ShortMethod)] + private void UpdateHash() + { + byte* pinned = this.pinnedWindowPointer; + this.insertHashIndex = (pinned[this.strstart] << DeflaterConstants.HASH_SHIFT) ^ pinned[this.strstart + 1]; + } + + /// + /// Inserts the current string in the head hash and returns the previous + /// 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; + + 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; + } + + private void SlideWindow() + { + 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; + + // 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 = 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 = pinnedPrev[i] & 0xFFFF; + pinnedPrev[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 = this.strstart; + + // scanMax is the highest position that we can look at + int scanMax = scan + Math.Min(DeflaterConstants.MAX_MATCH, this.lookahead) - 1; + int limit = Math.Max(scan - DeflaterConstants.MAX_DIST, 0); + + 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 + matchLength > scanMax) + { + return false; + } + + byte* pinnedWindow = this.pinnedWindowPointer; + 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 (matchLength >= this.goodLength) + { + chainLength >>= 2; + } + + short* pinnedPrev = this.pinnedPrevPointer; + do + { + match = curMatch; + scan = scanStart; + + if (pinnedWindow[match + matchLength] != scanEnd + || pinnedWindow[match + matchLength - 1] != scanEnd1 + || pinnedWindow[match] != pinnedWindow[scan] + || pinnedWindow[++match] != pinnedWindow[++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 + // n & (8 - 1) == n % 8. + switch ((scanMax - scan) & 7) + { + case 1: + if (pinnedWindow[++scan] == pinnedWindow[++match]) + { + break; + } + + break; + + case 2: + if (pinnedWindow[++scan] == pinnedWindow[++match] + && pinnedWindow[++scan] == pinnedWindow[++match]) + { + break; + } + + break; + + case 3: + if (pinnedWindow[++scan] == pinnedWindow[++match] + && pinnedWindow[++scan] == pinnedWindow[++match] + && pinnedWindow[++scan] == pinnedWindow[++match]) + { + break; + } + + break; + + case 4: + if (pinnedWindow[++scan] == pinnedWindow[++match] + && pinnedWindow[++scan] == pinnedWindow[++match] + && pinnedWindow[++scan] == pinnedWindow[++match] + && pinnedWindow[++scan] == pinnedWindow[++match]) + { + break; + } + + break; + + case 5: + if (pinnedWindow[++scan] == pinnedWindow[++match] + && pinnedWindow[++scan] == pinnedWindow[++match] + && pinnedWindow[++scan] == pinnedWindow[++match] + && pinnedWindow[++scan] == pinnedWindow[++match] + && pinnedWindow[++scan] == pinnedWindow[++match]) + { + break; + } + + break; + + case 6: + 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; + } + + break; + + case 7: + 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; + } + + break; + } + + 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 + // exhausted first. + do + { + if (scan == scanMax) + { + ++scan; // advance to first position not matched + ++match; + + break; + } + } + 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 - scanStart > matchLength) + { + matchStrt = curMatch; + matchLength = scan - scanStart; + + if (matchLength >= niceLength) + { + break; + } + + scanEnd1 = pinnedWindow[scan - 1]; + scanEnd = pinnedWindow[scan]; + } + } + while ((curMatch = pinnedPrev[curMatch & DeflaterConstants.WMASK] & 0xFFFF) > limit && --chainLength != 0); + + this.matchStart = matchStrt; + this.matchLen = matchLength; + return matchLength >= DeflaterConstants.MIN_MATCH; + } + + private bool DeflateStored(bool flush, bool finish) + { + if (!flush && (this.lookahead == 0)) + { + return false; + } + + this.strstart += this.lookahead; + this.lookahead = 0; + + int storedLength = this.strstart - this.blockStart; + + if ((storedLength >= DeflaterConstants.MAX_BLOCK_SIZE) || // Block is full + (this.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; + } + + 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 (this.lookahead < DeflaterConstants.MIN_LOOKAHEAD && !flush) + { + return false; + } + + while (this.lookahead >= DeflaterConstants.MIN_LOOKAHEAD || flush) + { + if (this.lookahead == 0) + { + // We are flushing everything + this.huffman.FlushBlock(this.window, this.blockStart, this.strstart - this.blockStart, finish); + this.blockStart = this.strstart; + return false; + } + + 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. + this.SlideWindow(); + } + + int 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 + bool full = this.huffman.TallyDist(this.strstart - this.matchStart, this.matchLen); + + this.lookahead -= this.matchLen; + if (this.matchLen <= this.maxLazy && this.lookahead >= DeflaterConstants.MIN_MATCH) + { + while (--this.matchLen > 0) + { + ++this.strstart; + this.InsertString(); + } + + ++this.strstart; + } + else + { + this.strstart += this.matchLen; + if (this.lookahead >= DeflaterConstants.MIN_MATCH - 1) + { + this.UpdateHash(); + } + } + + this.matchLen = DeflaterConstants.MIN_MATCH - 1; + if (!full) + { + continue; + } + } + else + { + // No match found + this.huffman.TallyLit(this.pinnedWindowPointer[this.strstart] & 0xff); + ++this.strstart; + --this.lookahead; + } + + if (this.huffman.IsFull()) + { + 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 (this.lookahead < DeflaterConstants.MIN_LOOKAHEAD && !flush) + { + return false; + } + + while (this.lookahead >= DeflaterConstants.MIN_LOOKAHEAD || flush) + { + if (this.lookahead == 0) + { + if (this.prevAvailable) + { + this.huffman.TallyLit(this.pinnedWindowPointer[this.strstart - 1] & 0xff); + } + + this.prevAvailable = false; + + // We are flushing everything + this.huffman.FlushBlock(this.window, this.blockStart, this.strstart - this.blockStart, finish); + this.blockStart = this.strstart; + return false; + } + + 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. + this.SlideWindow(); + } + + int prevMatch = this.matchStart; + int prevLen = this.matchLen; + if (this.lookahead >= DeflaterConstants.MIN_MATCH) + { + int hashHead = this.InsertString(); + + 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))) + { + this.matchLen = DeflaterConstants.MIN_MATCH - 1; + } + } + } + + // previous match was better + if ((prevLen >= DeflaterConstants.MIN_MATCH) && (this.matchLen <= prevLen)) + { + this.huffman.TallyDist(this.strstart - 1 - prevMatch, prevLen); + prevLen -= 2; + do + { + this.strstart++; + this.lookahead--; + if (this.lookahead >= DeflaterConstants.MIN_MATCH) + { + this.InsertString(); + } + } + while (--prevLen > 0); + + this.strstart++; + this.lookahead--; + this.prevAvailable = false; + this.matchLen = DeflaterConstants.MIN_MATCH - 1; + } + else + { + if (this.prevAvailable) + { + this.huffman.TallyLit(this.pinnedWindowPointer[this.strstart - 1] & 0xff); + } + + this.prevAvailable = true; + this.strstart++; + this.lookahead--; + } + + if (this.huffman.IsFull()) + { + int len = this.strstart - this.blockStart; + if (this.prevAvailable) + { + 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; + } + } +} diff --git a/src/ImageSharp/Formats/Png/Zlib/DeflaterHuffman.cs b/src/ImageSharp/Formats/Png/Zlib/DeflaterHuffman.cs new file mode 100644 index 0000000000..96ff6b6576 --- /dev/null +++ b/src/ImageSharp/Formats/Png/Zlib/DeflaterHuffman.cs @@ -0,0 +1,949 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Buffers; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using SixLabors.Memory; + +namespace SixLabors.ImageSharp.Formats.Png.Zlib +{ + /// + /// Performs Deflate Huffman encoding. + /// + internal sealed unsafe class DeflaterHuffman : IDisposable + { + 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 DistanceNumber = 30; + + // 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) + private const int Repeat3To6 = 16; + + // 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) + private const int Repeat11To138 = 18; + + 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[] 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 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 readonly IMemoryOwner distanceManagedBuffer; + private readonly short* pinnedDistanceBuffer; + private MemoryHandle distanceBufferHandle; + + private readonly IMemoryOwner literalManagedBuffer; + private readonly short* pinnedLiteralBuffer; + private MemoryHandle literalBufferHandle; + + private int lastLiteral; + private int extraBits; + private bool isDisposed; + + // TODO: These should be pre-generated array/readonlyspans. + static DeflaterHuffman() + { + // See RFC 1951 3.2.6 + // Literal codes + StaticLCodes = new short[LiteralNumber]; + StaticLLength = new byte[LiteralNumber]; + + int i = 0; + while (i < 144) + { + StaticLCodes[i] = BitReverse((0x030 + i) << 8); + StaticLLength[i++] = 8; + } + + while (i < 256) + { + StaticLCodes[i] = BitReverse((0x190 - 144 + i) << 7); + StaticLLength[i++] = 9; + } + + while (i < 280) + { + StaticLCodes[i] = BitReverse((0x000 - 256 + i) << 9); + StaticLLength[i++] = 7; + } + + while (i < LiteralNumber) + { + StaticLCodes[i] = BitReverse((0x0c0 - 280 + i) << 8); + StaticLLength[i++] = 8; + } + + // Distance codes + StaticDCodes = new short[DistanceNumber]; + StaticDLength = new byte[DistanceNumber]; + for (i = 0; i < DistanceNumber; i++) + { + StaticDCodes[i] = BitReverse(i << 11); + StaticDLength[i] = 5; + } + } + + /// + /// Initializes a new instance of the class. + /// + /// The memory allocator to use for buffer allocations. + public DeflaterHuffman(MemoryAllocator memoryAllocator) + { + this.Pending = new DeflaterPendingBuffer(memoryAllocator); + + 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(); + this.pinnedDistanceBuffer = (short*)this.distanceBufferHandle.Pointer; + + this.literalManagedBuffer = memoryAllocator.Allocate(BufferSize); + this.literalBufferHandle = this.literalManagedBuffer.Memory.Pin(); + this.pinnedLiteralBuffer = (short*)this.literalBufferHandle.Pointer; + } + + /// + /// Gets the pending buffer to use. + /// + public DeflaterPendingBuffer Pending { get; private set; } + + /// + /// Reset internal state + /// + [MethodImpl(InliningOptions.ShortMethod)] + public void Reset() + { + this.lastLiteral = 0; + this.extraBits = 0; + this.literalTree.Reset(); + this.distTree.Reset(); + this.blTree.Reset(); + } + + /// + /// 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.Pending, this.blTree); + this.distTree.WriteTree(this.Pending, this.blTree); + } + + /// + /// Compress current buffer writing data to pending buffer + /// + 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 = pinnedLiteral[i] & 0xFF; + int dist = pinnedDistance[i]; + if (dist-- != 0) + { + int lc = Lcode(litlen); + this.literalTree.WriteSymbol(pendingBuffer, 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(pendingBuffer, dc); + + bits = (dc >> 1) - 1; + if (bits > 0) + { + this.Pending.WriteBits(dist & ((1 << bits) - 1), bits); + } + } + else + { + this.literalTree.WriteSymbol(pendingBuffer, litlen); + } + } + + this.literalTree.WriteSymbol(pendingBuffer, 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 + [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); + 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.Frequencies[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; + ref byte staticLLengthRef = ref MemoryMarshal.GetReference(StaticLLength); + for (int i = 0; i < LiteralNumber; 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.Frequencies[i] * Unsafe.Add(ref staticDLengthRef, 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 + [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.pinnedDistanceBuffer[this.lastLiteral] = 0; + this.pinnedLiteralBuffer[this.lastLiteral++] = (byte)literal; + this.literalTree.Frequencies[literal]++; + return this.IsFull(); + } + + /// + /// Add distance code and length to literal and distance trees + /// + /// Distance code + /// Length + /// Value indicating if internal buffer is full + [MethodImpl(InliningOptions.ShortMethod)] + public bool TallyDist(int distance, int length) + { + this.pinnedDistanceBuffer[this.lastLiteral] = (short)distance; + this.pinnedLiteralBuffer[this.lastLiteral++] = (byte)(length - 3); + + int lc = Lcode(length - 3); + this.literalTree.Frequencies[lc]++; + if (lc >= 265 && lc < 285) + { + this.extraBits += (lc - 261) / 4; + } + + int dc = Dcode(distance - 1); + this.distTree.Frequencies[dc]++; + if (dc >= 4) + { + this.extraBits += (dc >> 1) - 1; + } + + return this.IsFull(); + } + + /// + /// Reverse the bits of a 16 bit value. + /// + /// 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]); + } + + /// + public void Dispose() + { + 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); + } + + [MethodImpl(InliningOptions.ShortMethod)] + private static int Lcode(int length) + { + if (length == 255) + { + return 285; + } + + int code = 257; + while (length >= 8) + { + code += 4; + length >>= 1; + } + + return code + length; + } + + [MethodImpl(InliningOptions.ShortMethod)] + private static int Dcode(int distance) + { + int code = 0; + while (distance >= 4) + { + code += 2; + distance >>= 1; + } + + return code + distance; + } + + private sealed class Tree : IDisposable + { + private readonly int minNumCodes; + private readonly int[] bitLengthCounts; + private readonly int maxLength; + private bool isDisposed; + + private readonly int elementCount; + + private readonly MemoryAllocator memoryAllocator; + + private IMemoryOwner codesMemoryOwner; + private MemoryHandle codesMemoryHandle; + private readonly 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.memoryAllocator = memoryAllocator; + this.elementCount = elements; + this.minNumCodes = minCodes; + this.maxLength = maxLength; + + 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* Frequencies { get; } + + public byte* Length { get; } + + /// + /// Resets the internal state of the tree + /// + [MethodImpl(InliningOptions.ShortMethod)] + public void Reset() + { + this.frequenciesMemoryOwner.Memory.Span.Clear(); + this.lengthsMemoryOwner.Memory.Span.Clear(); + this.codesMemoryOwner.Memory.Span.Clear(); + } + + [MethodImpl(InliningOptions.ShortMethod)] + public void WriteSymbol(DeflaterPendingBuffer pendingBuffer, int code) + => pendingBuffer.WriteBits(this.codes[code] & 0xFFFF, this.Length[code]); + + /// + /// Set static codes and length + /// + /// new codes + /// length for new codes + [MethodImpl(InliningOptions.ShortMethod)] + public void SetStaticCodes(ReadOnlySpan staticCodes, ReadOnlySpan staticLengths) + { + staticCodes.CopyTo(this.codesMemoryOwner.Memory.Span); + staticLengths.CopyTo(this.lengthsMemoryOwner.Memory.Span); + } + + /// + /// Build dynamic codes and lengths + /// + public void BuildCodes() + { + // 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++) + { + Unsafe.Add(ref nextCodeRef, bits) = code; + code += Unsafe.Add(ref bitLengthCountsRef, bits) << (15 - bits); + } + + for (int i = 0; i < this.NumCodes; i++) + { + int bits = this.Length[i]; + if (bits > 0) + { + 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.elementCount; + + // 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. + // Maxes out at 286 * 4 so too large for the stack. + using (IMemoryOwner heapMemoryOwner = this.memoryAllocator.Allocate(numSymbols)) + { + ref int heapRef = ref MemoryMarshal.GetReference(heapMemoryOwner.Memory.Span); + + 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) >> 1)] > freq) + { + Unsafe.Add(ref heapRef, pos) = Unsafe.Add(ref heapRef, ppos); + pos = ppos; + } + + Unsafe.Add(ref heapRef, 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 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; + + 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; + } + + // Construct the Huffman tree by repeatedly combining the least two + // frequent nodes. + do + { + 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); + + if (Unsafe.Add(ref heapRef, 0) != (childrenLength >> 1) - 1) + { + DeflateThrowHelper.ThrowHeapViolated(); + } + + this.BuildLength(childrenMemoryOwner.Memory.Span); + } + } + } + + /// + /// 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.elementCount; i++) + { + len += this.Frequencies[i] * this.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 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) + { + count = 1; + int nextlen = this.Length[i]; + if (nextlen == 0) + { + maxCount = 138; + minCount = 3; + } + else + { + maxCount = 6; + minCount = 3; + if (curLen != nextlen) + { + blTree.Frequencies[nextlen]++; + count = 0; + } + } + + curLen = nextlen; + i++; + + while (i < this.NumCodes && curLen == this.Length[i]) + { + i++; + if (++count >= maxCount) + { + break; + } + } + + if (count < minCount) + { + blTree.Frequencies[curLen] += (short)count; + } + else if (curLen != 0) + { + blTree.Frequencies[Repeat3To6]++; + } + else if (count <= 10) + { + blTree.Frequencies[Repeat3To10]++; + } + else + { + blTree.Frequencies[Repeat11To138]++; + } + } + } + + /// + /// Write the tree values. + /// + /// The pending buffer. + /// The tree to write. + public void WriteTree(DeflaterPendingBuffer pendingBuffer, Tree bitLengthTree) + { + 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) + { + count = 1; + int nextlen = this.Length[i]; + if (nextlen == 0) + { + maxCount = 138; + minCount = 3; + } + else + { + maxCount = 6; + minCount = 3; + if (curLen != nextlen) + { + bitLengthTree.WriteSymbol(pendingBuffer, nextlen); + count = 0; + } + } + + curLen = nextlen; + i++; + + while (i < this.NumCodes && curLen == this.Length[i]) + { + i++; + if (++count >= maxCount) + { + break; + } + } + + if (count < minCount) + { + while (count-- > 0) + { + bitLengthTree.WriteSymbol(pendingBuffer, curLen); + } + } + else if (curLen != 0) + { + bitLengthTree.WriteSymbol(pendingBuffer, Repeat3To6); + pendingBuffer.WriteBits(count - 3, 2); + } + else if (count <= 10) + { + bitLengthTree.WriteSymbol(pendingBuffer, Repeat3To10); + pendingBuffer.WriteBits(count - 3, 3); + } + else + { + bitLengthTree.WriteSymbol(pendingBuffer, Repeat11To138); + pendingBuffer.WriteBits(count - 11, 7); + } + } + } + + 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; + + Array.Clear(this.bitLengthCounts, 0, maxLen); + + // First calculate optimal bit lengths + using (IMemoryOwner lengthsMemoryOwner = this.memoryAllocator.Allocate(numNodes, AllocationOptions.Clean)) + { + ref int lengthsRef = ref MemoryMarshal.GetReference(lengthsMemoryOwner.Memory.Span); + + for (int i = numNodes - 1; i >= 0; i--) + { + if (children[(2 * i) + 1] != -1) + { + int bitLength = Unsafe.Add(ref lengthsRef, i) + 1; + if (bitLength > maxLen) + { + bitLength = maxLen; + overflow++; + } + + 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); + } + } + } + + if (overflow == 0) + { + return; + } + + int incrBitLen = maxLen - 1; + do + { + // Find the first bit length which could increase: + while (Unsafe.Add(ref bitLengthCountsRef, --incrBitLen) == 0) + { + } + + // Move this node one down and remove a corresponding + // number of overflow nodes. + do + { + Unsafe.Add(ref bitLengthCountsRef, incrBitLen)--; + Unsafe.Add(ref bitLengthCountsRef, ++incrBitLen)++; + overflow -= 1 << (maxLen - 1 - incrBitLen); + } + 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. + 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 + // 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 nodeIndex = 2 * numLeafs; + for (int bits = maxLen; bits != 0; bits--) + { + int n = Unsafe.Add(ref bitLengthCountsRef, bits - 1); + while (n > 0) + { + int childIndex = 2 * Unsafe.Add(ref childrenRef, nodeIndex++); + if (Unsafe.Add(ref childrenRef, childIndex + 1) == -1) + { + // We found another leaf + lengthPtr[Unsafe.Add(ref childrenRef, childIndex)] = (byte)bits; + n--; + } + } + } + } + + public void Dispose() + { + if (!this.isDisposed) + { + 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; + } + + GC.SuppressFinalize(this); + } + } + } +} diff --git a/src/ImageSharp/Formats/Png/Zlib/DeflaterOutputStream.cs b/src/ImageSharp/Formats/Png/Zlib/DeflaterOutputStream.cs new file mode 100644 index 0000000000..9eeb12cb08 --- /dev/null +++ b/src/ImageSharp/Formats/Png/Zlib/DeflaterOutputStream.cs @@ -0,0 +1,153 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.IO; +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. + /// + internal sealed class DeflaterOutputStream : Stream + { + private const int BufferLength = 512; + private IManagedByteBuffer memoryOwner; + private readonly byte[] buffer; + private Deflater deflater; + private readonly Stream rawStream; + private bool isDisposed; + + /// + /// Initializes a new instance of the class. + /// + /// 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) + { + this.rawStream = rawStream; + this.memoryOwner = memoryAllocator.AllocateManagedByteBuffer(BufferLength); + this.buffer = this.memoryOwner.Array; + this.deflater = new Deflater(memoryAllocator, compressionLevel); + } + + /// + public override bool CanRead => false; + + /// + public override bool CanSeek => false; + + /// + public override bool CanWrite => this.rawStream.CanWrite; + + /// + public override long Length => this.rawStream.Length; + + /// + public override long Position + { + get + { + return this.rawStream.Position; + } + + set + { + 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 int ReadByte() => throw new NotSupportedException(); + + /// + public override int Read(byte[] buffer, int offset, int count) => throw new NotSupportedException(); + + /// + public override void Flush() + { + this.deflater.Flush(); + this.Deflate(true); + this.rawStream.Flush(); + } + + /// + public override void Write(byte[] buffer, int offset, int count) + { + this.deflater.SetInput(buffer, offset, count); + this.Deflate(); + } + + private void Deflate() => this.Deflate(false); + + private void Deflate(bool flushing) + { + while (flushing || !this.deflater.IsNeedingInput) + { + int deflateCount = this.deflater.Deflate(this.buffer, 0, BufferLength); + + if (deflateCount <= 0) + { + break; + } + + this.rawStream.Write(this.buffer, 0, deflateCount); + } + + if (!this.deflater.IsNeedingInput) + { + DeflateThrowHelper.ThrowNoDeflate(); + } + } + + private void Finish() + { + this.deflater.Finish(); + while (!this.deflater.IsFinished) + { + int len = this.deflater.Deflate(this.buffer, 0, BufferLength); + if (len <= 0) + { + break; + } + + this.rawStream.Write(this.buffer, 0, len); + } + + if (!this.deflater.IsFinished) + { + DeflateThrowHelper.ThrowNoDeflate(); + } + + this.rawStream.Flush(); + } + + /// + protected override void Dispose(bool disposing) + { + if (!this.isDisposed) + { + if (disposing) + { + this.Finish(); + this.deflater.Dispose(); + this.memoryOwner.Dispose(); + } + + this.deflater = null; + this.memoryOwner = null; + this.isDisposed = true; + base.Dispose(disposing); + } + } + } +} diff --git a/src/ImageSharp/Formats/Png/Zlib/DeflaterPendingBuffer.cs b/src/ImageSharp/Formats/Png/Zlib/DeflaterPendingBuffer.cs new file mode 100644 index 0000000000..a5f00f03ca --- /dev/null +++ b/src/ImageSharp/Formats/Png/Zlib/DeflaterPendingBuffer.cs @@ -0,0 +1,179 @@ +// 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. + /// + internal sealed unsafe class DeflaterPendingBuffer : IDisposable + { + private readonly byte[] buffer; + private readonly byte* pinnedBuffer; + private IManagedByteBuffer bufferMemoryOwner; + private MemoryHandle bufferMemoryHandle; + + 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.bufferMemoryOwner = memoryAllocator.AllocateManagedByteBuffer(DeflaterConstants.PENDING_BUF_SIZE); + this.buffer = this.bufferMemoryOwner.Array; + this.bufferMemoryHandle = this.bufferMemoryOwner.Memory.Pin(); + this.pinnedBuffer = (byte*)this.bufferMemoryHandle.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() + { + if (!this.isDisposed) + { + 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 8e0bac938f..3c52d306f9 100644 --- a/src/ImageSharp/Formats/Png/Zlib/ZlibDeflateStream.cs +++ b/src/ImageSharp/Formats/Png/Zlib/ZlibDeflateStream.cs @@ -1,9 +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.IO; -using System.IO.Compression; +using SixLabors.Memory; namespace SixLabors.ImageSharp.Formats.Png.Zlib { @@ -38,14 +38,16 @@ 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. /// + /// 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; @@ -60,7 +62,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 @@ -78,29 +80,17 @@ 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. - 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); + this.deflateStream = new DeflaterOutputStream(memoryAllocator, this.rawStream, compressionLevel); } /// @@ -110,41 +100,36 @@ 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(); + } } /// - 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) @@ -164,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; @@ -184,11 +159,9 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib this.rawStream.WriteByte((byte)(crc & 0xFF)); } - base.Dispose(disposing); + this.deflateStream = null; - // Call the appropriate methods to clean up - // unmanaged resources here. - // Note disposing is done. + base.Dispose(disposing); this.isDisposed = true; } } 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/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/ITgaEncoderOptions.cs b/src/ImageSharp/Formats/Tga/ITgaEncoderOptions.cs new file mode 100644 index 0000000000..49983d2369 --- /dev/null +++ b/src/ImageSharp/Formats/Tga/ITgaEncoderOptions.cs @@ -0,0 +1,21 @@ +// 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; } + + /// + /// Gets a value indicating whether run length compression should be used. + /// + TgaCompression Compression { get; } + } +} diff --git a/src/ImageSharp/Formats/Tga/TGA_Specification.pdf b/src/ImageSharp/Formats/Tga/TGA_Specification.pdf new file mode 100644 index 0000000000..09c9a4ddda Binary files /dev/null and b/src/ImageSharp/Formats/Tga/TGA_Specification.pdf differ 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/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/TgaConfigurationModule.cs b/src/ImageSharp/Formats/Tga/TgaConfigurationModule.cs new file mode 100644 index 0000000000..18fbf4acd0 --- /dev/null +++ b/src/ImageSharp/Formats/Tga/TgaConfigurationModule.cs @@ -0,0 +1,19 @@ +// 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.SetEncoder(TgaFormat.Instance, new TgaEncoder()); + 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..5aabe92a1d --- /dev/null +++ b/src/ImageSharp/Formats/Tga/TgaConstants.cs @@ -0,0 +1,25 @@ +// 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" }; + + /// + /// The file header length of a tga image in bytes. + /// + public const int FileHeaderLength = 18; + } +} 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..d861450e04 --- /dev/null +++ b/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs @@ -0,0 +1,588 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Buffers; +using System.IO; +using System.Runtime.CompilerServices; + +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 tga specific metadata. + /// + private TgaMetadata tgaMetadata; + + /// + /// 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; + + /// + /// Initializes a new instance of the class. + /// + /// The configuration. + /// The 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 + { + bool inverted = this.ReadFileHeader(stream); + this.currentStream.Skip(this.fileHeader.IdLength); + + // 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"); + } + + 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(); + + if (this.fileHeader.ColorMapType is 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"); + } + + int colorMapPixelSizeInBytes = this.fileHeader.CMapDepth / 8; + int colorMapSizeInBytes = this.fileHeader.CMapLength * colorMapPixelSizeInBytes; + using (IManagedByteBuffer palette = this.memoryAllocator.AllocateManagedByteBuffer(colorMapSizeInBytes, AllocationOptions.Clean)) + { + 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; + } + + // 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: + if (this.fileHeader.ImageType.IsRunLengthEncoded()) + { + this.ReadRle(this.fileHeader.Width, this.fileHeader.Height, pixels, 1, inverted); + } + else + { + this.ReadMonoChrome(this.fileHeader.Width, this.fileHeader.Height, pixels, inverted); + } + + break; + + case 15: + case 16: + if (this.fileHeader.ImageType.IsRunLengthEncoded()) + { + this.ReadRle(this.fileHeader.Width, this.fileHeader.Height, pixels, 2, inverted); + } + else + { + this.ReadBgra16(this.fileHeader.Width, this.fileHeader.Height, pixels, inverted); + } + + break; + + case 24: + if (this.fileHeader.ImageType.IsRunLengthEncoded()) + { + this.ReadRle(this.fileHeader.Width, this.fileHeader.Height, pixels, 3, inverted); + } + else + { + this.ReadBgr24(this.fileHeader.Width, this.fileHeader.Height, pixels, inverted); + } + + break; + + case 32: + if (this.fileHeader.ImageType.IsRunLengthEncoded()) + { + this.ReadRle(this.fileHeader.Width, this.fileHeader.Height, pixels, 4, inverted); + } + else + { + this.ReadBgra32(this.fileHeader.Width, this.fileHeader.Height, pixels, inverted); + } + + break; + + default: + TgaThrowHelper.ThrowNotSupportedException("Does not support this kind of tga files."); + break; + } + + return image; + } + catch (IndexOutOfRangeException e) + { + throw new ImageFormatException("TGA image does not have a valid format.", e); + } + } + + /// + /// 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 + { + using (IManagedByteBuffer row = this.memoryAllocator.AllocateManagedByteBuffer(width, AllocationOptions.Clean)) + { + TPixel color = default; + Span rowSpan = row.GetSpan(); + + for (int y = 0; y < height; y++) + { + this.currentStream.Read(row); + int newY = Invert(y, height, inverted); + Span pixelRow = pixels.GetRowSpan(newY); + switch (colorMapPixelSizeInBytes) + { + case 2: + for (int x = 0; x < width; x++) + { + int colorIndex = rowSpan[x]; + + // 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); + pixelRow[x] = color; + } + + break; + + case 3: + for (int x = 0; x < width; x++) + { + int colorIndex = rowSpan[x]; + color.FromBgr24(Unsafe.As(ref palette[colorIndex * colorMapPixelSizeInBytes])); + pixelRow[x] = color; + } + + break; + + case 4: + for (int x = 0; x < width; x++) + { + int colorIndex = rowSpan[x]; + color.FromBgra32(Unsafe.As(ref palette[colorIndex * colorMapPixelSizeInBytes])); + pixelRow[x] = color; + } + + break; + } + } + } + } + + /// + /// 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 + { + 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++) + { + int newY = Invert(y, height, inverted); + Span pixelRow = pixels.GetRowSpan(newY); + 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: + // 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); + 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; + } + } + } + } + + /// + /// 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 + { + using (IManagedByteBuffer row = this.memoryAllocator.AllocatePaddedPixelRowBuffer(width, 1, 0)) + { + for (int y = 0; y < height; y++) + { + this.currentStream.Read(row); + int newY = Invert(y, height, inverted); + Span pixelSpan = pixels.GetRowSpan(newY); + PixelOperations.Instance.FromGray8Bytes( + this.configuration, + row.GetSpan(), + pixelSpan, + width); + } + } + } + + /// + /// 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 + { + using (IManagedByteBuffer row = this.memoryAllocator.AllocatePaddedPixelRowBuffer(width, 2, 0)) + { + 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, + rowSpan, + pixelSpan, + width); + } + } + } + + /// + /// 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 + { + using (IManagedByteBuffer row = this.memoryAllocator.AllocatePaddedPixelRowBuffer(width, 3, 0)) + { + for (int y = 0; y < height; y++) + { + this.currentStream.Read(row); + int newY = Invert(y, height, inverted); + Span pixelSpan = pixels.GetRowSpan(newY); + PixelOperations.Instance.FromBgr24Bytes( + this.configuration, + row.GetSpan(), + pixelSpan, + width); + } + } + } + + /// + /// 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 + { + using (IManagedByteBuffer row = this.memoryAllocator.AllocatePaddedPixelRowBuffer(width, 4, 0)) + { + for (int y = 0; y < height; y++) + { + this.currentStream.Read(row); + int newY = Invert(y, height, inverted); + Span pixelSpan = pixels.GetRowSpan(newY); + PixelOperations.Instance.FromBgra32Bytes( + this.configuration, + row.GetSpan(), + pixelSpan, + width); + } + } + } + + /// + /// 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 + { + TPixel color = default; + using (IMemoryOwner buffer = this.memoryAllocator.Allocate(width * height * bytesPerPixel, AllocationOptions.Clean)) + { + Span bufferSpan = buffer.GetSpan(); + this.UncompressRle(width, height, bufferSpan, bytesPerPixel); + for (int y = 0; y < height; y++) + { + int newY = Invert(y, height, inverted); + Span pixelRow = pixels.GetRowSpan(newY); + int rowStartIdx = y * width * bytesPerPixel; + for (int x = 0; x < width; x++) + { + int idx = rowStartIdx + (x * bytesPerPixel); + switch (bytesPerPixel) + { + case 1: + color.FromGray8(Unsafe.As(ref bufferSpan[idx])); + break; + case 2: + // 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; + case 3: + color.FromBgr24(Unsafe.As(ref bufferSpan[idx])); + break; + case 4: + color.FromBgra32(Unsafe.As(ref bufferSpan[idx])); + break; + } + + pixelRow[x] = color; + } + } + } + } + + /// + /// 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); + } + + /// + /// 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; + var pixel = new byte[bytesPerPixel]; + 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, bytesPerPixel); + int bufferIdx = uncompressedPixels * bytesPerPixel; + for (int i = 0; i < runLength + 1; i++, uncompressedPixels++) + { + pixel.AsSpan().CopyTo(buffer.Slice(bufferIdx)); + bufferIdx += bytesPerPixel; + } + } + else + { + // Non-run-length encoded packet. + int runLength = runLengthByte; + int bufferIdx = uncompressedPixels * bytesPerPixel; + for (int i = 0; i < runLength + 1; i++, uncompressedPixels++) + { + this.currentStream.Read(pixel, 0, bytesPerPixel); + pixel.AsSpan().CopyTo(buffer.Slice(bufferIdx)); + bufferIdx += bytesPerPixel; + } + } + } + } + + /// + /// 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; + + /// + /// Reads the tga file header from the stream. + /// + /// The containing image data. + /// true, if the image origin is top left. + private bool 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); + this.metadata = new ImageMetadata(); + this.tgaMetadata = this.metadata.GetFormatMetadata(TgaFormat.Instance); + this.tgaMetadata.BitsPerPixel = (TgaBitsPerPixel)this.fileHeader.PixelDepth; + + // 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; + } + + return false; + } + } +} diff --git a/src/ImageSharp/Formats/Tga/TgaEncoder.cs b/src/ImageSharp/Formats/Tga/TgaEncoder.cs new file mode 100644 index 0000000000..2fcbb822f5 --- /dev/null +++ b/src/ImageSharp/Formats/Tga/TgaEncoder.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.Advanced; +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 + { + /// + /// Gets or sets the number of bits per pixel. + /// + public TgaBitsPerPixel? BitsPerPixel { get; set; } + + /// + /// Gets or sets a value indicating whether no compression or run length compression should be used. + /// + public TgaCompression Compression { get; set; } = TgaCompression.RunLength; + + /// + 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..28b87e9857 --- /dev/null +++ b/src/ImageSharp/Formats/Tga/TgaEncoderCore.cs @@ -0,0 +1,348 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Buffers.Binary; +using System.IO; +using System.Numerics; +using System.Runtime.CompilerServices; + +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; + + /// + /// Reusable buffer for writing data. + /// + private readonly byte[] buffer = new byte[2]; + + /// + /// The color depth, in number of bits per pixel. + /// + private TgaBitsPerPixel? bitsPerPixel; + + /// + /// Indicates if run length compression should be used. + /// + private readonly TgaCompression compression; + + /// + /// 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; + this.compression = options.Compression; + } + + /// + /// 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 + { + 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; + + TgaImageType imageType = this.compression is TgaCompression.RunLength ? TgaImageType.RleTrueColor : TgaImageType.TrueColor; + if (this.bitsPerPixel == TgaBitsPerPixel.Pixel8) + { + 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.compression is TgaCompression.RunLength ? 32 : 0); + + var fileHeader = new TgaFileHeader( + idLength: 0, + colorMapType: 0, + imageType: imageType, + cMapStart: 0, + cMapLength: 0, + cMapDepth: 0, + xOffset: 0, + 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, + imageDescriptor: imageDescriptor); + +#if NETCOREAPP2_1 + Span buffer = stackalloc byte[TgaFileHeader.Size]; +#else + byte[] buffer = new byte[TgaFileHeader.Size]; +#endif + fileHeader.WriteTo(buffer); + + stream.Write(buffer, 0, TgaFileHeader.Size); + + if (this.compression is TgaCompression.RunLength) + { + this.WriteRunLengthEndcodedImage(stream, image.Frames.RootFrame); + } + else + { + 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.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; + } + } + + /// + /// 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 + { + Rgba32 color = default; + Buffer2D pixels = image.PixelBuffer; + Span pixelSpan = pixels.GetSpan(); + 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)); + + // Write the number of equal pixels, with the high bit set, indicating ist a compressed pixel run. + stream.WriteByte((byte)(equalPixelCount | 128)); + switch (this.bitsPerPixel) + { + case TgaBitsPerPixel.Pixel8: + int luminance = GetLuminance(currentPixel); + stream.WriteByte((byte)luminance); + break; + + case TgaBitsPerPixel.Pixel16: + var bgra5551 = new Bgra5551(color.ToVector4()); + BinaryPrimitives.TryWriteInt16LittleEndian(this.buffer, (short)bgra5551.PackedValue); + stream.WriteByte(this.buffer[0]); + stream.WriteByte(this.buffer[1]); + + 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; + } + } + + /// + /// 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 + { + 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); + + /// + /// 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. + /// + /// 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()); + } + } + } + + /// + /// 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()); + } + } + } + + /// + /// 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 + { + var vector = sourcePixel.ToVector4(); + return ImageMaths.GetBT709Luminance(ref vector, 256); + } + } +} diff --git a/src/ImageSharp/Formats/Tga/TgaFileHeader.cs b/src/ImageSharp/Formats/Tga/TgaFileHeader.cs new file mode 100644 index 0000000000..e2bbb6fbd2 --- /dev/null +++ b/src/ImageSharp/Formats/Tga/TgaFileHeader.cs @@ -0,0 +1,147 @@ +// 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.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 = TgaConstants.FileHeaderLength; + + 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]; + } + + public void WriteTo(Span buffer) + { + ref TgaFileHeader dest = ref Unsafe.As(ref MemoryMarshal.GetReference(buffer)); + + dest = this; + } + } +} 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..bd9cfa900c --- /dev/null +++ b/src/ImageSharp/Formats/Tga/TgaImageFormatDetector.cs @@ -0,0 +1,40 @@ +// 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 => TgaConstants.FileHeaderLength; + + /// + public IImageFormat DetectFormat(ReadOnlySpan header) + { + return this.IsSupportedFileFormat(header) ? TgaFormat.Instance : null; + } + + private bool IsSupportedFileFormat(ReadOnlySpan header) + { + if (header.Length >= this.HeaderSize) + { + // 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(); + } + + return true; + } + } +} diff --git a/src/ImageSharp/Formats/Tga/TgaImageType.cs b/src/ImageSharp/Formats/Tga/TgaImageType.cs new file mode 100644 index 0000000000..491fd3ea77 --- /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. + /// + 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/TgaImageTypeExtensions.cs b/src/ImageSharp/Formats/Tga/TgaImageTypeExtensions.cs new file mode 100644 index 0000000000..6a30cdddd7 --- /dev/null +++ b/src/ImageSharp/Formats/Tga/TgaImageTypeExtensions.cs @@ -0,0 +1,49 @@ +// 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; + } + + /// + /// 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) + { + switch (imageType) + { + case TgaImageType.NoImageData: + case TgaImageType.ColorMapped: + case TgaImageType.TrueColor: + case TgaImageType.BlackAndWhite: + case TgaImageType.RleColorMapped: + case TgaImageType.RleTrueColor: + case TgaImageType.RleBlackAndWhite: + return true; + + default: + return false; + } + } + } +} diff --git a/src/ImageSharp/Formats/Tga/TgaMetadata.cs b/src/ImageSharp/Formats/Tga/TgaMetadata.cs new file mode 100644 index 0000000000..4ce61d2e48 --- /dev/null +++ b/src/ImageSharp/Formats/Tga/TgaMetadata.cs @@ -0,0 +1,35 @@ +// 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() + { + } + + /// + /// 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() => new TgaMetadata(this); + } +} diff --git a/src/ImageSharp/Formats/Tga/TgaThrowHelper.cs b/src/ImageSharp/Formats/Tga/TgaThrowHelper.cs new file mode 100644 index 0000000000..845d009227 --- /dev/null +++ b/src/ImageSharp/Formats/Tga/TgaThrowHelper.cs @@ -0,0 +1,31 @@ +// 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 ThrowImageFormatException(string errorMessage) + { + throw new ImageFormatException(errorMessage); + } + + /// + /// 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); + } + } +} diff --git a/src/ImageSharp/GraphicsOptions.cs b/src/ImageSharp/GraphicsOptions.cs index 214b10810a..47b930e654 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; @@ -8,170 +8,82 @@ namespace SixLabors.ImageSharp /// /// Options for influencing the drawing functions. /// - public struct GraphicsOptions + public class GraphicsOptions : IDeepCloneable { - /// - /// Represents the default . - /// - public static readonly GraphicsOptions Default = new GraphicsOptions(true); - - private float? blendPercentage; - - private int? antialiasSubpixelDepth; - - 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.blendPercentage = 1; - this.antialiasSubpixelDepth = 16; - this.antialias = enableAntialiasing; - } - - /// - /// Initializes a new instance of the struct. - /// - /// If set to true [enable antialiasing]. - /// blending percentage to apply to the drawing operation - public GraphicsOptions(bool enableAntialiasing, float opacity) - { - Guard.MustBeBetweenOrEqualTo(opacity, 0, 1, nameof(opacity)); - - this.colorBlendingMode = PixelColorBlendingMode.Normal; - this.alphaCompositionMode = PixelAlphaCompositionMode.SrcOver; - this.blendPercentage = opacity; - this.antialiasSubpixelDepth = 16; - this.antialias = enableAntialiasing; - } + private int antialiasSubpixelDepth = 16; + private float blendPercentage = 1F; /// - /// 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) + public GraphicsOptions() { - 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. - /// - /// 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) + private GraphicsOptions(GraphicsOptions source) { - Guard.MustBeBetweenOrEqualTo(opacity, 0, 1, nameof(opacity)); - - this.colorBlendingMode = blending; - this.alphaCompositionMode = composition; - this.blendPercentage = opacity; - this.antialiasSubpixelDepth = 16; - this.antialias = enableAntialiasing; + this.AlphaCompositionMode = source.AlphaCompositionMode; + this.Antialias = source.Antialias; + this.AntialiasSubpixelDepth = source.AntialiasSubpixelDepth; + this.BlendPercentage = source.BlendPercentage; + this.ColorBlendingMode = source.ColorBlendingMode; } /// /// Gets or sets a value indicating whether antialiasing should be applied. + /// Defaults to true. /// - public bool Antialias - { - get => this.antialias ?? true; - set => this.antialias = value; - } + 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 { - 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 between indicating the blending percentage to apply to the drawing operation. + /// Range 0..1; Defaults to 1. /// public float BlendPercentage { - get => (this.blendPercentage ?? 1).Clamp(0, 1); - set => this.blendPercentage = value; - } + get + { + return this.blendPercentage; + } - // 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. + set + { + Guard.MustBeBetweenOrEqualTo(value, 0, 1F, nameof(this.BlendPercentage)); + this.blendPercentage = value; + } + } /// - /// 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 => this.colorBlendingMode; - set => this.colorBlendingMode = value; - } + 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 => this.alphaCompositionMode; - set => this.alphaCompositionMode = value; - } + public PixelAlphaCompositionMode AlphaCompositionMode { get; set; } = PixelAlphaCompositionMode.SrcOver; - /// - /// 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 GraphicsOptions DeepClone() => new GraphicsOptions(this); } -} \ No newline at end of file +} 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 +} 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/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 +} diff --git a/src/ImageSharp/Processing/Extensions/BackgroundColorExtensions.cs b/src/ImageSharp/Processing/Extensions/BackgroundColorExtensions.cs index dd1cc1ed24..4241721f46 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. @@ -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); } -} \ No newline at end of file +} diff --git a/src/ImageSharp/Processing/Extensions/GlowExtensions.cs b/src/ImageSharp/Processing/Extensions/GlowExtensions.cs index 39734882b0..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; @@ -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. @@ -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 74a59d3e13..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; @@ -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. @@ -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/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)); } } 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 0958e3aa9e..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, GraphicsOptions.Default) + : 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 2365318f3d..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, GraphicsOptions.Default) + : 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.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/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 +} 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/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.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 @@ + diff --git a/tests/ImageSharp.Tests/ConfigurationTests.cs b/tests/ImageSharp.Tests/ConfigurationTests.cs index a0e552aebf..6b35bbb972 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, @@ -108,14 +110,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] @@ -123,7 +124,7 @@ namespace SixLabors.ImageSharp.Tests { Configuration config = Configuration.CreateDefaultInstance(); - Assert.Equal(4, config.ImageFormats.Count()); + Assert.Equal(expectedDefaultConfigurationCount, config.ImageFormats.Count()); } [Fact] 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/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 361e7e70d1..031e732eaa 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,44 @@ 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 + { + Antialias = 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/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..e259d29d9c 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,11 +33,12 @@ 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); + var processor = new FillRegionProcessor(options, brush.Object, region); var img = new Image(1, 1); processor.Execute(img, bounds); @@ -51,8 +50,8 @@ namespace SixLabors.ImageSharp.Tests.Drawing { var bounds = new Rectangle(-100, -10, 10, 10); var brush = new Mock(); - var options = new GraphicsOptions(true); - var processor = new FillRegionProcessor(brush.Object, new MockRegion1(), options); + var options = new GraphicsOptions { Antialias = true }; + var processor = new FillRegionProcessor(options, brush.Object, new MockRegion1()); var img = new Image(10, 10); processor.Execute(img, bounds); } @@ -73,11 +72,12 @@ 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)); - 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 3691b54ce3..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(); + 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 160ff22a3e..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(); + 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 b76ee8ffcd..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(); + 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 c62a871481..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(); + 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 17a2b87c0e..6b08323b68 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; @@ -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(); - 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/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..a59afb271d 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,184 @@ namespace SixLabors.ImageSharp.Tests.Drawing.Text { public class TextGraphicsOptionsTests { + private readonly TextGraphicsOptions newTextGraphicsOptions = new TextGraphicsOptions(); + private readonly TextGraphicsOptions cloneTextGraphicsOptions = new TextGraphicsOptions().DeepClone(); + + [Fact] + public void CloneTextGraphicsOptionsIsNotNull() => Assert.True(this.cloneTextGraphicsOptions != null); + + [Fact] + public void DefaultTextGraphicsOptionsAntialias() + { + Assert.True(this.newTextGraphicsOptions.Antialias); + Assert.True(this.cloneTextGraphicsOptions.Antialias); + } + + [Fact] + public void DefaultTextGraphicsOptionsAntialiasSuppixelDepth() + { + const int Expected = 16; + Assert.Equal(Expected, this.newTextGraphicsOptions.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.cloneTextGraphicsOptions.BlendPercentage); + } + + [Fact] + public void DefaultTextGraphicsOptionsColorBlendingMode() + { + const PixelColorBlendingMode Expected = PixelColorBlendingMode.Normal; + Assert.Equal(Expected, this.newTextGraphicsOptions.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.cloneTextGraphicsOptions.AlphaCompositionMode); + } + + [Fact] + public void DefaultTextGraphicsOptionsApplyKerning() + { + const bool Expected = true; + Assert.Equal(Expected, this.newTextGraphicsOptions.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.cloneTextGraphicsOptions.HorizontalAlignment); + } + + [Fact] + public void DefaultTextGraphicsOptionsVerticalAlignment() + { + const VerticalAlignment Expected = VerticalAlignment.Top; + Assert.Equal(Expected, this.newTextGraphicsOptions.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.cloneTextGraphicsOptions.DpiX); + } + + [Fact] + public void DefaultTextGraphicsOptionsDpiY() + { + const float Expected = 72F; + Assert.Equal(Expected, this.newTextGraphicsOptions.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.cloneTextGraphicsOptions.TabWidth); + } + + [Fact] + public void DefaultTextGraphicsOptionsWrapTextWidth() + { + const float Expected = 0F; + Assert.Equal(Expected, this.newTextGraphicsOptions.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() { - 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 +196,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 +208,4 @@ namespace SixLabors.ImageSharp.Tests.Drawing.Text Assert.Equal(99, opt.AntialiasSubpixelDepth); } } -} \ No newline at end of file +} diff --git a/tests/ImageSharp.Tests/Formats/Bmp/BmpFileHeaderTests.cs b/tests/ImageSharp.Tests/Formats/Bmp/BmpFileHeaderTests.cs index 25cf29406e..4c3fe31493 100644 --- a/tests/ImageSharp.Tests/Formats/Bmp/BmpFileHeaderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Bmp/BmpFileHeaderTests.cs @@ -1,4 +1,11 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + using System; +using System.IO; +using System.Linq; + +using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.Formats.Bmp; using Xunit; 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 2a76310fcd..bdd84038e3 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,19 @@ namespace SixLabors.ImageSharp.Tests.Formats.Png using (Image image = provider.GetImage(new PngDecoder())) { image.DebugSave(provider); - image.CompareToOriginal(provider, ImageComparer.Exact); + + // 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) + { + if (TestEnvironment.IsWindows) + { + image.CompareToOriginal(provider, ImageComparer.Exact, (IImageDecoder)SystemDrawingReferenceDecoder.Instance); + } + } + else + { + image.CompareToOriginal(provider, ImageComparer.Exact); + } } } @@ -218,7 +231,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); 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 diff --git a/tests/ImageSharp.Tests/Formats/Tga/TgaDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Tga/TgaDecoderTests.cs new file mode 100644 index 0000000000..03ad10de40 --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Tga/TgaDecoderTests.cs @@ -0,0 +1,197 @@ +// 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; + +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); + TgaTestUtils.CompareWithReferenceDecoder(provider, image); + } + } + + [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); + TgaTestUtils.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); + TgaTestUtils.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); + TgaTestUtils.CompareWithReferenceDecoder(provider, image); + } + } + + [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); + TgaTestUtils.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); + TgaTestUtils.CompareWithReferenceDecoder(provider, image); + } + } + + [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); + TgaTestUtils.CompareWithReferenceDecoder(provider, image); + } + } + + [Theory] + [WithFile(Bit24TopLeft, PixelTypes.Rgba32)] + public void TgaDecoder_CanDecode_Palette_WithTopLeftOrigin_24Bit(TestImageProvider provider) + where TPixel : struct, IPixel + { + using (Image image = provider.GetImage(new TgaDecoder())) + { + image.DebugSave(provider); + TgaTestUtils.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); + TgaTestUtils.CompareWithReferenceDecoder(provider, image); + } + } + + [Theory] + [WithFile(GreyRle, PixelTypes.Rgba32)] + public void TgaDecoder_CanDecode_RunLengthEncoded_MonoChrome(TestImageProvider provider) + where TPixel : struct, IPixel + { + using (Image image = provider.GetImage(new TgaDecoder())) + { + image.DebugSave(provider); + TgaTestUtils.CompareWithReferenceDecoder(provider, image); + } + } + + [Theory] + [WithFile(Bit16Rle, PixelTypes.Rgba32)] + public void TgaDecoder_CanDecode_RunLengthEncoded_16Bit(TestImageProvider provider) + where TPixel : struct, IPixel + { + using (Image image = provider.GetImage(new TgaDecoder())) + { + image.DebugSave(provider); + TgaTestUtils.CompareWithReferenceDecoder(provider, image); + } + } + + [Theory] + [WithFile(Bit24Rle, PixelTypes.Rgba32)] + public void TgaDecoder_CanDecode_RunLengthEncoded_24Bit(TestImageProvider provider) + where TPixel : struct, IPixel + { + using (Image image = provider.GetImage(new TgaDecoder())) + { + image.DebugSave(provider); + TgaTestUtils.CompareWithReferenceDecoder(provider, image); + } + } + + [Theory] + [WithFile(Bit32Rle, PixelTypes.Rgba32)] + public void TgaDecoder_CanDecode_RunLengthEncoded_32Bit(TestImageProvider provider) + where TPixel : struct, IPixel + { + using (Image image = provider.GetImage(new TgaDecoder())) + { + image.DebugSave(provider); + TgaTestUtils.CompareWithReferenceDecoder(provider, image); + } + } + + [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); + TgaTestUtils.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); + TgaTestUtils.CompareWithReferenceDecoder(provider, image); + } + } + } +} diff --git a/tests/ImageSharp.Tests/Formats/Tga/TgaEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Tga/TgaEncoderTests.cs new file mode 100644 index 0000000000..e946729a15 --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Tga/TgaEncoderTests.cs @@ -0,0 +1,148 @@ +// 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; +using SixLabors.ImageSharp.PixelFormats; + +using Xunit; + +namespace SixLabors.ImageSharp.Tests.Formats.Tga +{ + using static TestImages.Tga; + + public class TgaEncoderTests + { + public static readonly TheoryData BitsPerPixel = + new TheoryData + { + TgaBitsPerPixel.Pixel24, + TgaBitsPerPixel.Pixel32 + }; + + 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() + { + Compression = TgaCompression.RunLength + }; + + 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] + [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, 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, 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, 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, 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, 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, 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, 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, TgaCompression.RunLength); + + private static void TestTgaEncoderCore( + TestImageProvider provider, + TgaBitsPerPixel bitsPerPixel, + 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, Compression = compression}; + + using (var memStream = new MemoryStream()) + { + image.Save(memStream, encoder); + memStream.Position = 0; + using (var encodedImage = (Image)Image.Load(memStream)) + { + TgaTestUtils.CompareWithReferenceDecoder(provider, encodedImage, useExactComparer, compareTolerance); + } + } + } + } + } +} diff --git a/tests/ImageSharp.Tests/Formats/Tga/TgaFileHeaderTests.cs b/tests/ImageSharp.Tests/Formats/Tga/TgaFileHeaderTests.cs new file mode 100644 index 0000000000..c227b79576 --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Tga/TgaFileHeaderTests.cs @@ -0,0 +1,33 @@ +// 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, + 15 // invalid tga image type + }; + + private MemoryStream Stream { get; } = new MemoryStream(Data); + + [Fact] + public void ImageLoad_WithInvalidImageType_Throws_UnknownImageFormatException() + { + Assert.Throws(() => + { + using (Image.Load(Configuration.Default, this.Stream, out IImageFormat _)) + { + } + }); + } + } +} diff --git a/tests/ImageSharp.Tests/Formats/Tga/TgaTestUtils.cs b/tests/ImageSharp.Tests/Formats/Tga/TgaTestUtils.cs new file mode 100644 index 0000000000..a2f2e86d7d --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Tga/TgaTestUtils.cs @@ -0,0 +1,61 @@ +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, + bool useExactComparer = true, + float compareTolerance = 0.01f) + 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)); + if (useExactComparer) + { + ImageComparer.Exact.VerifySimilarity(magickImage, image); + } + else + { + ImageComparer.Tolerant(compareTolerance).VerifySimilarity(magickImage, image); + } + } + + 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; + } + } + } +} diff --git a/tests/ImageSharp.Tests/GraphicsOptionsTests.cs b/tests/ImageSharp.Tests/GraphicsOptionsTests.cs index 6ff38626d6..69f904f1cb 100644 --- a/tests/ImageSharp.Tests/GraphicsOptionsTests.cs +++ b/tests/ImageSharp.Tests/GraphicsOptionsTests.cs @@ -1,21 +1,100 @@ -// 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.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 cloneGraphicsOptions = new GraphicsOptions().DeepClone(); + + [Fact] + public void CloneGraphicsOptionsIsNotNull() => Assert.True(this.cloneGraphicsOptions != null); + + [Fact] + public void DefaultGraphicsOptionsAntialias() + { + Assert.True(this.newGraphicsOptions.Antialias); + Assert.True(this.cloneGraphicsOptions.Antialias); + } + + [Fact] + public void DefaultGraphicsOptionsAntialiasSuppixelDepth() + { + const int Expected = 16; + Assert.Equal(Expected, this.newGraphicsOptions.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.cloneGraphicsOptions.BlendPercentage); + } + + [Fact] + public void DefaultGraphicsOptionsColorBlendingMode() + { + const PixelColorBlendingMode Expected = PixelColorBlendingMode.Normal; + Assert.Equal(Expected, this.newGraphicsOptions.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.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() { - 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/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 +} 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/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 +} 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); 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/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs index 146f2efcdb..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 = @@ -365,5 +366,24 @@ namespace SixLabors.ImageSharp.Tests public static readonly string[] All = { Rings, Giphy, Cheers, Trans, Kumin, Leo, Ratio4x1, Ratio1x4 }; } + + public static class 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 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"; + 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/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(); + } +} 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/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 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 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_pal_origin_topleft.tga b/tests/Images/Input/Tga/targa_24bit_pal_origin_topleft.tga new file mode 100644 index 0000000000..b8c4071745 --- /dev/null +++ b/tests/Images/Input/Tga/targa_24bit_pal_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.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_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 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