diff --git a/QRCoder.ImageSharp/ArtQRCode.cs b/QRCoder.ImageSharp/ArtQRCode.cs new file mode 100644 index 00000000..74a8b9c2 --- /dev/null +++ b/QRCoder.ImageSharp/ArtQRCode.cs @@ -0,0 +1,237 @@ +#if NET5_0 || NET6_0 || NETSTANDARD2_1_OR_GREATER + +using System; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Drawing; +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; +using static QRCoder.ImageSharp.ArtQRCode; +using static QRCoder.QRCodeGenerator; +// pull request raised to extend library used. +namespace QRCoder.ImageSharp +{ + public class ArtQRCode : AbstractQRCode, IDisposable + { + /// + /// Constructor without params to be used in COM Objects connections + /// + public ArtQRCode() { } + + /// + /// Creates new ArtQrCode object + /// + /// QRCodeData generated by the QRCodeGenerator + public ArtQRCode(QRCodeData data) : base(data) { } + + /// + /// Renders an art-style QR code with dots as modules. (With default settings: DarkColor=Black, LightColor=White, Background=Transparent, QuietZone=true) + /// + /// Amount of px each dark/light module of the QR code shall take place in the final QR code image + /// QRCode graphic as bitmap + public Image GetGraphic(int pixelsPerModule) + { + return this.GetGraphic(pixelsPerModule, Color.Black, Color.White, Color.Transparent); + } + + /// + /// Renders an art-style QR code with dots as modules and a background image (With default settings: DarkColor=Black, LightColor=White, Background=Transparent, QuietZone=true) + /// + /// A bitmap object that will be used as background picture + /// QRCode graphic as bitmap + public Image GetGraphic(Image? backgroundImage = null) + { + return this.GetGraphic(10, Color.Black, Color.White, Color.Transparent, backgroundImage: backgroundImage); + } + + /// + /// Renders an art-style QR code with dots as modules and various user settings + /// + /// Amount of px each dark/light module of the QR code shall take place in the final QR code image + /// Color of the dark modules + /// Color of the light modules + /// Color of the background + /// A bitmap object that will be used as background picture + /// Value between 0.0 to 1.0 that defines how big the module dots are. The bigger the value, the less round the dots will be. + /// If true a white border is drawn around the whole QR Code + /// Style of the quiet zones + /// Style of the background image (if set). Fill=spanning complete graphic; DataAreaOnly=Don't paint background into quietzone + /// Optional image that should be used instead of the default finder patterns + /// QRCode graphic as bitmap + public Image GetGraphic(int pixelsPerModule, Color darkColor, Color lightColor, Color backgroundColor, Image? backgroundImage = null, double pixelSizeFactor = 0.8, + bool drawQuietZones = true, QuietZoneStyle quietZoneRenderingStyle = QuietZoneStyle.Dotted, + BackgroundImageStyle backgroundImageStyle = BackgroundImageStyle.DataAreaOnly, Image? finderPatternImage = null) + { + if (pixelSizeFactor > 1) + throw new ArgumentException("The parameter pixelSize must be between 0 and 1. (0-100%)"); + + int pixelSize = (int)Math.Min(pixelsPerModule, Math.Floor(pixelsPerModule / pixelSizeFactor)); + + var numModules = QrCodeData.ModuleMatrix.Count - (drawQuietZones ? 0 : 8); + var offset = (drawQuietZones ? 0 : 4); + var size = numModules * pixelsPerModule; + + var image = new Image(size, size); + + var options = new DrawingOptions + { + GraphicsOptions = new GraphicsOptions + { + Antialias = true, + AntialiasSubpixelDepth = 2 + } + }; + + IBrush lightBrush = Brushes.Solid(lightColor); + IBrush darkBrush = Brushes.Solid(darkColor); + + IBrush backgroundBrush = Brushes.Solid(backgroundColor); + + //background rectangle: + IPath backgroundRectangle = new RectangularPolygon(0, 0, size, size); + + image.Mutate(x => x.Fill(options, brush: backgroundBrush, path: backgroundRectangle)); + + if(backgroundImage != null) + { + switch (backgroundImageStyle) + { + case BackgroundImageStyle.Fill: + backgroundImage = backgroundImage.Clone(x => x.Resize(size, size)); + image.Mutate(x => x.DrawImage(backgroundImage, new Point(0, 0), 1)); + break; + case BackgroundImageStyle.DataAreaOnly: + var bgOffset = 4 - offset; + backgroundImage = backgroundImage.Clone(x => x.Resize(size - (2 * bgOffset * pixelsPerModule), size - (2 * bgOffset * pixelsPerModule))); + image.Mutate(x => x.DrawImage(backgroundImage, new Point(bgOffset * pixelsPerModule, bgOffset * pixelsPerModule), 1)); + break; + } + } + + for (var x = 0; x < numModules; x += 1) + { + for (var y = 0; y < numModules; y += 1) + { + var rectangleF = new RectangularPolygon(x * pixelsPerModule, y * pixelsPerModule, pixelsPerModule, pixelsPerModule); + var elipse = new EllipsePolygon(x * pixelsPerModule + pixelSize / 2, y * pixelsPerModule + pixelSize / 2, pixelsPerModule, pixelsPerModule); + + var pixelIsDark = this.QrCodeData.ModuleMatrix[offset + y][offset + x]; + var solidBrush = pixelIsDark ? darkBrush : lightBrush; + //var pixelImage = pixelIsDark ? darkModulePixel : lightModulePixel; + + if (!IsPartOfFinderPattern(x, y, numModules, offset)) + if (drawQuietZones && quietZoneRenderingStyle == QuietZoneStyle.Flat && IsPartOfQuietZone(x, y, numModules)) + image.Mutate(im => im.Fill(options, solidBrush, rectangleF)); + else + image.Mutate(im => im.Fill(options, solidBrush, elipse)); + else if (finderPatternImage == null) + image.Mutate(im => im.Fill(options, solidBrush, rectangleF)); + } + } + + if (finderPatternImage != null) + { + var finderPatternSize = 7 * pixelsPerModule; + + finderPatternImage = finderPatternImage.Clone(x => x.Resize(finderPatternSize, finderPatternSize)); + + image.Mutate(x => x.DrawImage(finderPatternImage, 1)); //default position is 0,0 //new Rectangle(0, 0, finderPatternSize, finderPatternSize) + image.Mutate(x => x.DrawImage(finderPatternImage, new Point(size - finderPatternSize, 0), 1)); + image.Mutate(x => x.DrawImage(finderPatternImage, new Point(0, size - finderPatternSize), 1)); + //graphics.DrawImage(finderPatternImage, new Rectangle(0, size - finderPatternSize, finderPatternSize, finderPatternSize)); + } + return image; + } + + /// + /// Checks if a given module(-position) is part of the quietzone of a QR code + /// + /// X position + /// Y position + /// Total number of modules per row + /// true, if position is part of quiet zone + private bool IsPartOfQuietZone(int x, int y, int numModules) + { + return + x < 4 || //left + y < 4 || //top + x > numModules - 5 || //right + y > numModules - 5; //bottom + } + + + /// + /// Checks if a given module(-position) is part of one of the three finder patterns of a QR code + /// + /// X position + /// Y position + /// Total number of modules per row + /// Offset in modules (usually depending on drawQuietZones parameter) + /// true, if position is part of any finder pattern + private bool IsPartOfFinderPattern(int x, int y, int numModules, int offset) + { + var cornerSize = 11 - offset; + var outerLimitLow = (numModules - cornerSize - 1); + var outerLimitHigh = outerLimitLow + 8; + var invertedOffset = 4 - offset; + return + (x >= invertedOffset && x < cornerSize && y >= invertedOffset && y < cornerSize) || //Top-left finder pattern + (x > outerLimitLow && x < outerLimitHigh && y >= invertedOffset && y < cornerSize) || //Top-right finder pattern + (x >= invertedOffset && x < cornerSize && y > outerLimitLow && y < outerLimitHigh); //Bottom-left finder pattern + } + + /// + /// Defines how the quiet zones shall be rendered. + /// + public enum QuietZoneStyle + { + Dotted, + Flat + } + + /// + /// Defines how the background image (if set) shall be rendered. + /// + public enum BackgroundImageStyle + { + Fill, + DataAreaOnly + } + } + + public static class ArtQRCodeHelper + { + /// + /// Helper function to create an ArtQRCode graphic with a single function call + /// + /// Text/payload to be encoded inside the QR code + /// Amount of px each dark/light module of the QR code shall take place in the final QR code image + /// Color of the dark modules + /// Color of the light modules + /// Color of the background + /// The level of error correction data + /// Shall the generator be forced to work in UTF-8 mode? + /// Should the byte-order-mark be used? + /// Which ECI mode shall be used? + /// Set fixed QR code target version. + /// A bitmap object that will be used as background picture + /// Value between 0.0 to 1.0 that defines how big the module dots are. The bigger the value, the less round the dots will be. + /// If true a white border is drawn around the whole QR Code + /// Style of the quiet zones + /// Style of the background image (if set). Fill=spanning complete graphic; DataAreaOnly=Don't paint background into quietzone + /// Optional image that should be used instead of the default finder patterns + /// QRCode graphic as bitmap + public static Image GetQRCode(string plainText, int pixelsPerModule, Color darkColor, Color lightColor, Color backgroundColor, ECCLevel eccLevel, bool forceUtf8 = false, + bool utf8BOM = false, EciMode eciMode = EciMode.Default, int requestedVersion = -1, Image? backgroundImage = null, double pixelSizeFactor = 0.8, + bool drawQuietZones = true, QuietZoneStyle quietZoneRenderingStyle = QuietZoneStyle.Flat, + BackgroundImageStyle backgroundImageStyle = BackgroundImageStyle.DataAreaOnly, Image? finderPatternImage = null) + { + using (var qrGenerator = new QRCodeGenerator()) + using (var qrCodeData = qrGenerator.CreateQrCode(plainText, eccLevel, forceUtf8, utf8BOM, eciMode, requestedVersion)) + using (var qrCode = new ArtQRCode(qrCodeData)) + return qrCode.GetGraphic(pixelsPerModule, darkColor, lightColor, backgroundColor, backgroundImage, pixelSizeFactor, drawQuietZones, quietZoneRenderingStyle, backgroundImageStyle, finderPatternImage); + } + } +} + +#endif \ No newline at end of file diff --git a/QRCoder.ImageSharp/ImageSharpQRCode.cs b/QRCoder.ImageSharp/ImageSharpQRCode.cs new file mode 100644 index 00000000..58e5e2cd --- /dev/null +++ b/QRCoder.ImageSharp/ImageSharpQRCode.cs @@ -0,0 +1,203 @@ +using QRCoder.Models; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Drawing; +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; +using System; +using System.Collections.Generic; +using System.Text; + +namespace QRCoder.ImageSharp +{ + + public class ImageSharpQRCode : AbstractQRCode, IDisposable + { + public ImageSharpQRCode() + { + } + + public ImageSharpQRCode(QRCodeData data) : base(data) + { + } + + /// + /// Renders an art-style QR code with dots as modules. (With default settings: DarkColor=Black, LightColor=White, Background=Transparent, QuietZone=true) + /// + /// Amount of px each dark/light module of the QR code shall take place in the final QR code image + /// QRCode graphic as bitmap + public Image GetGraphic(int pixelsPerModule, Image? logoImage = null, LogoLocation logoLocation = LogoLocation.BottomRight, LogoBackgroundShape logoBackgroundShape = LogoBackgroundShape.Rectangle) + { + return this.GetGraphic(pixelsPerModule, Color.Black, Color.White, Color.Transparent, logoImage, logoLocation, logoBackgroundShape); + } + + /// + /// Renders an art-style QR code with dots as modules and a background image (With default settings: DarkColor=Black, LightColor=White, Background=Transparent, QuietZone=true) + /// + /// A bitmap object that will be used as background picture + /// QRCode graphic as bitmap + public Image GetGraphic(Image? logoImage = null) + { + return this.GetGraphic(10, Color.Black, Color.White, Color.Transparent, logoImage); + } + + /// + /// Renders an art-style QR code with dots as modules and various user settings + /// + /// Amount of px each dark/light module of the QR code shall take place in the final QR code image + /// Color of the dark modules + /// Color of the light modules + /// Color of the background + /// A bitmap object that will be used as background picture + /// Value between 0.0 to 1.0 that defines how big the module dots are. The bigger the value, the less round the dots will be. + /// If true a white border is drawn around the whole QR Code + /// Style of the quiet zones + /// Style of the background image (if set). Fill=spanning complete graphic; DataAreaOnly=Don't paint background into quietzone + /// Optional image that should be used instead of the default finder patterns + /// QRCode graphic as bitmap + public Image GetGraphic(int pixelsPerModule, Color darkColor, Color lightColor, Color backgroundColor, Image? logoImage = null, LogoLocation logoLocation = LogoLocation.BottomRight, LogoBackgroundShape logoBackgroundShape = LogoBackgroundShape.Circle, double pixelSizeFactor = 0.8, + bool drawQuietZones = true) + { + if (pixelSizeFactor > 1) + throw new ArgumentException("The parameter pixelSize must be between 0 and 1. (0-100%)"); + + int pixelSize = (int)Math.Min(pixelsPerModule, Math.Floor(pixelsPerModule / pixelSizeFactor)); + + var numModules = QrCodeData.ModuleMatrix.Count - (drawQuietZones ? 0 : 8); + var offset = (drawQuietZones ? 0 : 4); + var size = numModules * pixelsPerModule; + + var image = new Image(size, size); + + var options = new DrawingOptions + { + GraphicsOptions = new GraphicsOptions + { + Antialias = true, + AntialiasSubpixelDepth = 2 + } + }; + + IBrush lightBrush = Brushes.Solid(lightColor); + IBrush darkBrush = Brushes.Solid(darkColor); + + IBrush backgroundBrush = Brushes.Solid(backgroundColor); + + //background rectangle: + IPath backgroundRectangle = new RectangularPolygon(0, 0, size, size); + + image.Mutate(x => x.Fill(options, brush: backgroundBrush, path: backgroundRectangle)); + + //if (backgroundImage != null) + //{ + // switch (backgroundImageStyle) + // { + // case BackgroundImageStyle.Fill: + // backgroundImage = backgroundImage.Clone(x => x.Resize(size, size)); + // image.Mutate(x => x.DrawImage(backgroundImage, new Point(0, 0), 1)); + // break; + // case BackgroundImageStyle.DataAreaOnly: + // var bgOffset = 4 - offset; + // backgroundImage = backgroundImage.Clone(x => x.Resize(size - (2 * bgOffset * pixelsPerModule), size - (2 * bgOffset * pixelsPerModule))); + // image.Mutate(x => x.DrawImage(backgroundImage, new Point(bgOffset * pixelsPerModule, bgOffset * pixelsPerModule), 1)); + // break; + // } + //} + + for (var x = 0; x < numModules; x += 1) + { + for (var y = 0; y < numModules; y += 1) + { + var rectangleF = new RectangularPolygon(x * pixelsPerModule, y * pixelsPerModule, pixelsPerModule, pixelsPerModule); + + var pixelIsDark = this.QrCodeData.ModuleMatrix[offset + y][offset + x]; + var solidBrush = pixelIsDark ? darkBrush : lightBrush; + //var pixelImage = pixelIsDark ? darkModulePixel : lightModulePixel; + + if (!IsPartOfFinderPattern(x, y, numModules, offset)) + if (drawQuietZones && IsPartOfQuietZone(x, y, numModules)) + image.Mutate(im => im.Fill(options, solidBrush, rectangleF)); + else + image.Mutate(im => im.Fill(options, solidBrush, rectangleF)); + else + image.Mutate(im => im.Fill(options, solidBrush, rectangleF)); + } + } + + if(logoImage != null) + { + var logoSize = (int)(size * 0.15); + var logoOffset = drawQuietZones ? 4 : 0; + var locationPadding = logoLocation switch + { + LogoLocation.Center => new PointF(size / 2, size / 2), + _ => new Point(size - logoSize / 2 - logoOffset * pixelsPerModule, size - logoSize / 2 - logoOffset * pixelsPerModule), + }; + + + var locationLogo = logoLocation switch + { + LogoLocation.Center => new Point(size / 2 - logoSize / 2, size / 2 - logoSize / 2), + _ => new Point(size - logoSize - logoOffset * pixelsPerModule, size - logoSize - logoOffset * pixelsPerModule), + }; + + + IPath backgroundShape = logoBackgroundShape switch + { + LogoBackgroundShape.Circle => new EllipsePolygon(locationPadding, logoSize / 2), + _ => logoLocation == LogoLocation.Center ? new RectangularPolygon(size / 2 - logoSize/2, size /2 - logoSize /2, logoSize, logoSize) : new RectangularPolygon(size - logoSize - logoOffset * pixelsPerModule, size - logoSize - logoOffset * pixelsPerModule, logoSize, logoSize), + }; + + backgroundShape = backgroundShape.Scale(1.03f); + + image.Mutate(x => x.Fill(backgroundBrush, backgroundShape)); + + logoImage = logoImage.Clone(x => + { + x.Resize(logoSize, logoSize); + + }); + image.Mutate(x => x.DrawImage(logoImage, locationLogo, 1)); + } + + return image; + } + + /// + /// Checks if a given module(-position) is part of the quietzone of a QR code + /// + /// X position + /// Y position + /// Total number of modules per row + /// true, if position is part of quiet zone + private bool IsPartOfQuietZone(int x, int y, int numModules) + { + return + x < 4 || //left + y < 4 || //top + x > numModules - 5 || //right + y > numModules - 5; //bottom + } + + + /// + /// Checks if a given module(-position) is part of one of the three finder patterns of a QR code + /// + /// X position + /// Y position + /// Total number of modules per row + /// Offset in modules (usually depending on drawQuietZones parameter) + /// true, if position is part of any finder pattern + private bool IsPartOfFinderPattern(int x, int y, int numModules, int offset) + { + var cornerSize = 11 - offset; + var outerLimitLow = (numModules - cornerSize - 1); + var outerLimitHigh = outerLimitLow + 8; + var invertedOffset = 4 - offset; + return + (x >= invertedOffset && x < cornerSize && y >= invertedOffset && y < cornerSize) || //Top-left finder pattern + (x > outerLimitLow && x < outerLimitHigh && y >= invertedOffset && y < cornerSize) || //Top-right finder pattern + (x >= invertedOffset && x < cornerSize && y > outerLimitLow && y < outerLimitHigh); //Bottom-left finder pattern + } + } +} diff --git a/QRCoder.ImageSharp/QRCoder.ImageSharp.csproj b/QRCoder.ImageSharp/QRCoder.ImageSharp.csproj new file mode 100644 index 00000000..3f179b4c --- /dev/null +++ b/QRCoder.ImageSharp/QRCoder.ImageSharp.csproj @@ -0,0 +1,19 @@ + + + + netstandard2.1;net5.0;net5.0-windows;net6.0;net6.0-windows + disable + enable + 1.0.1-dev + + + + + + + + + + + + diff --git a/QRCoder.ImageSharpTests/ArtQRCodeRendererTests.cs b/QRCoder.ImageSharpTests/ArtQRCodeRendererTests.cs new file mode 100644 index 00000000..20d2fd63 --- /dev/null +++ b/QRCoder.ImageSharpTests/ArtQRCodeRendererTests.cs @@ -0,0 +1,115 @@ + +#if !NET35 && NET6_0 +using QRCoder; +using QRCoderTests.Helpers; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.PixelFormats; +using FluentAssertions; +#if NET6_0 +using QRCoder.ImageSharp; +#endif + +namespace QRCoderTests +{ + [TestClass] + public class ImageSharpArtQRCodeRendererTests + { + + + [TestMethod] + [TestCategory("QRRenderer/ArtQRCode")] + public void can_create_standard_qrcode_graphic() + { + var gen = new QRCodeGenerator(); + var data = gen.CreateQrCode("This is a quick test! 123#?", QRCodeGenerator.ECCLevel.H); + var bmp = new ArtQRCode(data).GetGraphic(10); + var result = HelperFunctions.ImageToHash(bmp); + //bmp.SaveAsPng("qrcode_imagesharp.png"); + result.Should().Be("e8de533db63b5784de075c4e4cc3e0c9"); //different hash than the System.Drawing example since the algorithm is slighty different -> (anti-aliasing). + } + + [TestMethod] + [TestCategory("QRRenderer/ArtQRCode")] + public void can_create_standard_qrcode_graphic_with_custom_finder() + { + var gen = new QRCodeGenerator(); + var data = gen.CreateQrCode("This is a quick test! 123#?", QRCodeGenerator.ECCLevel.H); + var finder = new Image(15, 15); + var bmp = new ArtQRCode(data).GetGraphic(10, Color.Black, Color.White, Color.Transparent, finderPatternImage: finder); + //bmp.SaveAsPng("finder_custom.png"); + var result = HelperFunctions.ImageToHash(bmp); + + result.Should().Be("63dcb31fd8910a10aba57929b6327790"); + + } + + [TestMethod] + [TestCategory("QRRenderer/ArtQRCode")] + public void can_create_standard_qrcode_graphic_without_quietzone() + { + var gen = new QRCodeGenerator(); + var data = gen.CreateQrCode("This is a quick test! 123#?", QRCodeGenerator.ECCLevel.H); + var bmp = new ArtQRCode(data).GetGraphic(10, Color.Black, Color.White, Color.Transparent, drawQuietZones: false); + + var result = HelperFunctions.ImageToHash(bmp); + + //bmp.SaveAsPng("without_quietzone.png"); + + result.Should().Be("d96f9c8c64cdb5c651dd59bab6b564d7"); + + } + + [TestMethod] + [TestCategory("QRRenderer/ArtQRCode")] + public void can_create_standard_qrcode_graphic_with_background() + { + var gen = new QRCodeGenerator(); + var data = gen.CreateQrCode("This is a quick test! 123#?", QRCodeGenerator.ECCLevel.H); + var bmp = new ArtQRCode(data).GetGraphic(Image.Load(HelperFunctions.GetAssemblyPath() + "\\assets\\noun_software engineer_2909346.png")); + //Used logo is licensed under public domain. Ref.: https://thenounproject.com/Iconathon1/collection/redefining-women/?i=2909346 + + var result = HelperFunctions.ImageToHash(bmp); + + bmp.SaveAsPng("custom_background.png"); + + result.Should().Be("aea31c69506b0d933fd49205e7b37f33"); + + } + + [TestMethod] + [TestCategory("QRRenderer/ArtQRCode")] + public void should_throw_pixelfactor_oor_exception() + { + var gen = new QRCodeGenerator(); + var data = gen.CreateQrCode("This is a quick test! 123#?", QRCodeGenerator.ECCLevel.H); + var aCode = new ArtQRCode(data); + + var exception = Assert.ThrowsException(() => aCode.GetGraphic(10, Color.Black, Color.White, Color.Transparent, pixelSizeFactor: 2)); + + exception.Message.Should().Be("The parameter pixelSize must be between 0 and 1. (0-100%)"); + } + + [TestMethod] + [TestCategory("QRRenderer/ArtQRCode")] + public void can_instantate_parameterless() + { + var asciiCode = new ArtQRCode(); + asciiCode.Should().NotBeNull(); + asciiCode.Should().BeOfType(); + } + + [TestMethod] + [TestCategory("QRRenderer/ArtQRCode")] + public void can_render_artqrcode_from_helper() + { + //Create QR code + var bmp = ArtQRCodeHelper.GetQRCode("A", 10, Color.Black, Color.White, Color.Transparent, QRCodeGenerator.ECCLevel.L); + + var result = HelperFunctions.ImageToHash(bmp); + //bmp.SaveAsPng("Helper.png"); + result.Should().Be("1dbda9e61f832a7ebdb5d97f8c6e8fb6"); + + } + } +} +#endif \ No newline at end of file diff --git a/QRCoder.ImageSharpTests/Helpers/HelperFunctions.cs b/QRCoder.ImageSharpTests/Helpers/HelperFunctions.cs new file mode 100644 index 00000000..e46e1ddd --- /dev/null +++ b/QRCoder.ImageSharpTests/Helpers/HelperFunctions.cs @@ -0,0 +1,94 @@ +using System; +using System.Text; +using System.IO; +using System.Security.Cryptography; +#if NET6_0_OR_GREATER +using SixLabors.ImageSharp.Formats.Png; +#endif + +#if !NETCOREAPP1_1 +using System.Drawing; +#endif +#if NETFRAMEWORK || NET5_0_WINDOWS +using SW = System.Windows; +using System.Windows.Media; +using System.Windows.Media.Imaging; +#endif + + +namespace QRCoderTests.Helpers +{ + public static class HelperFunctions + { + +#if NETFRAMEWORK || NET5_0_WINDOWS + public static BitmapSource ToBitmapSource(DrawingImage source) + { + DrawingVisual drawingVisual = new DrawingVisual(); + DrawingContext drawingContext = drawingVisual.RenderOpen(); + drawingContext.DrawImage(source, new SW.Rect(new SW.Point(0, 0), new SW.Size(source.Width, source.Height))); + drawingContext.Close(); + + RenderTargetBitmap bmp = new RenderTargetBitmap((int)source.Width, (int)source.Height, 96, 96, PixelFormats.Pbgra32); + bmp.Render(drawingVisual); + return bmp; + } + + public static Bitmap BitmapSourceToBitmap(DrawingImage xamlImg) + { + using (MemoryStream ms = new MemoryStream()) + { + PngBitmapEncoder encoder = new PngBitmapEncoder(); + encoder.Frames.Add(BitmapFrame.Create(ToBitmapSource(xamlImg))); + encoder.Save(ms); + + using (Bitmap bmp = new Bitmap(ms)) + { + return new Bitmap(bmp); + } + } + } +#endif + +#if !NETCOREAPP1_1 + public static string GetAssemblyPath() + { + return +#if NET5_0 || NET6_0 + AppDomain.CurrentDomain.BaseDirectory; +#elif NET35 || NET452 + Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().CodeBase).Replace("file:\\", ""); +#else + Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location).Replace("file:\\", ""); +#endif + } +#endif + + +#if NET6_0 + public static string ImageToHash(SixLabors.ImageSharp.Image image) + { + using var ms = new MemoryStream(); + image.Save(ms, new PngEncoder()); + byte[] imgBytes = ms.ToArray(); + return ByteArrayToHash(imgBytes); + } +#endif + + public static string ByteArrayToHash(byte[] data) + { +#if !NETCOREAPP1_1 + var md5 = MD5.Create(); + var hash = md5.ComputeHash(data); +#else + var hash = new SshNet.Security.Cryptography.MD5().ComputeHash(data); +#endif + return BitConverter.ToString(hash).Replace("-", "").ToLower(); + } + + public static string StringToHash(string data) + { + return ByteArrayToHash(Encoding.UTF8.GetBytes(data)); + } + } +} diff --git a/QRCoder.ImageSharpTests/ImageSharpQrCodeTests.cs b/QRCoder.ImageSharpTests/ImageSharpQrCodeTests.cs new file mode 100644 index 00000000..77f112fb --- /dev/null +++ b/QRCoder.ImageSharpTests/ImageSharpQrCodeTests.cs @@ -0,0 +1,36 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using QRCoder.ImageSharp; +using QRCoder.Models; +using QRCoderTests.Helpers; +using SixLabors.ImageSharp; + +namespace QRCoder.ImageSharpTests +{ + [TestClass] + public class ImageSharpQrCodeTests + { + [TestMethod] + public void TestRegularQrCode() + { + var gen = new QRCodeGenerator(); + var data = gen.CreateQrCode("This is a quick test! 123#?", QRCodeGenerator.ECCLevel.H); + var qrcode = new ImageSharpQRCode(data); + var image = qrcode.GetGraphic(); + image.SaveAsPng("imageSharpQr.png"); + } + + [TestMethod] + public void TestLogoQrCode() + { + var gen = new QRCodeGenerator(); + var data = gen.CreateQrCode("This is a quick test! 123#?", QRCodeGenerator.ECCLevel.H); + var qrcode = new ImageSharpQRCode(data); + var image = qrcode.GetGraphic(60, Image.Load(HelperFunctions.GetAssemblyPath() + "\\assets\\noun_software engineer_2909346.png"), LogoLocation.Center, LogoBackgroundShape.Circle); + image.SaveAsPng("imageSharpQr_logo.png"); + } + } +} diff --git a/QRCoder.ImageSharpTests/QRCoder.ImageSharpTests.csproj b/QRCoder.ImageSharpTests/QRCoder.ImageSharpTests.csproj new file mode 100644 index 00000000..54dcc637 --- /dev/null +++ b/QRCoder.ImageSharpTests/QRCoder.ImageSharpTests.csproj @@ -0,0 +1,35 @@ + + + + net6.0 + enable + enable + + false + + + + + + + + + + + + + + + + + + + + Always + + + Always + + + + diff --git a/QRCoder.ImageSharpTests/Usings.cs b/QRCoder.ImageSharpTests/Usings.cs new file mode 100644 index 00000000..ab67c7ea --- /dev/null +++ b/QRCoder.ImageSharpTests/Usings.cs @@ -0,0 +1 @@ +global using Microsoft.VisualStudio.TestTools.UnitTesting; \ No newline at end of file diff --git a/QRCoder.ImageSharpTests/assets/noun_Scientist_2909361.svg b/QRCoder.ImageSharpTests/assets/noun_Scientist_2909361.svg new file mode 100644 index 00000000..869a0e73 --- /dev/null +++ b/QRCoder.ImageSharpTests/assets/noun_Scientist_2909361.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/QRCoder.ImageSharpTests/assets/noun_software engineer_2909346.png b/QRCoder.ImageSharpTests/assets/noun_software engineer_2909346.png new file mode 100644 index 00000000..bc100986 Binary files /dev/null and b/QRCoder.ImageSharpTests/assets/noun_software engineer_2909346.png differ diff --git a/QRCoder.SkiaSharp/Extensions/SaveExtensions.cs b/QRCoder.SkiaSharp/Extensions/SaveExtensions.cs new file mode 100644 index 00000000..5e1631c0 --- /dev/null +++ b/QRCoder.SkiaSharp/Extensions/SaveExtensions.cs @@ -0,0 +1,36 @@ +using QRCoder.Models; +using SkiaSharp; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace QRCoder.SkiaSharp.Extensions +{ + public static class SaveExtensions + { + /// + /// Saves a png of the qr code + /// + /// + /// + public static void SaveToPng(this SkiaSharpQRCode skiaSharpQRCode, string path, int quality = 80) + { + using var image = skiaSharpQRCode.GetGraphic(); + using var imageData = image.Encode(SKEncodedImageFormat.Png, quality); + using var stream = File.OpenWrite(path); + imageData.SaveTo(stream); + } + + public static void SaveToPng(this SkiaSharpQRCode skiaSharpQRCode, Stream stream, int quality, int pixelsPerModule, SKImage? logoImage = null, LogoLocation logoLocation = LogoLocation.BottomRight, LogoBackgroundShape logoBackgroundShape = LogoBackgroundShape.Circle) => SaveToPng(skiaSharpQRCode, stream, quality, pixelsPerModule, SKColors.Black, SKColors.White, SKColors.White, logoImage, logoLocation, logoBackgroundShape); + + public static void SaveToPng(this SkiaSharpQRCode skiaSharpQRCode, Stream stream, int quality, int pixelsPerModule, SKColor darkColor, SKColor lightColor, SKColor backgroundColor, SKImage? logoImage = null, LogoLocation logoLocation = LogoLocation.BottomRight, LogoBackgroundShape logoBackgroundShape = LogoBackgroundShape.Circle, bool drawQuietZones = true) + { + using var image = skiaSharpQRCode.GetGraphic(pixelsPerModule, darkColor, lightColor, backgroundColor, logoImage, logoLocation, logoBackgroundShape, drawQuietZones: drawQuietZones); + using var imageData = image.Encode(SKEncodedImageFormat.Png, quality); + imageData.SaveTo(stream); + stream.Position = 0; + } + } +} diff --git a/QRCoder.SkiaSharp/QRCoder.SkiaSharp.csproj b/QRCoder.SkiaSharp/QRCoder.SkiaSharp.csproj new file mode 100644 index 00000000..5b45031f --- /dev/null +++ b/QRCoder.SkiaSharp/QRCoder.SkiaSharp.csproj @@ -0,0 +1,18 @@ + + + + net6.0 + enable + enable + 1.0.0-dev1 + + + + + + + + + + + diff --git a/QRCoder.SkiaSharp/SkiaSharpQRCode.cs b/QRCoder.SkiaSharp/SkiaSharpQRCode.cs new file mode 100644 index 00000000..500bfbd5 --- /dev/null +++ b/QRCoder.SkiaSharp/SkiaSharpQRCode.cs @@ -0,0 +1,222 @@ +using QRCoder.Models; +using SkiaSharp; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace QRCoder.SkiaSharp +{ + public class SkiaSharpQRCode : AbstractQRCode, IDisposable + { + public SkiaSharpQRCode() + { + } + + public SkiaSharpQRCode(QRCodeData data) : base(data) + { + } + + /// + /// Renders an art-style QR code with dots as modules. (With default settings: DarkColor=Black, LightColor=White, Background=Transparent, QuietZone=true) + /// + /// Amount of px each dark/light module of the QR code shall take place in the final QR code image + /// QRCode graphic as bitmap + public SKImage GetGraphic(int pixelsPerModule, SKImage? logoImage = null, LogoLocation logoLocation = LogoLocation.BottomRight, LogoBackgroundShape logoBackgroundShape = LogoBackgroundShape.Rectangle) + { + return this.GetGraphic(pixelsPerModule, SKColors.Black, SKColors.White, SKColors.Transparent, logoImage, logoLocation, logoBackgroundShape); + } + + /// + /// Renders an art-style QR code with dots as modules and a background image (With default settings: DarkColor=Black, LightColor=White, Background=Transparent, QuietZone=true) + /// + /// A bitmap object that will be used as background picture + /// QRCode graphic as bitmap + public SKImage GetGraphic(SKImage? logoImage = null) + { + return this.GetGraphic(10, SKColors.Black, SKColors.White, SKColors.Transparent, logoImage); + } + + /// + /// Renders an art-style QR code with dots as modules and various user settings + /// + /// Amount of px each dark/light module of the QR code shall take place in the final QR code image + /// Color of the dark modules + /// Color of the light modules + /// Color of the background + /// A bitmap object that will be used as background picture + /// Value between 0.0 to 1.0 that defines how big the module dots are. The bigger the value, the less round the dots will be. + /// If true a white border is drawn around the whole QR Code + /// Style of the quiet zones + /// Style of the background image (if set). Fill=spanning complete graphic; DataAreaOnly=Don't paint background into quietzone + /// Optional image that should be used instead of the default finder patterns + /// QRCode graphic as bitmap + public SKImage GetGraphic(int pixelsPerModule, SKColor darkColor, SKColor lightColor, SKColor backgroundColor, SKImage? logoImage = null, LogoLocation logoLocation = LogoLocation.BottomRight, LogoBackgroundShape logoBackgroundShape = LogoBackgroundShape.Circle, double pixelSizeFactor = 0.8, + bool drawQuietZones = true) + { + if (pixelSizeFactor > 1) + throw new ArgumentException("The parameter pixelSize must be between 0 and 1. (0-100%)"); + + int pixelSize = (int)Math.Min(pixelsPerModule, Math.Floor(pixelsPerModule / pixelSizeFactor)); + + var numModules = QrCodeData.ModuleMatrix.Count - (drawQuietZones ? 0 : 8); + var offset = (drawQuietZones ? 0 : 4); + var size = numModules * pixelsPerModule; + + var imageInfo = new SKImageInfo(size, size, SKColorType.RgbaF32); + + using var surface = SKSurface.Create(imageInfo); + + var canvas = surface.Canvas; + + var lightBrush = new SKPaint + { + Color = lightColor, + IsAntialias = true, + Style = SKPaintStyle.Fill + }; + + var darkBrush = new SKPaint + { + Color = darkColor, + IsAntialias = true, + Style = SKPaintStyle.Fill + }; + + var backgroundBrush = new SKPaint + { + Color = backgroundColor, + IsAntialias = true, + Style = SKPaintStyle.Fill + }; + + + //background rectangle: + + canvas.DrawRect(0, 0, size, size, backgroundBrush); + + //if (backgroundImage != null) + //{ + // switch (backgroundImageStyle) + // { + // case BackgroundImageStyle.Fill: + // backgroundImage = backgroundImage.Clone(x => x.Resize(size, size)); + // image.Mutate(x => x.DrawImage(backgroundImage, new Point(0, 0), 1)); + // break; + // case BackgroundImageStyle.DataAreaOnly: + // var bgOffset = 4 - offset; + // backgroundImage = backgroundImage.Clone(x => x.Resize(size - (2 * bgOffset * pixelsPerModule), size - (2 * bgOffset * pixelsPerModule))); + // image.Mutate(x => x.DrawImage(backgroundImage, new Point(bgOffset * pixelsPerModule, bgOffset * pixelsPerModule), 1)); + // break; + // } + //} + + for (var x = 0; x < numModules; x += 1) + { + for (var y = 0; y < numModules; y += 1) + { + var rectangleF = SKRect.Create(x * pixelsPerModule, y * pixelsPerModule, pixelsPerModule, pixelsPerModule); //creates a rectangle in positions x * pixelsPerModule, y * pixelsPerModule and with the width, height pixelsPerModule. Do not use the constructor since that uses the 4 point location. + + var pixelIsDark = this.QrCodeData.ModuleMatrix[offset + y][offset + x]; + var solidBrush = pixelIsDark ? darkBrush : lightBrush; + //var pixelImage = pixelIsDark ? darkModulePixel : lightModulePixel; + + if (!IsPartOfFinderPattern(x, y, numModules, offset)) + if (drawQuietZones && IsPartOfQuietZone(x, y, numModules)) + canvas.DrawRect(rectangleF, solidBrush); // .Mutate(im => im.Fill(options, solidBrush, rectangleF)); + else + canvas.DrawRect(rectangleF, solidBrush); // .Mutate(im => im.Fill(options, solidBrush, rectangleF)); + else + canvas.DrawRect(rectangleF, solidBrush); // .Mutate(im => im.Fill(options, solidBrush, rectangleF)); + } + } + + if (logoImage != null) + { + var logoSize = (int)(size * 0.15); + var logoOffset = drawQuietZones ? 4 : 0; + + var locationPadding = logoLocation switch + { + LogoLocation.Center => new SKPoint(size / 2, size / 2), + _ => new SKPoint(size - logoSize / 2 - logoOffset * pixelsPerModule, size - logoSize / 2 - logoOffset * pixelsPerModule), + }; + + var locationLogo = logoLocation switch + { + LogoLocation.Center => new SKPoint(size / 2 - logoSize / 2, size / 2 - logoSize / 2), + _ => new SKPoint(size - logoSize - logoOffset * pixelsPerModule, size - logoSize - logoOffset * pixelsPerModule), + }; + + + + switch (logoBackgroundShape) + { + case LogoBackgroundShape.Circle: + canvas.DrawOval(locationPadding, new SKSize(1.20f*logoSize / 2, 1.20f * logoSize/2), backgroundBrush); + + break; + case LogoBackgroundShape.Rectangle: + var paddingRectangle = logoLocation switch + { + LogoLocation.Center => SKRect.Create(size / 2 - logoSize / 2, size / 2 - logoSize / 2, logoSize, logoSize), + _ => SKRect.Create(size - logoSize - logoOffset * pixelsPerModule, size - logoSize - logoOffset * pixelsPerModule, logoSize, logoSize) + }; + paddingRectangle.Inflate(1.05f, 1.05f); + canvas.DrawRect(paddingRectangle, backgroundBrush); + break; + } + + + var destinationArea = logoLocation switch + { + LogoLocation.Center => SKRect.Create(size / 2 - logoSize / 2, size / 2 - logoSize / 2, logoSize, logoSize), + _ => SKRect.Create(size - logoSize - logoOffset * pixelsPerModule, size - logoSize - logoOffset * pixelsPerModule, logoSize, logoSize) + }; + //var sourceArea = SKRect.Create(logoImage.Info.Width, logoImage.Info.Height); + + canvas.DrawImage(logoImage, destinationArea); + } + + return surface.Snapshot(); + } + + /// + /// Checks if a given module(-position) is part of the quietzone of a QR code + /// + /// X position + /// Y position + /// Total number of modules per row + /// true, if position is part of quiet zone + private bool IsPartOfQuietZone(int x, int y, int numModules) + { + return + x < 4 || //left + y < 4 || //top + x > numModules - 5 || //right + y > numModules - 5; //bottom + } + + + /// + /// Checks if a given module(-position) is part of one of the three finder patterns of a QR code + /// + /// X position + /// Y position + /// Total number of modules per row + /// Offset in modules (usually depending on drawQuietZones parameter) + /// true, if position is part of any finder pattern + private bool IsPartOfFinderPattern(int x, int y, int numModules, int offset) + { + var cornerSize = 11 - offset; + var outerLimitLow = (numModules - cornerSize - 1); + var outerLimitHigh = outerLimitLow + 8; + var invertedOffset = 4 - offset; + return + (x >= invertedOffset && x < cornerSize && y >= invertedOffset && y < cornerSize) || //Top-left finder pattern + (x > outerLimitLow && x < outerLimitHigh && y >= invertedOffset && y < cornerSize) || //Top-right finder pattern + (x >= invertedOffset && x < cornerSize && y > outerLimitLow && y < outerLimitHigh); //Bottom-left finder pattern + } + } +} diff --git a/QRCoder.SkiaSharpTests/Helpers/HelperFunctions.cs b/QRCoder.SkiaSharpTests/Helpers/HelperFunctions.cs new file mode 100644 index 00000000..c0490310 --- /dev/null +++ b/QRCoder.SkiaSharpTests/Helpers/HelperFunctions.cs @@ -0,0 +1,91 @@ +using System; +using System.Text; +using System.IO; +using System.Security.Cryptography; + +#if !NETCOREAPP1_1 +using System.Drawing; +#endif +#if NETFRAMEWORK || NET5_0_WINDOWS +using SW = System.Windows; +using System.Windows.Media; +using System.Windows.Media.Imaging; +#endif + + +namespace QRCoderTests.Helpers +{ + public static class HelperFunctions + { + +#if NETFRAMEWORK || NET5_0_WINDOWS + public static BitmapSource ToBitmapSource(DrawingImage source) + { + DrawingVisual drawingVisual = new DrawingVisual(); + DrawingContext drawingContext = drawingVisual.RenderOpen(); + drawingContext.DrawImage(source, new SW.Rect(new SW.Point(0, 0), new SW.Size(source.Width, source.Height))); + drawingContext.Close(); + + RenderTargetBitmap bmp = new RenderTargetBitmap((int)source.Width, (int)source.Height, 96, 96, PixelFormats.Pbgra32); + bmp.Render(drawingVisual); + return bmp; + } + + public static Bitmap BitmapSourceToBitmap(DrawingImage xamlImg) + { + using (MemoryStream ms = new MemoryStream()) + { + PngBitmapEncoder encoder = new PngBitmapEncoder(); + encoder.Frames.Add(BitmapFrame.Create(ToBitmapSource(xamlImg))); + encoder.Save(ms); + + using (Bitmap bmp = new Bitmap(ms)) + { + return new Bitmap(bmp); + } + } + } +#endif + +#if !NETCOREAPP1_1 + public static string GetAssemblyPath() + { + return +#if NET5_0 || NET6_0 + AppDomain.CurrentDomain.BaseDirectory; +#elif NET35 || NET452 + Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().CodeBase).Replace("file:\\", ""); +#else + Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location).Replace("file:\\", ""); +#endif + } +#endif + + +//#if NET6_0 +// public static string ImageToHash(SixLabors.ImageSharp.Image image) +// { +// using var ms = new MemoryStream(); +// image.Save(ms, new PngEncoder()); +// byte[] imgBytes = ms.ToArray(); +// return ByteArrayToHash(imgBytes); +// } +//#endif + + public static string ByteArrayToHash(byte[] data) + { +#if !NETCOREAPP1_1 + var md5 = MD5.Create(); + var hash = md5.ComputeHash(data); +#else + var hash = new SshNet.Security.Cryptography.MD5().ComputeHash(data); +#endif + return BitConverter.ToString(hash).Replace("-", "").ToLower(); + } + + public static string StringToHash(string data) + { + return ByteArrayToHash(Encoding.UTF8.GetBytes(data)); + } + } +} diff --git a/QRCoder.SkiaSharpTests/QRCoder.SkiaSharpTests.csproj b/QRCoder.SkiaSharpTests/QRCoder.SkiaSharpTests.csproj new file mode 100644 index 00000000..96d4b27f --- /dev/null +++ b/QRCoder.SkiaSharpTests/QRCoder.SkiaSharpTests.csproj @@ -0,0 +1,31 @@ + + + + net6.0 + enable + enable + + false + + + + + + + + + + + + + + + + Always + + + Always + + + + diff --git a/QRCoder.SkiaSharpTests/SkiaSharpTests.cs b/QRCoder.SkiaSharpTests/SkiaSharpTests.cs new file mode 100644 index 00000000..22a6c7af --- /dev/null +++ b/QRCoder.SkiaSharpTests/SkiaSharpTests.cs @@ -0,0 +1,43 @@ +using QRCoder.SkiaSharp; +using QRCoderTests.Helpers; +using SkiaSharp; + +namespace QRCoder.SkiaSharpTests +{ + [TestClass] + public class SkiaSharpTests + { + [TestMethod] + public void TestStandardQrCodeGeneration() + { + var gen = new QRCodeGenerator(); + var data = gen.CreateQrCode("This is a quick test! 123#?", QRCodeGenerator.ECCLevel.H); + var qrcode = new SkiaSharpQRCode(data); + using var image = qrcode.GetGraphic(); + using var imageData = image.Encode(SKEncodedImageFormat.Png, 80); + using var stream = File.OpenWrite("qrcode_skia.png"); + + imageData.SaveTo(stream); + + } + + [TestMethod] + public void TestLogoQrCodeGeneration() + { + var gen = new QRCodeGenerator(); + var data = gen.CreateQrCode("This is a quick test! 123#?", QRCodeGenerator.ECCLevel.H); + var qrcode = new SkiaSharpQRCode(data); + + var logoImagePath = HelperFunctions.GetAssemblyPath() + "\\assets\\noun_software engineer_2909346.png"; + + using var logoStream = File.OpenRead(logoImagePath); + using var logoImage = SKImage.FromEncodedData(logoStream); + + using var image = qrcode.GetGraphic(60, SKColors.Black,SKColors.White, SKColors.Red, logoImage, Models.LogoLocation.Center, Models.LogoBackgroundShape.Circle); + using var imageData = image.Encode(SKEncodedImageFormat.Png, 80); + using var stream = File.OpenWrite("qrcode_skia_logo.png"); + + imageData.SaveTo(stream); + } + } +} \ No newline at end of file diff --git a/QRCoder.SkiaSharpTests/Usings.cs b/QRCoder.SkiaSharpTests/Usings.cs new file mode 100644 index 00000000..ab67c7ea --- /dev/null +++ b/QRCoder.SkiaSharpTests/Usings.cs @@ -0,0 +1 @@ +global using Microsoft.VisualStudio.TestTools.UnitTesting; \ No newline at end of file diff --git a/QRCoder.SkiaSharpTests/assets/noun_Scientist_2909361.svg b/QRCoder.SkiaSharpTests/assets/noun_Scientist_2909361.svg new file mode 100644 index 00000000..869a0e73 --- /dev/null +++ b/QRCoder.SkiaSharpTests/assets/noun_Scientist_2909361.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/QRCoder.SkiaSharpTests/assets/noun_software engineer_2909346.png b/QRCoder.SkiaSharpTests/assets/noun_software engineer_2909346.png new file mode 100644 index 00000000..bc100986 Binary files /dev/null and b/QRCoder.SkiaSharpTests/assets/noun_software engineer_2909346.png differ diff --git a/QRCoder.sln b/QRCoder.sln index 5e8de931..2768e04a 100644 --- a/QRCoder.sln +++ b/QRCoder.sln @@ -15,6 +15,16 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "QRCoderTests", "QRCoderTest EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "QRCoder.Xaml", "QRCoder.Xaml\QRCoder.Xaml.csproj", "{A7A7E073-2504-4BA2-A63B-87AC34174789}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "QRCoder.ImageSharp", "QRCoder.ImageSharp\QRCoder.ImageSharp.csproj", "{229BC36C-54D6-43FC-9D82-EE0D6FD9883C}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "QRCoder.ImageSharpTests", "QRCoder.ImageSharpTests\QRCoder.ImageSharpTests.csproj", "{F5596C43-DDB9-47BC-BCA0-2308A59F630B}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{3E51E726-E6AB-492B-A37D-DF1CE024D54F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "QRCoder.SkiaSharp", "QRCoder.SkiaSharp\QRCoder.SkiaSharp.csproj", "{D339E382-4B87-4E78-B5B9-381F88F14B4F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "QRCoder.SkiaSharpTests", "QRCoder.SkiaSharpTests\QRCoder.SkiaSharpTests.csproj", "{8F2B4B7D-ACEE-45CE-B691-CA0B06EDAB95}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -119,10 +129,79 @@ Global {A7A7E073-2504-4BA2-A63B-87AC34174789}.Release|x64.Build.0 = Release|Any CPU {A7A7E073-2504-4BA2-A63B-87AC34174789}.Release|x86.ActiveCfg = Release|Any CPU {A7A7E073-2504-4BA2-A63B-87AC34174789}.Release|x86.Build.0 = Release|Any CPU + {229BC36C-54D6-43FC-9D82-EE0D6FD9883C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {229BC36C-54D6-43FC-9D82-EE0D6FD9883C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {229BC36C-54D6-43FC-9D82-EE0D6FD9883C}.Debug|ARM.ActiveCfg = Debug|Any CPU + {229BC36C-54D6-43FC-9D82-EE0D6FD9883C}.Debug|ARM.Build.0 = Debug|Any CPU + {229BC36C-54D6-43FC-9D82-EE0D6FD9883C}.Debug|x64.ActiveCfg = Debug|Any CPU + {229BC36C-54D6-43FC-9D82-EE0D6FD9883C}.Debug|x64.Build.0 = Debug|Any CPU + {229BC36C-54D6-43FC-9D82-EE0D6FD9883C}.Debug|x86.ActiveCfg = Debug|Any CPU + {229BC36C-54D6-43FC-9D82-EE0D6FD9883C}.Debug|x86.Build.0 = Debug|Any CPU + {229BC36C-54D6-43FC-9D82-EE0D6FD9883C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {229BC36C-54D6-43FC-9D82-EE0D6FD9883C}.Release|Any CPU.Build.0 = Release|Any CPU + {229BC36C-54D6-43FC-9D82-EE0D6FD9883C}.Release|ARM.ActiveCfg = Release|Any CPU + {229BC36C-54D6-43FC-9D82-EE0D6FD9883C}.Release|ARM.Build.0 = Release|Any CPU + {229BC36C-54D6-43FC-9D82-EE0D6FD9883C}.Release|x64.ActiveCfg = Release|Any CPU + {229BC36C-54D6-43FC-9D82-EE0D6FD9883C}.Release|x64.Build.0 = Release|Any CPU + {229BC36C-54D6-43FC-9D82-EE0D6FD9883C}.Release|x86.ActiveCfg = Release|Any CPU + {229BC36C-54D6-43FC-9D82-EE0D6FD9883C}.Release|x86.Build.0 = Release|Any CPU + {F5596C43-DDB9-47BC-BCA0-2308A59F630B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F5596C43-DDB9-47BC-BCA0-2308A59F630B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F5596C43-DDB9-47BC-BCA0-2308A59F630B}.Debug|ARM.ActiveCfg = Debug|Any CPU + {F5596C43-DDB9-47BC-BCA0-2308A59F630B}.Debug|ARM.Build.0 = Debug|Any CPU + {F5596C43-DDB9-47BC-BCA0-2308A59F630B}.Debug|x64.ActiveCfg = Debug|Any CPU + {F5596C43-DDB9-47BC-BCA0-2308A59F630B}.Debug|x64.Build.0 = Debug|Any CPU + {F5596C43-DDB9-47BC-BCA0-2308A59F630B}.Debug|x86.ActiveCfg = Debug|Any CPU + {F5596C43-DDB9-47BC-BCA0-2308A59F630B}.Debug|x86.Build.0 = Debug|Any CPU + {F5596C43-DDB9-47BC-BCA0-2308A59F630B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F5596C43-DDB9-47BC-BCA0-2308A59F630B}.Release|Any CPU.Build.0 = Release|Any CPU + {F5596C43-DDB9-47BC-BCA0-2308A59F630B}.Release|ARM.ActiveCfg = Release|Any CPU + {F5596C43-DDB9-47BC-BCA0-2308A59F630B}.Release|ARM.Build.0 = Release|Any CPU + {F5596C43-DDB9-47BC-BCA0-2308A59F630B}.Release|x64.ActiveCfg = Release|Any CPU + {F5596C43-DDB9-47BC-BCA0-2308A59F630B}.Release|x64.Build.0 = Release|Any CPU + {F5596C43-DDB9-47BC-BCA0-2308A59F630B}.Release|x86.ActiveCfg = Release|Any CPU + {F5596C43-DDB9-47BC-BCA0-2308A59F630B}.Release|x86.Build.0 = Release|Any CPU + {D339E382-4B87-4E78-B5B9-381F88F14B4F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D339E382-4B87-4E78-B5B9-381F88F14B4F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D339E382-4B87-4E78-B5B9-381F88F14B4F}.Debug|ARM.ActiveCfg = Debug|Any CPU + {D339E382-4B87-4E78-B5B9-381F88F14B4F}.Debug|ARM.Build.0 = Debug|Any CPU + {D339E382-4B87-4E78-B5B9-381F88F14B4F}.Debug|x64.ActiveCfg = Debug|Any CPU + {D339E382-4B87-4E78-B5B9-381F88F14B4F}.Debug|x64.Build.0 = Debug|Any CPU + {D339E382-4B87-4E78-B5B9-381F88F14B4F}.Debug|x86.ActiveCfg = Debug|Any CPU + {D339E382-4B87-4E78-B5B9-381F88F14B4F}.Debug|x86.Build.0 = Debug|Any CPU + {D339E382-4B87-4E78-B5B9-381F88F14B4F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D339E382-4B87-4E78-B5B9-381F88F14B4F}.Release|Any CPU.Build.0 = Release|Any CPU + {D339E382-4B87-4E78-B5B9-381F88F14B4F}.Release|ARM.ActiveCfg = Release|Any CPU + {D339E382-4B87-4E78-B5B9-381F88F14B4F}.Release|ARM.Build.0 = Release|Any CPU + {D339E382-4B87-4E78-B5B9-381F88F14B4F}.Release|x64.ActiveCfg = Release|Any CPU + {D339E382-4B87-4E78-B5B9-381F88F14B4F}.Release|x64.Build.0 = Release|Any CPU + {D339E382-4B87-4E78-B5B9-381F88F14B4F}.Release|x86.ActiveCfg = Release|Any CPU + {D339E382-4B87-4E78-B5B9-381F88F14B4F}.Release|x86.Build.0 = Release|Any CPU + {8F2B4B7D-ACEE-45CE-B691-CA0B06EDAB95}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8F2B4B7D-ACEE-45CE-B691-CA0B06EDAB95}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8F2B4B7D-ACEE-45CE-B691-CA0B06EDAB95}.Debug|ARM.ActiveCfg = Debug|Any CPU + {8F2B4B7D-ACEE-45CE-B691-CA0B06EDAB95}.Debug|ARM.Build.0 = Debug|Any CPU + {8F2B4B7D-ACEE-45CE-B691-CA0B06EDAB95}.Debug|x64.ActiveCfg = Debug|Any CPU + {8F2B4B7D-ACEE-45CE-B691-CA0B06EDAB95}.Debug|x64.Build.0 = Debug|Any CPU + {8F2B4B7D-ACEE-45CE-B691-CA0B06EDAB95}.Debug|x86.ActiveCfg = Debug|Any CPU + {8F2B4B7D-ACEE-45CE-B691-CA0B06EDAB95}.Debug|x86.Build.0 = Debug|Any CPU + {8F2B4B7D-ACEE-45CE-B691-CA0B06EDAB95}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8F2B4B7D-ACEE-45CE-B691-CA0B06EDAB95}.Release|Any CPU.Build.0 = Release|Any CPU + {8F2B4B7D-ACEE-45CE-B691-CA0B06EDAB95}.Release|ARM.ActiveCfg = Release|Any CPU + {8F2B4B7D-ACEE-45CE-B691-CA0B06EDAB95}.Release|ARM.Build.0 = Release|Any CPU + {8F2B4B7D-ACEE-45CE-B691-CA0B06EDAB95}.Release|x64.ActiveCfg = Release|Any CPU + {8F2B4B7D-ACEE-45CE-B691-CA0B06EDAB95}.Release|x64.Build.0 = Release|Any CPU + {8F2B4B7D-ACEE-45CE-B691-CA0B06EDAB95}.Release|x86.ActiveCfg = Release|Any CPU + {8F2B4B7D-ACEE-45CE-B691-CA0B06EDAB95}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {1B51624B-9915-4ED6-8FC1-1B7C472246E5} = {3E51E726-E6AB-492B-A37D-DF1CE024D54F} + {F5596C43-DDB9-47BC-BCA0-2308A59F630B} = {3E51E726-E6AB-492B-A37D-DF1CE024D54F} + {8F2B4B7D-ACEE-45CE-B691-CA0B06EDAB95} = {3E51E726-E6AB-492B-A37D-DF1CE024D54F} + EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {F1845CDF-5EE5-456F-B6C8-717E4E2284F4} EndGlobalSection diff --git a/QRCoder/Models/LogoBackgroundShape.cs b/QRCoder/Models/LogoBackgroundShape.cs new file mode 100644 index 00000000..f61ffc0e --- /dev/null +++ b/QRCoder/Models/LogoBackgroundShape.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace QRCoder.Models +{ + public enum LogoBackgroundShape + { + Circle, + Rectangle + } +} diff --git a/QRCoder/Models/LogoLocation.cs b/QRCoder/Models/LogoLocation.cs new file mode 100644 index 00000000..4874125e --- /dev/null +++ b/QRCoder/Models/LogoLocation.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace QRCoder.Models +{ + public enum LogoLocation + { + Center, + BottomRight + } +} diff --git a/QRCoderTests/ArtQRCodeRendererTests.cs b/QRCoderTests/ArtQRCodeRendererTests.cs index bcd32c9f..fd31535e 100644 --- a/QRCoderTests/ArtQRCodeRendererTests.cs +++ b/QRCoderTests/ArtQRCodeRendererTests.cs @@ -21,7 +21,7 @@ public void can_create_standard_qrcode_graphic() var gen = new QRCodeGenerator(); var data = gen.CreateQrCode("This is a quick test! 123#?", QRCodeGenerator.ECCLevel.H); var bmp = new ArtQRCode(data).GetGraphic(10); - + bmp.Save("original.png"); var result = HelperFunctions.BitmapToHash(bmp); #if NET35_OR_GREATER || NET40_OR_GREATER result.ShouldBe("11ebdda91b9632d016798cb6de2f5339"); @@ -38,7 +38,7 @@ public void can_create_standard_qrcode_graphic_with_custom_finder() var data = gen.CreateQrCode("This is a quick test! 123#?", QRCodeGenerator.ECCLevel.H); var finder = new Bitmap(15, 15); var bmp = new ArtQRCode(data).GetGraphic(10, Color.Black, Color.White, Color.Transparent, finderPatternImage: finder); - + bmp.Save("finder.png"); var result = HelperFunctions.BitmapToHash(bmp); #if NET35_OR_GREATER || NET40_OR_GREATER result.ShouldBe("c54a7389ae995abc838f0d228acc3bad"); @@ -71,7 +71,7 @@ public void can_create_standard_qrcode_graphic_with_background() var data = gen.CreateQrCode("This is a quick test! 123#?", QRCodeGenerator.ECCLevel.H); var bmp = new ArtQRCode(data).GetGraphic((Bitmap)Image.FromFile(HelperFunctions.GetAssemblyPath() + "\\assets\\noun_software engineer_2909346.png")); //Used logo is licensed under public domain. Ref.: https://thenounproject.com/Iconathon1/collection/redefining-women/?i=2909346 - + bmp.Save("custom_background.png"); var result = HelperFunctions.BitmapToHash(bmp); #if NET35_OR_GREATER || NET40_OR_GREATER diff --git a/QRCoderTests/Helpers/HelperFunctions.cs b/QRCoderTests/Helpers/HelperFunctions.cs index 5fd9aa13..484aa854 100644 --- a/QRCoderTests/Helpers/HelperFunctions.cs +++ b/QRCoderTests/Helpers/HelperFunctions.cs @@ -2,6 +2,7 @@ using System.Text; using System.IO; using System.Security.Cryptography; + #if !NETCOREAPP1_1 using System.Drawing; #endif diff --git a/QRCoderTests/QRCoderTests.csproj b/QRCoderTests/QRCoderTests.csproj index 3f1aa274..751a5775 100644 --- a/QRCoderTests/QRCoderTests.csproj +++ b/QRCoderTests/QRCoderTests.csproj @@ -58,6 +58,9 @@ Always + + + $(MSBuildProgramFiles32)\Reference Assemblies\Microsoft\Framework\.NETFramework\v3.5\Profile\Client false diff --git a/QRCoderTests/SvgQRCodeRendererTests.cs b/QRCoderTests/SvgQRCodeRendererTests.cs index b0620594..709cd618 100644 --- a/QRCoderTests/SvgQRCodeRendererTests.cs +++ b/QRCoderTests/SvgQRCodeRendererTests.cs @@ -117,7 +117,7 @@ public void can_render_svg_qrcode_with_png_logo() logoObj.GetMediaType().ShouldBe(SvgQRCode.SvgLogo.MediaType.PNG); var svg = new SvgQRCode(data).GetGraphic(10, Color.DarkGray, Color.White, logo: logoObj); - + var result = HelperFunctions.StringToHash(svg); result.ShouldBe("78e02e8ba415f15817d5ed88c4afca31"); }