diff --git a/QRCoder/Extensions/StringValueAttribute.cs b/QRCoder/Extensions/StringValueAttribute.cs new file mode 100644 index 00000000..cc6e1bf4 --- /dev/null +++ b/QRCoder/Extensions/StringValueAttribute.cs @@ -0,0 +1,52 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text; + +namespace QRCoder.Extensions +{ + /// + /// Used to represent a string value for a value in an enum + /// + public class StringValueAttribute : Attribute + { + + #region Properties + + /// + /// Holds the alue in an enum + /// + public string StringValue { get; protected set; } + + #endregion + + /// + /// Init a StringValue Attribute + /// + /// + public StringValueAttribute(string value) + { + this.StringValue = value; + } + } + + public static class CustomExtensions + { + /// + /// Will get the string value for a given enum's value + /// + /// + /// + public static string GetStringValue(this Enum value) + { +#if NETSTANDARD1_3 + var fieldInfo = value.GetType().GetRuntimeField(value.ToString()); +#else + var fieldInfo = value.GetType().GetField(value.ToString()); +#endif + var attr = fieldInfo.GetCustomAttributes(typeof(StringValueAttribute), false) as StringValueAttribute[]; + return attr.Length > 0 ? attr[0].StringValue : null; + } + } +} diff --git a/QRCoder/SvgQRCode.cs b/QRCoder/SvgQRCode.cs index f0d01909..3dc0c097 100644 --- a/QRCoder/SvgQRCode.cs +++ b/QRCoder/SvgQRCode.cs @@ -1,8 +1,11 @@ #if NETFRAMEWORK || NETSTANDARD2_0 || NET5_0 +using QRCoder.Extensions; using System; using System.Collections; +using System.Collections.Generic; using System.Drawing; using System.Text; +using System.Text.RegularExpressions; using static QRCoder.QRCodeGenerator; using static QRCoder.SvgQRCode; @@ -16,11 +19,27 @@ public class SvgQRCode : AbstractQRCode, IDisposable public SvgQRCode() { } public SvgQRCode(QRCodeData data) : base(data) { } + /// + /// Returns a QR code as SVG string + /// + /// The pixel size each b/w module is drawn + /// SVG as string public string GetGraphic(int pixelsPerModule) { var viewBox = new Size(pixelsPerModule*this.QrCodeData.ModuleMatrix.Count, pixelsPerModule * this.QrCodeData.ModuleMatrix.Count); return this.GetGraphic(viewBox, Color.Black, Color.White); } + + /// + /// Returns a QR code as SVG string with custom colors, optional quietzone and logo + /// + /// The pixel size each b/w module is drawn + /// Color of the dark modules + /// Color of the light modules + /// If true a white border is drawn around the whole QR Code + /// Defines if width/height or viewbox should be used for size definition + /// A (optional) logo to be rendered on the code (either Bitmap or SVG) + /// SVG as string public string GetGraphic(int pixelsPerModule, Color darkColor, Color lightColor, bool drawQuietZones = true, SizingMode sizingMode = SizingMode.WidthHeightAttribute, SvgLogo logo = null) { var offset = drawQuietZones ? 0 : 4; @@ -29,6 +48,16 @@ public string GetGraphic(int pixelsPerModule, Color darkColor, Color lightColor, return this.GetGraphic(viewBox, darkColor, lightColor, drawQuietZones, sizingMode, logo); } + /// + /// Returns a QR code as SVG string with custom colors (in HEX syntax), optional quietzone and logo + /// + /// The pixel size each b/w module is drawn + /// The color of the dark/black modules in hex (e.g. #000000) representation + /// The color of the light/white modules in hex (e.g. #ffffff) representation + /// If true a white border is drawn around the whole QR Code + /// Defines if width/height or viewbox should be used for size definition + /// A (optional) logo to be rendered on the code (either Bitmap or SVG) + /// SVG as string public string GetGraphic(int pixelsPerModule, string darkColorHex, string lightColorHex, bool drawQuietZones = true, SizingMode sizingMode = SizingMode.WidthHeightAttribute, SvgLogo logo = null) { var offset = drawQuietZones ? 0 : 4; @@ -37,16 +66,44 @@ public string GetGraphic(int pixelsPerModule, string darkColorHex, string lightC return this.GetGraphic(viewBox, darkColorHex, lightColorHex, drawQuietZones, sizingMode, logo); } + /// + /// Returns a QR code as SVG string with optional quietzone and logo + /// + /// The viewbox of the QR code graphic + /// If true a white border is drawn around the whole QR Code + /// Defines if width/height or viewbox should be used for size definition + /// A (optional) logo to be rendered on the code (either Bitmap or SVG) + /// SVG as string public string GetGraphic(Size viewBox, bool drawQuietZones = true, SizingMode sizingMode = SizingMode.WidthHeightAttribute, SvgLogo logo = null) { return this.GetGraphic(viewBox, Color.Black, Color.White, drawQuietZones, sizingMode, logo); } + /// + /// Returns a QR code as SVG string with custom colors and optional quietzone and logo + /// + /// The viewbox of the QR code graphic + /// Color of the dark modules + /// Color of the light modules + /// If true a white border is drawn around the whole QR Code + /// Defines if width/height or viewbox should be used for size definition + /// A (optional) logo to be rendered on the code (either Bitmap or SVG) + /// SVG as string public string GetGraphic(Size viewBox, Color darkColor, Color lightColor, bool drawQuietZones = true, SizingMode sizingMode = SizingMode.WidthHeightAttribute, SvgLogo logo = null) { return this.GetGraphic(viewBox, ColorTranslator.ToHtml(Color.FromArgb(darkColor.ToArgb())), ColorTranslator.ToHtml(Color.FromArgb(lightColor.ToArgb())), drawQuietZones, sizingMode, logo); } + /// + /// Returns a QR code as SVG string with custom colors (in HEX syntax), optional quietzone and logo + /// + /// The viewbox of the QR code graphic + /// The color of the dark/black modules in hex (e.g. #000000) representation + /// The color of the light/white modules in hex (e.g. #ffffff) representation + /// If true a white border is drawn around the whole QR Code + /// Defines if width/height or viewbox should be used for size definition + /// A (optional) logo to be rendered on the code (either Bitmap or SVG) + /// SVG as string public string GetGraphic(Size viewBox, string darkColorHex, string lightColorHex, bool drawQuietZones = true, SizingMode sizingMode = SizingMode.WidthHeightAttribute, SvgLogo logo = null) { int offset = drawQuietZones ? 0 : 4; @@ -54,6 +111,9 @@ public string GetGraphic(Size viewBox, string darkColorHex, string lightColorHex double pixelsPerModule = Math.Min(viewBox.Width, viewBox.Height) / (double)drawableModulesCount; double qrSize = drawableModulesCount * pixelsPerModule; string svgSizeAttributes = (sizingMode == SizingMode.WidthHeightAttribute) ? $@"width=""{viewBox.Width}"" height=""{viewBox.Height}""" : $@"viewBox=""0 0 {viewBox.Width} {viewBox.Height}"""; + ImageAttributes? logoAttr = null; + if (logo != null) + logoAttr = GetLogoAttributes(logo, viewBox); // Merge horizontal rectangles int[,] matrix = new int[drawableModulesCount, drawableModulesCount]; @@ -66,7 +126,7 @@ public string GetGraphic(Size viewBox, string darkColorHex, string lightColorHex for (int xi = 0; xi < drawableModulesCount; xi += 1) { matrix[yi, xi] = 0; - if (bitArray[xi+offset]) + if (bitArray[xi+offset] && (logo == null || !logo.FillLogoBackground() || !IsBlockedByLogo((xi+offset)*pixelsPerModule, (yi+offset) * pixelsPerModule, logoAttr, pixelsPerModule))) { if(x0 == -1) { @@ -91,7 +151,7 @@ public string GetGraphic(Size viewBox, string darkColorHex, string lightColorHex } } - StringBuilder svgFile = new StringBuilder($@""); + StringBuilder svgFile = new StringBuilder($@""); svgFile.AppendLine($@""); for (int yi = 0; yi < drawableModulesCount; yi += 1) { @@ -118,16 +178,33 @@ public string GetGraphic(Size viewBox, string darkColorHex, string lightColorHex // Output SVG rectangles double x = xi * pixelsPerModule; - svgFile.AppendLine($@""); + if (logo == null || !logo.FillLogoBackground() || !IsBlockedByLogo(x, y, logoAttr, pixelsPerModule)) + svgFile.AppendLine($@""); + } } } //Render logo, if set if (logo != null) - { - svgFile.AppendLine($@""); - svgFile.AppendLine($@""); + { + + if (logo.GetMediaType() == SvgLogo.MediaType.PNG) + { + svgFile.AppendLine($@""); + svgFile.AppendLine($@""); + } + else if (logo.GetMediaType() == SvgLogo.MediaType.SVG) + { + svgFile.AppendLine($@""); + var rawLogo = (string)logo.GetRawLogo(); + //Remove some attributes from logo, because it would lead to wrong sizing inside our svg wrapper + new List() { "width", "height", "x", "y" }.ForEach(attr => + { + rawLogo = Regex.Replace(rawLogo, $@"(?!=]*?) +{attr}=(""[^""]+""|'[^']+')(?=[^>]*>)", ""); + }); + svgFile.Append(rawLogo); + } svgFile.AppendLine(@""); } @@ -135,30 +212,70 @@ public string GetGraphic(Size viewBox, string darkColorHex, string lightColorHex return svgFile.ToString(); } + private bool IsBlockedByLogo(double x, double y, ImageAttributes? attr, double pixelPerModule) + { + return x + pixelPerModule >= attr.Value.X && x <= attr.Value.X + attr.Value.Width && y + pixelPerModule >= attr.Value.Y && y <= attr.Value.Y + attr.Value.Height; + } + + private ImageAttributes GetLogoAttributes(SvgLogo logo, Size viewBox) + { + var imgWidth = logo.GetIconSizePercent() / 100d * viewBox.Width; + var imgHeight = logo.GetIconSizePercent() / 100d * viewBox.Height; + var imgPosX = viewBox.Width / 2d - imgWidth / 2d; + var imgPosY = viewBox.Height / 2d - imgHeight / 2d; + return new ImageAttributes() + { + Width = imgWidth, + Height = imgHeight, + X = imgPosX, + Y = imgPosY + }; + } + + private struct ImageAttributes + { + public double Width; + public double Height; + public double X; + public double Y; + } + private string CleanSvgVal(double input) { //Clean double values for international use/formats - return input.ToString(System.Globalization.CultureInfo.InvariantCulture); + //We use explicitly "G15" to avoid differences between .NET full and Core platforms + //https://stackoverflow.com/questions/64898117/tostring-has-a-different-behavior-between-net-462-and-net-core-3-1 + return input.ToString("G15", System.Globalization.CultureInfo.InvariantCulture); } + /// + /// Mode of sizing attribution on svg root node + /// public enum SizingMode { WidthHeightAttribute, ViewBoxAttribute } + /// + /// Represents a logo graphic that can be rendered on a SvgQRCode + /// public class SvgLogo { private string _logoData; - private string _mediaType; + private MediaType _mediaType; private int _iconSizePercent; + private bool _fillLogoBackground; + private object _logoRaw; + /// /// Create a logo object to be used in SvgQRCode renderer /// /// Logo to be rendered as Bitmap/rasterized graphic /// Degree of percentage coverage of the QR code by the logo - public SvgLogo(Bitmap iconRasterized, int iconSizePercent = 15) + /// If true, the background behind the logo will be cleaned + public SvgLogo(Bitmap iconRasterized, int iconSizePercent = 15, bool fillLogoBackground = true) { _iconSizePercent = iconSizePercent; using (var ms = new System.IO.MemoryStream()) @@ -169,7 +286,9 @@ public SvgLogo(Bitmap iconRasterized, int iconSizePercent = 15) _logoData = Convert.ToBase64String(ms.GetBuffer(), Base64FormattingOptions.None); } } - _mediaType = "image/png"; + _mediaType = MediaType.PNG; + _fillLogoBackground = fillLogoBackground; + _logoRaw = iconRasterized; } /// @@ -177,22 +296,71 @@ public SvgLogo(Bitmap iconRasterized, int iconSizePercent = 15) /// /// Logo to be rendered as SVG/vectorized graphic/string /// Degree of percentage coverage of the QR code by the logo - public SvgLogo(string iconVectorized, int iconSizePercent = 15) + /// If true, the background behind the logo will be cleaned + public SvgLogo(string iconVectorized, int iconSizePercent = 15, bool fillLogoBackground = true) { _iconSizePercent = iconSizePercent; _logoData = Convert.ToBase64String(Encoding.UTF8.GetBytes(iconVectorized), Base64FormattingOptions.None); - _mediaType = "image/svg+xml"; + _mediaType = MediaType.SVG; + _fillLogoBackground = fillLogoBackground; + _logoRaw = iconVectorized; + } + + /// + /// Returns the raw logo's data + /// + /// + public object GetRawLogo() + { + return _logoRaw; + } + + /// + /// Returns the media type of the logo + /// + /// + public MediaType GetMediaType() + { + return _mediaType; } + /// + /// Returns the logo as data-uri + /// + /// public string GetDataUri() { - return $"data:{_mediaType};base64,{_logoData}"; + return $"data:{_mediaType.GetStringValue()};base64,{_logoData}"; } + /// + /// Returns how much of the QR code should be covered by the logo (in percent) + /// + /// public int GetIconSizePercent() { return _iconSizePercent; } + + /// + /// Returns if the background of the logo should be cleaned (no QR modules will be rendered behind the logo) + /// + /// + public bool FillLogoBackground() + { + return _fillLogoBackground; + } + + /// + /// Media types for SvgLogos + /// + public enum MediaType : int + { + [StringValue("image/png")] + PNG = 0, + [StringValue("image/svg+xml")] + SVG = 1 + } } } diff --git a/QRCoderTests/Helpers/HelperFunctions.cs b/QRCoderTests/Helpers/HelperFunctions.cs index d8f7e78c..c67fce99 100644 --- a/QRCoderTests/Helpers/HelperFunctions.cs +++ b/QRCoderTests/Helpers/HelperFunctions.cs @@ -36,10 +36,20 @@ public static string BitmapToHash(Bitmap bmp) imgBytes = ms.ToArray(); ms.Dispose(); } + return ByteArrayToHash(imgBytes); + } + + public static string ByteArrayToHash(byte[] data) + { var md5 = new MD5CryptoServiceProvider(); - var hash = md5.ComputeHash(imgBytes); + var hash = md5.ComputeHash(data); return BitConverter.ToString(hash).Replace("-", "").ToLower(); } + + public static string StringToHash(string data) + { + return ByteArrayToHash(Encoding.UTF8.GetBytes(data)); + } #endif } diff --git a/QRCoderTests/SvgQRCodeRendererTests.cs b/QRCoderTests/SvgQRCodeRendererTests.cs index 65ef6cac..b4124d21 100644 --- a/QRCoderTests/SvgQRCodeRendererTests.cs +++ b/QRCoderTests/SvgQRCodeRendererTests.cs @@ -5,6 +5,7 @@ using QRCoderTests.Helpers.XUnitExtenstions; using System.IO; using System.Security.Cryptography; +using QRCoderTests.Helpers; #if !NETCOREAPP1_1 using System.Drawing; #endif @@ -27,6 +28,19 @@ private string GetAssemblyPath() #endif } + [Fact] + [Category("QRRenderer/SvgQRCode")] + public void can_render_svg_qrcode_simple() + { + //Create QR code + var gen = new QRCodeGenerator(); + var data = gen.CreateQrCode("This is a quick test! 123#?", QRCodeGenerator.ECCLevel.L); + var svg = new SvgQRCode(data).GetGraphic(5); + + var result = HelperFunctions.StringToHash(svg); + result.ShouldBe("5c251275a435a9aed7e591eb9c2e9949"); + } + [Fact] [Category("QRRenderer/SvgQRCode")] public void can_render_svg_qrcode() @@ -36,11 +50,21 @@ public void can_render_svg_qrcode() var data = gen.CreateQrCode("This is a quick test! 123#?", QRCodeGenerator.ECCLevel.H); var svg = new SvgQRCode(data).GetGraphic(10, Color.Red, Color.White); - var md5 = new MD5CryptoServiceProvider(); - var hash = md5.ComputeHash(System.Text.Encoding.UTF8.GetBytes(svg)); - var result = BitConverter.ToString(hash).Replace("-", "").ToLower(); + var result = HelperFunctions.StringToHash(svg); + result.ShouldBe("1baa8c6ac3bd8c1eabcd2c5422dd9f78"); + } + + [Fact] + [Category("QRRenderer/SvgQRCode")] + public void can_render_svg_qrcode_viewbox_mode() + { + //Create QR code + var gen = new QRCodeGenerator(); + var data = gen.CreateQrCode("This is a quick test! 123#?", QRCodeGenerator.ECCLevel.H); + var svg = new SvgQRCode(data).GetGraphic(new Size(128,128)); - result.ShouldBe("0ad8bc75675d04ba0caff51c7a89992c"); + var result = HelperFunctions.StringToHash(svg); + result.ShouldBe("56719c7db39937c74377855a5dc4af0a"); } [Fact] @@ -52,11 +76,8 @@ public void can_render_svg_qrcode_without_quietzones() var data = gen.CreateQrCode("This is a quick test! 123#?", QRCodeGenerator.ECCLevel.H); var svg = new SvgQRCode(data).GetGraphic(10, Color.Red, Color.White, false); - var md5 = new MD5CryptoServiceProvider(); - var hash = md5.ComputeHash(System.Text.Encoding.UTF8.GetBytes(svg)); - var result = BitConverter.ToString(hash).Replace("-", "").ToLower(); - - result.ShouldBe("24392f47d4c1c2c5097bd6b3f8eefccc"); + var result = HelperFunctions.StringToHash(svg); + result.ShouldBe("2a582427d86b51504c08ebcbcf0472bd"); } [Fact] @@ -73,11 +94,8 @@ public void can_render_svg_qrcode_with_png_logo() var svg = new SvgQRCode(data).GetGraphic(10, Color.DarkGray, Color.White, logo: logoObj); - var md5 = new MD5CryptoServiceProvider(); - var hash = md5.ComputeHash(System.Text.Encoding.UTF8.GetBytes(svg)); - var result = BitConverter.ToString(hash).Replace("-", "").ToLower(); - - result.ShouldBe("4ff45872787f321524cc4d071239c25e"); + var result = HelperFunctions.StringToHash(svg); + result.ShouldBe("78e02e8ba415f15817d5ed88c4afca31"); } [Fact] @@ -94,11 +112,28 @@ public void can_render_svg_qrcode_with_svg_logo() var svg = new SvgQRCode(data).GetGraphic(10, Color.DarkGray, Color.White, logo: logoObj); - var md5 = new MD5CryptoServiceProvider(); - var hash = md5.ComputeHash(System.Text.Encoding.UTF8.GetBytes(svg)); - var result = BitConverter.ToString(hash).Replace("-", "").ToLower(); + var result = HelperFunctions.StringToHash(svg); + result.ShouldBe("71f461136fdbe2ab85902d23ad2d7eb8"); + } + + [Fact] + [Category("QRRenderer/SvgQRCode")] + public void can_instantate_parameterless() + { + var svgCode = new SvgQRCode(); + svgCode.ShouldNotBeNull(); + svgCode.ShouldBeOfType(); + } + + [Fact] + [Category("QRRenderer/SvgQRCode")] + public void can_render_svg_qrcode_from_helper() + { + //Create QR code + var svg = SvgQRCodeHelper.GetQRCode("A", 2, "#000000", "#ffffff", QRCodeGenerator.ECCLevel.Q); - result.ShouldBe("b4ded3964e2e640b6b6c74d1c89d71fa"); + var result = HelperFunctions.StringToHash(svg); + result.ShouldBe("f5ec37aa9fb207e3701cc0d86c4a357d"); } #endif }