diff --git a/Samples/SVGViewer/DebugRenderer.cs b/Samples/SVGViewer/DebugRenderer.cs index 34785eaa3..9419362c7 100644 --- a/Samples/SVGViewer/DebugRenderer.cs +++ b/Samples/SVGViewer/DebugRenderer.cs @@ -110,6 +110,14 @@ public Matrix Transform } } + public SizeF RenderSize + { + get + { + return new SizeF(0, 0); + } + } + public void Dispose() { if (_clip != null) @@ -117,5 +125,18 @@ public void Dispose() if (_transform != null) _transform.Dispose(); } + + public Bitmap GetMask() + { + return null; + } + + public void SetMask(Bitmap mask) + { + } + + public void DisposeMask() + { + } } } diff --git a/Source/Basic Shapes/SvgVisualElement.cs b/Source/Basic Shapes/SvgVisualElement.cs index 8e1386483..eab769f81 100644 --- a/Source/Basic Shapes/SvgVisualElement.cs +++ b/Source/Basic Shapes/SvgVisualElement.cs @@ -16,6 +16,7 @@ public abstract partial class SvgVisualElement : SvgElement, ISvgBoundable, ISvg { private bool? _requiresSmoothRendering; private Region _previousClip; + private Bitmap _previousMask; /// /// Gets the for this element. @@ -64,6 +65,16 @@ public virtual Uri ClipPath set { Attributes["clip-path"] = value; } } + /// + /// Gets the associated if one has been specified. + /// + [SvgAttribute("mask")] + public virtual Uri Mask + { + get { return GetAttribute("mask", false); } + set { Attributes["mask"] = value; } + } + /// /// Gets or sets the algorithm which is to be used to determine the clipping region. /// @@ -179,7 +190,15 @@ private void RenderInternal(ISvgRenderer renderer, Action renderMe if (PushTransforms(renderer)) { SetClip(renderer); + + var isMaskSet = SetMask(renderer); renderMethod.Invoke(renderer); + + if (isMaskSet) + { + ResetMask(renderer); + } + ResetClip(renderer); } } @@ -300,7 +319,7 @@ protected internal virtual bool RenderStroke(ISvgRenderer renderer) strokeWidth = Math.Max(strokeWidth, 1f); /* divide by stroke width - GDI uses stroke width as unit.*/ - var dashPattern = strokeDashArray.Select(u => ((u.ToDeviceValue(renderer, UnitRenderingType.Other, this) <= 0f) ? 1f : + var dashPattern = strokeDashArray.Select(u => ((u.ToDeviceValue(renderer, UnitRenderingType.Other, this) <= 0f) ? 1f : u.ToDeviceValue(renderer, UnitRenderingType.Other, this)) / strokeWidth).ToArray(); var length = dashPattern.Length; @@ -362,7 +381,7 @@ protected internal virtual bool RenderStroke(ISvgRenderer renderer) if (dashOffset != 0f) { - pen.DashOffset = ((dashOffset.ToDeviceValue(renderer, UnitRenderingType.Other, this) <= 0f) ? 1f : + pen.DashOffset = ((dashOffset.ToDeviceValue(renderer, UnitRenderingType.Other, this) <= 0f) ? 1f : dashOffset.ToDeviceValue(renderer, UnitRenderingType.Other, this)) / strokeWidth; } } @@ -454,6 +473,32 @@ protected internal virtual void ResetClip(ISvgRenderer renderer) } } + protected internal virtual bool SetMask(ISvgRenderer renderer) + { + var maskPath = this.Mask.ReplaceWithNullIfNone(); + + if (maskPath != null) + { + this._previousMask = renderer.GetMask(); + + var element = this.OwnerDocument.GetElementById(maskPath.ToString()); + if (element != null) + { + renderer.SetMask(element.RenderMask(renderer)); + + return true; + } + } + + return false; + } + + protected internal virtual void ResetMask(ISvgRenderer renderer) + { + renderer.DisposeMask(); + renderer.SetMask(this._previousMask); + } + /// /// Sets the clipping region of the specified . /// diff --git a/Source/Clipping and Masking/SvgMask.cs b/Source/Clipping and Masking/SvgMask.cs index 788ab6790..4a3df52c6 100644 --- a/Source/Clipping and Masking/SvgMask.cs +++ b/Source/Clipping and Masking/SvgMask.cs @@ -1,3 +1,6 @@ +using System; +using System.Drawing; + namespace Svg { /// @@ -70,5 +73,30 @@ public override SvgElement DeepCopy() { return DeepCopy(); } + + /// + /// Renders the mask to a bitmap. + /// + /// The current renderer. + /// A object. + public Bitmap RenderMask(ISvgRenderer renderer) + { + var boundable = renderer.GetBoundable(); + + var maskBitmap = new Bitmap((int)Math.Round(renderer.RenderSize.Width), (int)Math.Round(renderer.RenderSize.Height)); + + var graphics = Graphics.FromImage(maskBitmap); + graphics.Transform = renderer.Transform.Clone(); + graphics.Clear(System.Drawing.Color.Black); + + using (var maskRenderer = SvgRenderer.FromGraphics(graphics, renderer.RenderSize)) + { + maskRenderer.SetBoundable(boundable); + + RenderElement(maskRenderer); + } + + return maskBitmap; + } } } diff --git a/Source/Document Structure/SvgImage.cs b/Source/Document Structure/SvgImage.cs index 801079eaa..10ed9cbed 100644 --- a/Source/Document Structure/SvgImage.cs +++ b/Source/Document Structure/SvgImage.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Diagnostics; using System.Drawing; @@ -146,6 +146,7 @@ protected override void Render(ISvgRenderer renderer) var destRect = destClip; renderer.SetClip(new Region(destClip), CombineMode.Intersect); SetClip(renderer); + SetMask(renderer); var aspectRatio = AspectRatio; if (aspectRatio.Align != SvgPreserveAspectRatio.none) @@ -227,6 +228,7 @@ protected override void Render(ISvgRenderer renderer) } } + ResetMask(renderer); ResetClip(renderer); } } diff --git a/Source/ExtensionMethods/PointFExtensions.cs b/Source/ExtensionMethods/PointFExtensions.cs new file mode 100644 index 000000000..80423baf7 --- /dev/null +++ b/Source/ExtensionMethods/PointFExtensions.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Linq; +using System.Text; + +namespace Svg.ExtensionMethods +{ + internal static class PointFExtensions + { + internal static RectangleF GetBounds(this IEnumerable points) + { + var minX = points.Min(point => point.X); + var maxX = points.Max(point => point.X); + var minY = points.Min(point => point.Y); + var maxY = points.Max(point => point.Y); + + return new RectangleF(minX, minY, maxX - minX, maxY - minY); + } + } +} diff --git a/Source/ExtensionMethods/RectangleFExtensions.cs b/Source/ExtensionMethods/RectangleFExtensions.cs new file mode 100644 index 000000000..69a316ca4 --- /dev/null +++ b/Source/ExtensionMethods/RectangleFExtensions.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Drawing.Drawing2D; +using System.Text; + +namespace Svg.ExtensionMethods +{ + internal static class RectangleFExtensions + { + internal static PointF[] GetPoints(this RectangleF rectangle) + { + return new[] + { + rectangle.Location, + rectangle.Location + rectangle.Size + }; + } + + internal static RectangleF Transform(this RectangleF rectangle, Matrix matrix) + { + var points = GetPoints(rectangle); + matrix.TransformPoints(points); + return points.GetBounds(); + } + } +} diff --git a/Source/Rendering/ISvgRenderer.cs b/Source/Rendering/ISvgRenderer.cs index 2a0c068c4..9a6f7f1aa 100644 --- a/Source/Rendering/ISvgRenderer.cs +++ b/Source/Rendering/ISvgRenderer.cs @@ -13,13 +13,17 @@ public interface ISvgRenderer : IDisposable void FillPath(Brush brush, GraphicsPath path); ISvgBoundable GetBoundable(); Region GetClip(); + Bitmap GetMask(); ISvgBoundable PopBoundable(); void RotateTransform(float fAngle, MatrixOrder order = MatrixOrder.Append); void ScaleTransform(float sx, float sy, MatrixOrder order = MatrixOrder.Append); void SetBoundable(ISvgBoundable boundable); void SetClip(Region region, CombineMode combineMode = CombineMode.Replace); + void SetMask(Bitmap mask); + void DisposeMask(); SmoothingMode SmoothingMode { get; set; } Matrix Transform { get; set; } + SizeF RenderSize { get; } void TranslateTransform(float dx, float dy, MatrixOrder order = MatrixOrder.Append); void DrawImage(Image image, RectangleF destRect, RectangleF srcRect, GraphicsUnit graphicsUnit, float opacity); } diff --git a/Source/Rendering/SvgRenderer.cs b/Source/Rendering/SvgRenderer.cs index 2fb42fce4..f40cd6a44 100644 --- a/Source/Rendering/SvgRenderer.cs +++ b/Source/Rendering/SvgRenderer.cs @@ -1,8 +1,11 @@ -using System.Collections.Generic; +using Svg.ExtensionMethods; +using System; +using System.Collections.Generic; using System.Drawing; using System.Drawing.Drawing2D; using System.Drawing.Imaging; using System.Drawing.Text; +using System.Runtime.InteropServices; namespace Svg { @@ -14,6 +17,7 @@ public sealed class SvgRenderer : ISvgRenderer, IGraphicsProvider private readonly Graphics _innerGraphics; private readonly bool _disposable; private readonly Image _image; + private Bitmap _mask; private readonly Stack _boundables = new Stack(); @@ -35,23 +39,31 @@ public float DpiY get { return _innerGraphics.DpiY; } } + public SizeF RenderSize + { + get; + private set; + } + /// /// Initializes a new instance of the class. /// - private SvgRenderer(Graphics graphics, bool disposable = true) + private SvgRenderer(Graphics graphics, SizeF renderSize, bool disposable = true) { _innerGraphics = graphics; _disposable = disposable; + this.RenderSize = renderSize; } private SvgRenderer(Graphics graphics, Image image) - : this(graphics) + : this(graphics, new SizeF(image.Width, image.Height)) { _image = image; } public void DrawImage(Image image, RectangleF destRect, RectangleF srcRect, GraphicsUnit graphicsUnit) { - _innerGraphics.DrawImage(image, destRect, srcRect, graphicsUnit); + var bounds = destRect.Transform(this.Transform); + DrawMasked(graphics => graphics.DrawImage(image, destRect, srcRect, graphicsUnit), bounds); } public void DrawImage(Image image, RectangleF destRect, RectangleF srcRect, GraphicsUnit graphicsUnit, float opacity) { @@ -73,13 +85,17 @@ public void DrawImageUnscaled(Image image, Point location) { _innerGraphics.DrawImageUnscaled(image, location); } + public void DrawPath(Pen pen, GraphicsPath path) { - _innerGraphics.DrawPath(pen, path); + var bounds = path.GetBounds(this.Transform, pen); + DrawMasked(graphics => graphics.DrawPath(pen, path), bounds); } + public void FillPath(Brush brush, GraphicsPath path) { - _innerGraphics.FillPath(brush, path); + var bounds = path.GetBounds(this.Transform); + DrawMasked(graphics => graphics.FillPath(brush, path), bounds); } public Region GetClip() { @@ -144,16 +160,17 @@ private static Graphics CreateGraphics(Image image) public static ISvgRenderer FromImage(Image image) { var g = CreateGraphics(image); - return new SvgRenderer(g); + return new SvgRenderer(g, new Size(image.Width, image.Height)); } /// /// Creates a new from the specified . /// /// The to create the renderer from. - public static ISvgRenderer FromGraphics(Graphics graphics) + /// The size of the rendered image. + public static ISvgRenderer FromGraphics(Graphics graphics, SizeF renderSize) { - return new SvgRenderer(graphics, false); + return new SvgRenderer(graphics, renderSize, false); } public static ISvgRenderer FromNull() @@ -162,5 +179,90 @@ public static ISvgRenderer FromNull() var g = CreateGraphics(img); return new SvgRenderer(g, img); } + + public void SetMask(Bitmap mask) + { + _mask = mask; + } + + public Bitmap GetMask() + { + return _mask; + } + + public void DisposeMask() + { + if (_mask != null) + { + _mask.Dispose(); + } + } + + private void DrawMasked(Action drawAction, RectangleF bounds) + { + if (_mask == null) + { + drawAction(_innerGraphics); + return; + } + + var fullBounds = new RectangleF(new PointF(), this.RenderSize); + var renderedBounds = Rectangle.Round(RectangleF.Intersect(bounds, fullBounds)); + + if (renderedBounds.Width == 0 || renderedBounds.Height == 0) + { + return; + } + + using (var buffer = new Bitmap(renderedBounds.Width, renderedBounds.Height, PixelFormat.Format32bppArgb)) + { + var localTransform = new Matrix(); + localTransform.Translate(-renderedBounds.X, -renderedBounds.Y); + localTransform.Multiply(this.Transform); + + var bufferGraphics = Graphics.FromImage(buffer); + + bufferGraphics.Transform = localTransform; + drawAction(bufferGraphics); + + ApplyAlphaMask(buffer, _mask, renderedBounds); + + var previousTransform = _innerGraphics.Transform; + _innerGraphics.Transform = new Matrix(); + _innerGraphics.DrawImageUnscaled(buffer, new Point(renderedBounds.X, renderedBounds.Y)); + _innerGraphics.Transform = previousTransform; + } + } + + private void ApplyAlphaMask(Bitmap buffer, Bitmap mask, Rectangle renderedBounds) + { + var bufferData = buffer.LockBits(new Rectangle(0, 0, buffer.Width, buffer.Height), ImageLockMode.ReadWrite, PixelFormat.Format32bppArgb); + var maskData = mask.LockBits(new Rectangle(0, 0, mask.Width, mask.Height), ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb); + + var bufferBytes = new byte[buffer.Width * buffer.Height * 4]; + var maskBytes = new byte[mask.Width * mask.Height * 4]; + + Marshal.Copy(bufferData.Scan0, bufferBytes, 0, bufferBytes.Length); + Marshal.Copy(maskData.Scan0, maskBytes, 0, maskBytes.Length); + + for (int y = 0; y < renderedBounds.Height; y++) + { + for (int x = 0; x < renderedBounds.Width; x++) + { + var bufferPixelAddress = (y * buffer.Width + x) * 4; + var maskPixelAddress = ((y + renderedBounds.Y) * mask.Width + (x + renderedBounds.X)) * 4; + + var alpha = (maskBytes[maskPixelAddress] + maskBytes[maskPixelAddress + 1] + maskBytes[maskPixelAddress + 2]) / 3; + var newAlpha = (byte)(bufferBytes[bufferPixelAddress + 3] * alpha / 255); + + bufferBytes[bufferPixelAddress + 3] = newAlpha; + } + } + + Marshal.Copy(bufferBytes, 0, bufferData.Scan0, bufferBytes.Length); + + buffer.UnlockBits(bufferData); + mask.UnlockBits(maskData); + } } } diff --git a/Source/SvgDocument.cs b/Source/SvgDocument.cs index 24377d0d6..7b290952d 100644 --- a/Source/SvgDocument.cs +++ b/Source/SvgDocument.cs @@ -590,7 +590,7 @@ public void Draw(Graphics graphics, SizeF? size) throw new ArgumentNullException("graphics"); } - using (var renderer = SvgRenderer.FromGraphics(graphics)) + using (var renderer = SvgRenderer.FromGraphics(graphics, size.HasValue ? size.Value : this.Bounds.Size)) { var boundable = size.HasValue ? (ISvgBoundable)new GenericBoundable(0, 0, size.Value.Width, size.Value.Height) : this; this.Draw(renderer, boundable); diff --git a/Tests/Svg.UnitTests/MaskingTests.cs b/Tests/Svg.UnitTests/MaskingTests.cs new file mode 100644 index 000000000..73c6aaca3 --- /dev/null +++ b/Tests/Svg.UnitTests/MaskingTests.cs @@ -0,0 +1,79 @@ +using NUnit.Framework; +using System.Drawing; +using System.Drawing.Imaging; +using System.IO; + +namespace Svg.UnitTests +{ + /// + /// Test class to test rendering masks (see issue 482). + /// + [TestFixture] + public class MaskingTests : SvgTestHelper + { + [Test] + public void RenderTestFileFromIssue482() + { + var document = OpenSvg(GetXMLDocFromResource(GetFullResourceString("Issue482_MasksNotRendered.TestSvgMasks.svg"))); + + var renderedDocument = document.Draw(); + var expectedImage = GetBitmapFromResource("Issue482_MasksNotRendered.TestSvgMasks.png"); + + float equalPercentage; + Bitmap difference; + + ImagesAreEqual(renderedDocument, expectedImage, 1, out equalPercentage, out difference); + + Assert.Greater(equalPercentage, 99); + } + + [Test] + public void RenderVariousElementsDefaultSize() + { + var document = OpenSvg(GetXMLDocFromResource(GetFullResourceString("Issue482_MasksNotRendered.VariousElements.svg"))); + + var renderedDocument = document.Draw(); + var expectedImage = GetBitmapFromResource("Issue482_MasksNotRendered.VariousElements.png"); + + float equalPercentage; + Bitmap difference; + + ImagesAreEqual(renderedDocument, expectedImage, 1, out equalPercentage, out difference); + + Assert.Greater(equalPercentage, 99); + } + + [Test] + public void RenderVariousElementsUpscaled() + { + var document = OpenSvg(GetXMLDocFromResource(GetFullResourceString("Issue482_MasksNotRendered.VariousElements.svg"))); + + var renderedDocument = document.Draw(1440, 2560); + var expectedImage = GetBitmapFromResource("Issue482_MasksNotRendered.VariousElements_1440x2560.png"); + + float equalPercentage; + Bitmap difference; + + ImagesAreEqual(renderedDocument, expectedImage, 1, out equalPercentage, out difference); + + Assert.Greater(equalPercentage, 99); + } + + [Test] + public void RenderPcb() + { + var document = OpenSvg(GetXMLDocFromResource(GetFullResourceString("Issue482_MasksNotRendered.PCB.svg"))); + + var renderedDocument = document.Draw(1440, 2560); + + var expectedImage = GetBitmapFromResource("Issue482_MasksNotRendered.PCB.png"); + + float equalPercentage; + Bitmap difference; + + ImagesAreEqual(renderedDocument, expectedImage, 1, out equalPercentage, out difference); + + Assert.Greater(equalPercentage, 99); + } + } +} \ No newline at end of file diff --git a/Tests/Svg.UnitTests/Resources/Issue482_MasksNotRendered/PCB.png b/Tests/Svg.UnitTests/Resources/Issue482_MasksNotRendered/PCB.png new file mode 100644 index 000000000..1a635e73b Binary files /dev/null and b/Tests/Svg.UnitTests/Resources/Issue482_MasksNotRendered/PCB.png differ diff --git a/Tests/Svg.UnitTests/Resources/Issue482_MasksNotRendered/PCB.svg b/Tests/Svg.UnitTests/Resources/Issue482_MasksNotRendered/PCB.svg new file mode 100644 index 000000000..9d5f5c415 --- /dev/null +++ b/Tests/Svg.UnitTests/Resources/Issue482_MasksNotRendered/PCB.svg @@ -0,0 +1,389 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Tests/Svg.UnitTests/Resources/Issue482_MasksNotRendered/TestSvgMasks.png b/Tests/Svg.UnitTests/Resources/Issue482_MasksNotRendered/TestSvgMasks.png new file mode 100644 index 000000000..8a5a25911 Binary files /dev/null and b/Tests/Svg.UnitTests/Resources/Issue482_MasksNotRendered/TestSvgMasks.png differ diff --git a/Tests/Svg.UnitTests/Resources/Issue482_MasksNotRendered/TestSvgMasks.svg b/Tests/Svg.UnitTests/Resources/Issue482_MasksNotRendered/TestSvgMasks.svg new file mode 100644 index 000000000..6e0dd33a2 --- /dev/null +++ b/Tests/Svg.UnitTests/Resources/Issue482_MasksNotRendered/TestSvgMasks.svg @@ -0,0 +1 @@ +Created with SnapCircle1Diameter 32mmSAAnalysis1PT 17.4°PI 57.9°LL -56°PI-LL 1.9°SVA -69mmLine1Length 62.3mmAngle 40.8°Wedge1Angle 38.5° \ No newline at end of file diff --git a/Tests/Svg.UnitTests/Resources/Issue482_MasksNotRendered/VariousElements.png b/Tests/Svg.UnitTests/Resources/Issue482_MasksNotRendered/VariousElements.png new file mode 100644 index 000000000..0589d8568 Binary files /dev/null and b/Tests/Svg.UnitTests/Resources/Issue482_MasksNotRendered/VariousElements.png differ diff --git a/Tests/Svg.UnitTests/Resources/Issue482_MasksNotRendered/VariousElements.svg b/Tests/Svg.UnitTests/Resources/Issue482_MasksNotRendered/VariousElements.svg new file mode 100644 index 000000000..bab4aad35 --- /dev/null +++ b/Tests/Svg.UnitTests/Resources/Issue482_MasksNotRendered/VariousElements.svg @@ -0,0 +1,1889 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/Svg.UnitTests/Resources/Issue482_MasksNotRendered/VariousElements_1440x2560.png b/Tests/Svg.UnitTests/Resources/Issue482_MasksNotRendered/VariousElements_1440x2560.png new file mode 100644 index 000000000..f874497a5 Binary files /dev/null and b/Tests/Svg.UnitTests/Resources/Issue482_MasksNotRendered/VariousElements_1440x2560.png differ diff --git a/Tests/Svg.UnitTests/Svg.UnitTests.csproj b/Tests/Svg.UnitTests/Svg.UnitTests.csproj index 9452c00a6..3be3a0616 100644 --- a/Tests/Svg.UnitTests/Svg.UnitTests.csproj +++ b/Tests/Svg.UnitTests/Svg.UnitTests.csproj @@ -41,11 +41,27 @@ TRACE;DEBUG;NETCORE;NETCORE50 + + + + + + + + + + + + + + + + diff --git a/Tests/Svg.UnitTests/SvgTestHelper.cs b/Tests/Svg.UnitTests/SvgTestHelper.cs index f43d5b684..944be7676 100644 --- a/Tests/Svg.UnitTests/SvgTestHelper.cs +++ b/Tests/Svg.UnitTests/SvgTestHelper.cs @@ -6,6 +6,7 @@ using System.IO; using System.Text; using System.Xml; +using System.Runtime.InteropServices; namespace Svg.UnitTests { @@ -200,6 +201,20 @@ protected virtual XmlDocument GetXMLDocFromResource(string fullResourceString) return GetResourceXmlDoc(fullResourceString); } + /// + /// Gets a bitmap from resource. + /// + /// Resource path. + /// A object. + protected virtual Bitmap GetBitmapFromResource(string resourcePath) + { + var fullPath = GetFullResourceString(resourcePath); + using (var stream = GetResourceStream(fullPath)) + { + return new Bitmap(Image.FromStream(stream)); + } + } + /// /// Load, draw and check svg file. /// @@ -274,62 +289,83 @@ protected virtual bool ImagesAreEqual(Bitmap img1, Bitmap img2) protected virtual bool ImagesAreEqual(Bitmap img1, Bitmap img2, out float imgEqualPercentage) { Bitmap imgDiff; // To ignore. - return ImagesAreEqual(img1, img2, out imgEqualPercentage, out imgDiff); + return ImagesAreEqual(img1, img2, 0, out imgEqualPercentage, out imgDiff); } /// /// Compare Images. /// - /// Image 1. - /// Image 2. + /// Image 1. + /// Image 2. + /// The difference allowed for each component (R, G, B, A). This is required to be set experimentally as the renders might be slightly different for each target - probably due to different anti-aliasing algorithms or settings. /// Image equal value in percentage. 0.0% == completely unequal. 100.0% == completely equal. - /// Image with red pixel where and are unequal. + /// Image with red pixel where and are unequal. /// If images are completely equal: true; otherwise: false - protected virtual bool ImagesAreEqual(Bitmap img1, Bitmap img2, out float imgEqualPercentage, out Bitmap imgDiff) + protected virtual bool ImagesAreEqual(Bitmap image1, Bitmap image2, int allowedDifference, out float imgEqualPercentage, out Bitmap diffImage) { // Defaults. var diffColor = Color.Red; // Reset. imgEqualPercentage = 0; - imgDiff = null; + diffImage = null; // Requirements. - if (img1 == null) + if (image1 == null) return false; - if (img2 == null) + if (image2 == null) return false; - if (img1.Size.Width < 1 && img1.Height < 1) + if (image1.Size.Width < 1 && image1.Height < 1) return false; - if (!img1.Size.Equals(img2.Size)) + if (!image1.Size.Equals(image2.Size)) return false; - // Compare bitmaps. - imgDiff = new Bitmap(img1.Size.Width, img1.Size.Height); + diffImage = new Bitmap(image1.Width, image1.Height, PixelFormat.Format32bppArgb); + + var image1Data = image1.LockBits(new Rectangle(0, 0, image1.Width, image1.Height), ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb); + var image2Data = image2.LockBits(new Rectangle(0, 0, image2.Width, image2.Height), ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb); + var diffImageData = diffImage.LockBits(new Rectangle(0, 0, diffImage.Width, diffImage.Height), ImageLockMode.ReadWrite, PixelFormat.Format32bppArgb); + + var image1Bytes = new byte[image1Data.Stride * image1.Height]; + var image2Bytes = new byte[image2Data.Stride * image2.Height]; + var diffImageBytes = new byte[diffImageData.Stride * diffImage.Height]; + + Marshal.Copy(image1Data.Scan0, image1Bytes, 0, image1Bytes.Length); + Marshal.Copy(image2Data.Scan0, image2Bytes, 0, image2Bytes.Length); + Marshal.Copy(diffImageData.Scan0, diffImageBytes, 0, diffImageBytes.Length); + int diffPixelCount = 0; - for (int i = 0; i < img1.Width; ++i) + + for (var pixelIndex = 0; pixelIndex < image1Bytes.Length; pixelIndex += 4) { - for (int j = 0; j < img1.Height; ++j) + for (var componentIndex = 0; componentIndex < 4; componentIndex++) { - Color color; - if ((color = img1.GetPixel(i, j)) == img2.GetPixel(i, j)) - { - imgDiff.SetPixel(i, j, color); - } - else + if (Math.Abs(image1Bytes[pixelIndex + componentIndex] - image2Bytes[pixelIndex + componentIndex]) <= allowedDifference) { - ++diffPixelCount; - imgDiff.SetPixel(i, j, diffColor); + continue; } + + diffPixelCount++; + + diffImageBytes[pixelIndex] = diffColor.B; + diffImageBytes[pixelIndex + 1] = diffColor.G; + diffImageBytes[pixelIndex + 2] = diffColor.R; + diffImageBytes[pixelIndex + 3] = diffColor.A; } } + Marshal.Copy(diffImageBytes, 0, diffImageData.Scan0, diffImageBytes.Length); + + image1.UnlockBits(image1Data); + image2.UnlockBits(image2Data); + diffImage.UnlockBits(diffImageData); + // Calculate percentage. - int totalPixelCount = img1.Width * img1.Height; + int totalPixelCount = image1.Width * image1.Height; var imgDiffFactor = ((float)diffPixelCount / totalPixelCount); - imgEqualPercentage = imgDiffFactor * 100; + imgEqualPercentage = 100 - imgDiffFactor * 100; - return (imgDiffFactor == 1f); + return (imgDiffFactor == 0f); } } }