Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds Support for ImageSharp and SkiaSharp using two independent projects for maximum compatibility #407

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
237 changes: 237 additions & 0 deletions 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
{
/// <summary>
/// Constructor without params to be used in COM Objects connections
/// </summary>
public ArtQRCode() { }

/// <summary>
/// Creates new ArtQrCode object
/// </summary>
/// <param name="data">QRCodeData generated by the QRCodeGenerator</param>
public ArtQRCode(QRCodeData data) : base(data) { }

/// <summary>
/// Renders an art-style QR code with dots as modules. (With default settings: DarkColor=Black, LightColor=White, Background=Transparent, QuietZone=true)
/// </summary>
/// <param name="pixelsPerModule">Amount of px each dark/light module of the QR code shall take place in the final QR code image</param>
/// <returns>QRCode graphic as bitmap</returns>
public Image GetGraphic(int pixelsPerModule)
{
return this.GetGraphic(pixelsPerModule, Color.Black, Color.White, Color.Transparent);
}

/// <summary>
/// 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)
/// </summary>
/// <param name="backgroundImage">A bitmap object that will be used as background picture</param>
/// <returns>QRCode graphic as bitmap</returns>
public Image GetGraphic(Image? backgroundImage = null)
{
return this.GetGraphic(10, Color.Black, Color.White, Color.Transparent, backgroundImage: backgroundImage);
}

/// <summary>
/// Renders an art-style QR code with dots as modules and various user settings
/// </summary>
/// <param name="pixelsPerModule">Amount of px each dark/light module of the QR code shall take place in the final QR code image</param>
/// <param name="darkColor">Color of the dark modules</param>
/// <param name="lightColor">Color of the light modules</param>
/// <param name="backgroundColor">Color of the background</param>
/// <param name="backgroundImage">A bitmap object that will be used as background picture</param>
/// <param name="pixelSizeFactor">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.</param>
/// <param name="drawQuietZones">If true a white border is drawn around the whole QR Code</param>
/// <param name="quietZoneRenderingStyle">Style of the quiet zones</param>
/// <param name="backgroundImageStyle">Style of the background image (if set). Fill=spanning complete graphic; DataAreaOnly=Don't paint background into quietzone</param>
/// <param name="finderPatternImage">Optional image that should be used instead of the default finder patterns</param>
/// <returns>QRCode graphic as bitmap</returns>
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<Rgba32>(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;
}

/// <summary>
/// Checks if a given module(-position) is part of the quietzone of a QR code
/// </summary>
/// <param name="x">X position</param>
/// <param name="y">Y position</param>
/// <param name="numModules">Total number of modules per row</param>
/// <returns>true, if position is part of quiet zone</returns>
private bool IsPartOfQuietZone(int x, int y, int numModules)
{
return
x < 4 || //left
y < 4 || //top
x > numModules - 5 || //right
y > numModules - 5; //bottom
}


/// <summary>
/// Checks if a given module(-position) is part of one of the three finder patterns of a QR code
/// </summary>
/// <param name="x">X position</param>
/// <param name="y">Y position</param>
/// <param name="numModules">Total number of modules per row</param>
/// <param name="offset">Offset in modules (usually depending on drawQuietZones parameter)</param>
/// <returns>true, if position is part of any finder pattern</returns>
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
}

/// <summary>
/// Defines how the quiet zones shall be rendered.
/// </summary>
public enum QuietZoneStyle
{
Dotted,
Flat
}

/// <summary>
/// Defines how the background image (if set) shall be rendered.
/// </summary>
public enum BackgroundImageStyle
{
Fill,
DataAreaOnly
}
}

public static class ArtQRCodeHelper
{
/// <summary>
/// Helper function to create an ArtQRCode graphic with a single function call
/// </summary>
/// <param name="plainText">Text/payload to be encoded inside the QR code</param>
/// <param name="pixelsPerModule">Amount of px each dark/light module of the QR code shall take place in the final QR code image</param>
/// <param name="darkColor">Color of the dark modules</param>
/// <param name="lightColor">Color of the light modules</param>
/// <param name="backgroundColor">Color of the background</param>
/// <param name="eccLevel">The level of error correction data</param>
/// <param name="forceUtf8">Shall the generator be forced to work in UTF-8 mode?</param>
/// <param name="utf8BOM">Should the byte-order-mark be used?</param>
/// <param name="eciMode">Which ECI mode shall be used?</param>
/// <param name="requestedVersion">Set fixed QR code target version.</param>
/// <param name="backgroundImage">A bitmap object that will be used as background picture</param>
/// <param name="pixelSizeFactor">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.</param>
/// <param name="drawQuietZones">If true a white border is drawn around the whole QR Code</param>
/// <param name="quietZoneRenderingStyle">Style of the quiet zones</param>
/// <param name="backgroundImageStyle">Style of the background image (if set). Fill=spanning complete graphic; DataAreaOnly=Don't paint background into quietzone</param>
/// <param name="finderPatternImage">Optional image that should be used instead of the default finder patterns</param>
/// <returns>QRCode graphic as bitmap</returns>
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