Skip to content

Commit 8395725

Browse files
authored
Merge pull request #346 from codebude/bugfix/343-svglogo-background-and-positioning
Bugfix/343 svglogo background and positioning
2 parents 0c3bc02 + 093c3a6 commit 8395725

File tree

4 files changed

+297
-32
lines changed

4 files changed

+297
-32
lines changed
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Reflection;
5+
using System.Text;
6+
7+
namespace QRCoder.Extensions
8+
{
9+
/// <summary>
10+
/// Used to represent a string value for a value in an enum
11+
/// </summary>
12+
public class StringValueAttribute : Attribute
13+
{
14+
15+
#region Properties
16+
17+
/// <summary>
18+
/// Holds the alue in an enum
19+
/// </summary>
20+
public string StringValue { get; protected set; }
21+
22+
#endregion
23+
24+
/// <summary>
25+
/// Init a StringValue Attribute
26+
/// </summary>
27+
/// <param name="value"></param>
28+
public StringValueAttribute(string value)
29+
{
30+
this.StringValue = value;
31+
}
32+
}
33+
34+
public static class CustomExtensions
35+
{
36+
/// <summary>
37+
/// Will get the string value for a given enum's value
38+
/// </summary>
39+
/// <param name="value"></param>
40+
/// <returns></returns>
41+
public static string GetStringValue(this Enum value)
42+
{
43+
#if NETSTANDARD1_3
44+
var fieldInfo = value.GetType().GetRuntimeField(value.ToString());
45+
#else
46+
var fieldInfo = value.GetType().GetField(value.ToString());
47+
#endif
48+
var attr = fieldInfo.GetCustomAttributes(typeof(StringValueAttribute), false) as StringValueAttribute[];
49+
return attr.Length > 0 ? attr[0].StringValue : null;
50+
}
51+
}
52+
}

QRCoder/SvgQRCode.cs

Lines changed: 181 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
#if NETFRAMEWORK || NETSTANDARD2_0 || NET5_0
2+
using QRCoder.Extensions;
23
using System;
34
using System.Collections;
5+
using System.Collections.Generic;
46
using System.Drawing;
57
using System.Text;
8+
using System.Text.RegularExpressions;
69
using static QRCoder.QRCodeGenerator;
710
using static QRCoder.SvgQRCode;
811

@@ -16,11 +19,27 @@ public class SvgQRCode : AbstractQRCode, IDisposable
1619
public SvgQRCode() { }
1720
public SvgQRCode(QRCodeData data) : base(data) { }
1821

22+
/// <summary>
23+
/// Returns a QR code as SVG string
24+
/// </summary>
25+
/// <param name="pixelsPerModule">The pixel size each b/w module is drawn</param>
26+
/// <returns>SVG as string</returns>
1927
public string GetGraphic(int pixelsPerModule)
2028
{
2129
var viewBox = new Size(pixelsPerModule*this.QrCodeData.ModuleMatrix.Count, pixelsPerModule * this.QrCodeData.ModuleMatrix.Count);
2230
return this.GetGraphic(viewBox, Color.Black, Color.White);
2331
}
32+
33+
/// <summary>
34+
/// Returns a QR code as SVG string with custom colors, optional quietzone and logo
35+
/// </summary>
36+
/// <param name="pixelsPerModule">The pixel size each b/w module is drawn</param>
37+
/// <param name="darkColor">Color of the dark modules</param>
38+
/// <param name="lightColor">Color of the light modules</param>
39+
/// <param name="drawQuietZones">If true a white border is drawn around the whole QR Code</param>
40+
/// <param name="sizingMode">Defines if width/height or viewbox should be used for size definition</param>
41+
/// <param name="logo">A (optional) logo to be rendered on the code (either Bitmap or SVG)</param>
42+
/// <returns>SVG as string</returns>
2443
public string GetGraphic(int pixelsPerModule, Color darkColor, Color lightColor, bool drawQuietZones = true, SizingMode sizingMode = SizingMode.WidthHeightAttribute, SvgLogo logo = null)
2544
{
2645
var offset = drawQuietZones ? 0 : 4;
@@ -29,6 +48,16 @@ public string GetGraphic(int pixelsPerModule, Color darkColor, Color lightColor,
2948
return this.GetGraphic(viewBox, darkColor, lightColor, drawQuietZones, sizingMode, logo);
3049
}
3150

51+
/// <summary>
52+
/// Returns a QR code as SVG string with custom colors (in HEX syntax), optional quietzone and logo
53+
/// </summary>
54+
/// <param name="pixelsPerModule">The pixel size each b/w module is drawn</param>
55+
/// <param name="darkColorHex">The color of the dark/black modules in hex (e.g. #000000) representation</param>
56+
/// <param name="lightColorHex">The color of the light/white modules in hex (e.g. #ffffff) representation</param>
57+
/// <param name="drawQuietZones">If true a white border is drawn around the whole QR Code</param>
58+
/// <param name="sizingMode">Defines if width/height or viewbox should be used for size definition</param>
59+
/// <param name="logo">A (optional) logo to be rendered on the code (either Bitmap or SVG)</param>
60+
/// <returns>SVG as string</returns>
3261
public string GetGraphic(int pixelsPerModule, string darkColorHex, string lightColorHex, bool drawQuietZones = true, SizingMode sizingMode = SizingMode.WidthHeightAttribute, SvgLogo logo = null)
3362
{
3463
var offset = drawQuietZones ? 0 : 4;
@@ -37,23 +66,54 @@ public string GetGraphic(int pixelsPerModule, string darkColorHex, string lightC
3766
return this.GetGraphic(viewBox, darkColorHex, lightColorHex, drawQuietZones, sizingMode, logo);
3867
}
3968

69+
/// <summary>
70+
/// Returns a QR code as SVG string with optional quietzone and logo
71+
/// </summary>
72+
/// <param name="viewBox">The viewbox of the QR code graphic</param>
73+
/// <param name="drawQuietZones">If true a white border is drawn around the whole QR Code</param>
74+
/// <param name="sizingMode">Defines if width/height or viewbox should be used for size definition</param>
75+
/// <param name="logo">A (optional) logo to be rendered on the code (either Bitmap or SVG)</param>
76+
/// <returns>SVG as string</returns>
4077
public string GetGraphic(Size viewBox, bool drawQuietZones = true, SizingMode sizingMode = SizingMode.WidthHeightAttribute, SvgLogo logo = null)
4178
{
4279
return this.GetGraphic(viewBox, Color.Black, Color.White, drawQuietZones, sizingMode, logo);
4380
}
4481

82+
/// <summary>
83+
/// Returns a QR code as SVG string with custom colors and optional quietzone and logo
84+
/// </summary>
85+
/// <param name="viewBox">The viewbox of the QR code graphic</param>
86+
/// <param name="darkColor">Color of the dark modules</param>
87+
/// <param name="lightColor">Color of the light modules</param>
88+
/// <param name="drawQuietZones">If true a white border is drawn around the whole QR Code</param>
89+
/// <param name="sizingMode">Defines if width/height or viewbox should be used for size definition</param>
90+
/// <param name="logo">A (optional) logo to be rendered on the code (either Bitmap or SVG)</param>
91+
/// <returns>SVG as string</returns>
4592
public string GetGraphic(Size viewBox, Color darkColor, Color lightColor, bool drawQuietZones = true, SizingMode sizingMode = SizingMode.WidthHeightAttribute, SvgLogo logo = null)
4693
{
4794
return this.GetGraphic(viewBox, ColorTranslator.ToHtml(Color.FromArgb(darkColor.ToArgb())), ColorTranslator.ToHtml(Color.FromArgb(lightColor.ToArgb())), drawQuietZones, sizingMode, logo);
4895
}
4996

97+
/// <summary>
98+
/// Returns a QR code as SVG string with custom colors (in HEX syntax), optional quietzone and logo
99+
/// </summary>
100+
/// <param name="viewBox">The viewbox of the QR code graphic</param>
101+
/// <param name="darkColorHex">The color of the dark/black modules in hex (e.g. #000000) representation</param>
102+
/// <param name="lightColorHex">The color of the light/white modules in hex (e.g. #ffffff) representation</param>
103+
/// <param name="drawQuietZones">If true a white border is drawn around the whole QR Code</param>
104+
/// <param name="sizingMode">Defines if width/height or viewbox should be used for size definition</param>
105+
/// <param name="logo">A (optional) logo to be rendered on the code (either Bitmap or SVG)</param>
106+
/// <returns>SVG as string</returns>
50107
public string GetGraphic(Size viewBox, string darkColorHex, string lightColorHex, bool drawQuietZones = true, SizingMode sizingMode = SizingMode.WidthHeightAttribute, SvgLogo logo = null)
51108
{
52109
int offset = drawQuietZones ? 0 : 4;
53110
int drawableModulesCount = this.QrCodeData.ModuleMatrix.Count - (drawQuietZones ? 0 : offset * 2);
54111
double pixelsPerModule = Math.Min(viewBox.Width, viewBox.Height) / (double)drawableModulesCount;
55112
double qrSize = drawableModulesCount * pixelsPerModule;
56113
string svgSizeAttributes = (sizingMode == SizingMode.WidthHeightAttribute) ? $@"width=""{viewBox.Width}"" height=""{viewBox.Height}""" : $@"viewBox=""0 0 {viewBox.Width} {viewBox.Height}""";
114+
ImageAttributes? logoAttr = null;
115+
if (logo != null)
116+
logoAttr = GetLogoAttributes(logo, viewBox);
57117

58118
// Merge horizontal rectangles
59119
int[,] matrix = new int[drawableModulesCount, drawableModulesCount];
@@ -66,7 +126,7 @@ public string GetGraphic(Size viewBox, string darkColorHex, string lightColorHex
66126
for (int xi = 0; xi < drawableModulesCount; xi += 1)
67127
{
68128
matrix[yi, xi] = 0;
69-
if (bitArray[xi+offset])
129+
if (bitArray[xi+offset] && (logo == null || !logo.FillLogoBackground() || !IsBlockedByLogo((xi+offset)*pixelsPerModule, (yi+offset) * pixelsPerModule, logoAttr, pixelsPerModule)))
70130
{
71131
if(x0 == -1)
72132
{
@@ -91,7 +151,7 @@ public string GetGraphic(Size viewBox, string darkColorHex, string lightColorHex
91151
}
92152
}
93153

94-
StringBuilder svgFile = new StringBuilder($@"<svg version=""1.1"" baseProfile=""full"" shape-rendering=""crispEdges"" {svgSizeAttributes} xmlns=""http://www.w3.org/2000/svg"">");
154+
StringBuilder svgFile = new StringBuilder($@"<svg version=""1.1"" baseProfile=""full"" shape-rendering=""crispEdges"" {svgSizeAttributes} xmlns=""http://www.w3.org/2000/svg"" xmlns:xlink=""http://www.w3.org/1999/xlink"">");
95155
svgFile.AppendLine($@"<rect x=""0"" y=""0"" width=""{CleanSvgVal(qrSize)}"" height=""{CleanSvgVal(qrSize)}"" fill=""{lightColorHex}"" />");
96156
for (int yi = 0; yi < drawableModulesCount; yi += 1)
97157
{
@@ -118,47 +178,104 @@ public string GetGraphic(Size viewBox, string darkColorHex, string lightColorHex
118178

119179
// Output SVG rectangles
120180
double x = xi * pixelsPerModule;
121-
svgFile.AppendLine($@"<rect x=""{CleanSvgVal(x)}"" y=""{CleanSvgVal(y)}"" width=""{CleanSvgVal(xL * pixelsPerModule)}"" height=""{CleanSvgVal(yL * pixelsPerModule)}"" fill=""{darkColorHex}"" />");
181+
if (logo == null || !logo.FillLogoBackground() || !IsBlockedByLogo(x, y, logoAttr, pixelsPerModule))
182+
svgFile.AppendLine($@"<rect x=""{CleanSvgVal(x)}"" y=""{CleanSvgVal(y)}"" width=""{CleanSvgVal(xL * pixelsPerModule)}"" height=""{CleanSvgVal(yL * pixelsPerModule)}"" fill=""{darkColorHex}"" />");
183+
122184
}
123185
}
124186
}
125187

126188
//Render logo, if set
127189
if (logo != null)
128-
{
129-
svgFile.AppendLine($@"<svg width=""100%"" height=""100%"" version=""1.1"" xmlns = ""http://www.w3.org/2000/svg"">");
130-
svgFile.AppendLine($@"<image x=""{50 - (logo.GetIconSizePercent() / 2)}%"" y=""{50 - (logo.GetIconSizePercent() / 2)}%"" width=""{logo.GetIconSizePercent()}%"" height=""{logo.GetIconSizePercent()}%"" href=""{logo.GetDataUri()}"" />");
190+
{
191+
192+
if (logo.GetMediaType() == SvgLogo.MediaType.PNG)
193+
{
194+
svgFile.AppendLine($@"<svg width=""100%"" height=""100%"" version=""1.1"" xmlns = ""http://www.w3.org/2000/svg"">");
195+
svgFile.AppendLine($@"<image x=""{CleanSvgVal(logoAttr.Value.X)}"" y=""{CleanSvgVal(logoAttr.Value.Y)}"" width=""{CleanSvgVal(logoAttr.Value.Width)}"" height=""{CleanSvgVal(logoAttr.Value.Height)}"" xlink:href=""{logo.GetDataUri()}"" />");
196+
}
197+
else if (logo.GetMediaType() == SvgLogo.MediaType.SVG)
198+
{
199+
svgFile.AppendLine($@"<svg x=""{CleanSvgVal(logoAttr.Value.X)}"" y=""{CleanSvgVal(logoAttr.Value.Y)}"" width=""{CleanSvgVal(logoAttr.Value.Width)}"" height=""{CleanSvgVal(logoAttr.Value.Height)}"" version=""1.1"" xmlns = ""http://www.w3.org/2000/svg"">");
200+
var rawLogo = (string)logo.GetRawLogo();
201+
//Remove some attributes from logo, because it would lead to wrong sizing inside our svg wrapper
202+
new List<string>() { "width", "height", "x", "y" }.ForEach(attr =>
203+
{
204+
rawLogo = Regex.Replace(rawLogo, $@"(?!=<svg[^>]*?) +{attr}=(""[^""]+""|'[^']+')(?=[^>]*>)", "");
205+
});
206+
svgFile.Append(rawLogo);
207+
}
131208
svgFile.AppendLine(@"</svg>");
132209
}
133210

134211
svgFile.Append(@"</svg>");
135212
return svgFile.ToString();
136213
}
137214

215+
private bool IsBlockedByLogo(double x, double y, ImageAttributes? attr, double pixelPerModule)
216+
{
217+
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;
218+
}
219+
220+
private ImageAttributes GetLogoAttributes(SvgLogo logo, Size viewBox)
221+
{
222+
var imgWidth = logo.GetIconSizePercent() / 100d * viewBox.Width;
223+
var imgHeight = logo.GetIconSizePercent() / 100d * viewBox.Height;
224+
var imgPosX = viewBox.Width / 2d - imgWidth / 2d;
225+
var imgPosY = viewBox.Height / 2d - imgHeight / 2d;
226+
return new ImageAttributes()
227+
{
228+
Width = imgWidth,
229+
Height = imgHeight,
230+
X = imgPosX,
231+
Y = imgPosY
232+
};
233+
}
234+
235+
private struct ImageAttributes
236+
{
237+
public double Width;
238+
public double Height;
239+
public double X;
240+
public double Y;
241+
}
242+
138243
private string CleanSvgVal(double input)
139244
{
140245
//Clean double values for international use/formats
141-
return input.ToString(System.Globalization.CultureInfo.InvariantCulture);
246+
//We use explicitly "G15" to avoid differences between .NET full and Core platforms
247+
//https://stackoverflow.com/questions/64898117/tostring-has-a-different-behavior-between-net-462-and-net-core-3-1
248+
return input.ToString("G15", System.Globalization.CultureInfo.InvariantCulture);
142249
}
143250

251+
/// <summary>
252+
/// Mode of sizing attribution on svg root node
253+
/// </summary>
144254
public enum SizingMode
145255
{
146256
WidthHeightAttribute,
147257
ViewBoxAttribute
148258
}
149259

260+
/// <summary>
261+
/// Represents a logo graphic that can be rendered on a SvgQRCode
262+
/// </summary>
150263
public class SvgLogo
151264
{
152265
private string _logoData;
153-
private string _mediaType;
266+
private MediaType _mediaType;
154267
private int _iconSizePercent;
268+
private bool _fillLogoBackground;
269+
private object _logoRaw;
155270

271+
156272
/// <summary>
157273
/// Create a logo object to be used in SvgQRCode renderer
158274
/// </summary>
159275
/// <param name="iconRasterized">Logo to be rendered as Bitmap/rasterized graphic</param>
160276
/// <param name="iconSizePercent">Degree of percentage coverage of the QR code by the logo</param>
161-
public SvgLogo(Bitmap iconRasterized, int iconSizePercent = 15)
277+
/// <param name="fillLogoBackground">If true, the background behind the logo will be cleaned</param>
278+
public SvgLogo(Bitmap iconRasterized, int iconSizePercent = 15, bool fillLogoBackground = true)
162279
{
163280
_iconSizePercent = iconSizePercent;
164281
using (var ms = new System.IO.MemoryStream())
@@ -169,30 +286,81 @@ public SvgLogo(Bitmap iconRasterized, int iconSizePercent = 15)
169286
_logoData = Convert.ToBase64String(ms.GetBuffer(), Base64FormattingOptions.None);
170287
}
171288
}
172-
_mediaType = "image/png";
289+
_mediaType = MediaType.PNG;
290+
_fillLogoBackground = fillLogoBackground;
291+
_logoRaw = iconRasterized;
173292
}
174293

175294
/// <summary>
176295
/// Create a logo object to be used in SvgQRCode renderer
177296
/// </summary>
178297
/// <param name="iconVectorized">Logo to be rendered as SVG/vectorized graphic/string</param>
179298
/// <param name="iconSizePercent">Degree of percentage coverage of the QR code by the logo</param>
180-
public SvgLogo(string iconVectorized, int iconSizePercent = 15)
299+
/// <param name="fillLogoBackground">If true, the background behind the logo will be cleaned</param>
300+
public SvgLogo(string iconVectorized, int iconSizePercent = 15, bool fillLogoBackground = true)
181301
{
182302
_iconSizePercent = iconSizePercent;
183303
_logoData = Convert.ToBase64String(Encoding.UTF8.GetBytes(iconVectorized), Base64FormattingOptions.None);
184-
_mediaType = "image/svg+xml";
304+
_mediaType = MediaType.SVG;
305+
_fillLogoBackground = fillLogoBackground;
306+
_logoRaw = iconVectorized;
307+
}
308+
309+
/// <summary>
310+
/// Returns the raw logo's data
311+
/// </summary>
312+
/// <returns></returns>
313+
public object GetRawLogo()
314+
{
315+
return _logoRaw;
316+
}
317+
318+
/// <summary>
319+
/// Returns the media type of the logo
320+
/// </summary>
321+
/// <returns></returns>
322+
public MediaType GetMediaType()
323+
{
324+
return _mediaType;
185325
}
186326

327+
/// <summary>
328+
/// Returns the logo as data-uri
329+
/// </summary>
330+
/// <returns></returns>
187331
public string GetDataUri()
188332
{
189-
return $"data:{_mediaType};base64,{_logoData}";
333+
return $"data:{_mediaType.GetStringValue()};base64,{_logoData}";
190334
}
191335

336+
/// <summary>
337+
/// Returns how much of the QR code should be covered by the logo (in percent)
338+
/// </summary>
339+
/// <returns></returns>
192340
public int GetIconSizePercent()
193341
{
194342
return _iconSizePercent;
195343
}
344+
345+
/// <summary>
346+
/// Returns if the background of the logo should be cleaned (no QR modules will be rendered behind the logo)
347+
/// </summary>
348+
/// <returns></returns>
349+
public bool FillLogoBackground()
350+
{
351+
return _fillLogoBackground;
352+
}
353+
354+
/// <summary>
355+
/// Media types for SvgLogos
356+
/// </summary>
357+
public enum MediaType : int
358+
{
359+
[StringValue("image/png")]
360+
PNG = 0,
361+
[StringValue("image/svg+xml")]
362+
SVG = 1
363+
}
196364
}
197365
}
198366

0 commit comments

Comments
 (0)