Skip to content

Commit

Permalink
Add better support for QR Codes (#144)
Browse files Browse the repository at this point in the history
* Add better support for QR Codes

- Implement `^FT` Positioning (#142)
- Implement Error Correction (#67)
- Parse FieldData correctly for Normal Mode and Mixed Mode

* Add RenderFormat and RenderQuality to DrawerOptions (#141)

* Drop QRCoder Dependency

* Fix XML docstrings

* Bump version numbers
  • Loading branch information
primo-ppcg authored Nov 25, 2022
1 parent 44e1a6c commit 93caafc
Show file tree
Hide file tree
Showing 11 changed files with 192 additions and 83 deletions.
2 changes: 1 addition & 1 deletion src/BinaryKits.Zpl.Label/BinaryKits.Zpl.Label.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<TargetFrameworks>net472;netstandard2.0;net6.0</TargetFrameworks>
<Description>This package allows you to simply and reliably prepare labels complying with the Zebra programming language, using predefined class/typing. It also supports you with image conversion.</Description>
<Company>Binary Kits Pte. Ltd.</Company>
<Version>3.1.3</Version>
<Version>3.1.4</Version>
<Authors>Binary Kits Pte. Ltd.</Authors>
<PackageReleaseNotes></PackageReleaseNotes>
<PackageTags>Zebra ZPL ZPL2 Printer Label</PackageTags>
Expand Down
19 changes: 16 additions & 3 deletions src/BinaryKits.Zpl.Label/Elements/ZplQrCode.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,11 @@ public class ZplQrCode : ZplPositionedElementBase, IFormatElement
public int MagnificationFactor { get; private set; }

public ErrorCorrectionLevel ErrorCorrectionLevel { get; private set; }

public int MaskValue { get; private set; }

public FieldOrientation FieldOrientation { get; protected set; }

/// <summary>
/// Zpl QrCode
/// </summary>
Expand All @@ -23,21 +26,31 @@ public class ZplQrCode : ZplPositionedElementBase, IFormatElement
/// <param name="magnificationFactor">Size of the QR code, 1 on 150 dpi printers, 2 on 200 dpi printers, 3 on 300 dpi printers, 6 on 600 dpi printers</param>
/// <param name="errorCorrectionLevel"></param>
/// <param name="maskValue">0-7, (default: 7)</param>
/// <param name="fieldOrientation"></param>
/// <param name="bottomToTop"></param>
public ZplQrCode(
string content,
int positionX,
int positionY,
int model = 2,
int magnificationFactor = 2,
ErrorCorrectionLevel errorCorrectionLevel = ErrorCorrectionLevel.HighReliability,
int maskValue = 7)
: base(positionX, positionY)
int maskValue = 7,
FieldOrientation fieldOrientation = FieldOrientation.Normal,
bool bottomToTop = false)
: base(positionX, positionY, bottomToTop)
{
Content = content;
Model = model;
MagnificationFactor = magnificationFactor;
ErrorCorrectionLevel = errorCorrectionLevel;
MaskValue = maskValue;
FieldOrientation = fieldOrientation;
}

protected string RenderFieldOrientation()
{
return RenderFieldOrientation(FieldOrientation);
}

///<inheritdoc/>
Expand All @@ -48,7 +61,7 @@ public override IEnumerable<string> Render(ZplRenderOptions context)
//^ FDMM,AAC - 42 ^ FS
var result = new List<string>();
result.AddRange(RenderPosition(context));
result.Add($"^BQN,{Model},{context.Scale(MagnificationFactor)},{RenderErrorCorrectionLevel(ErrorCorrectionLevel)},{MaskValue}");
result.Add($"^BQ{RenderFieldOrientation()},{Model},{context.Scale(MagnificationFactor)},{RenderErrorCorrectionLevel(ErrorCorrectionLevel)},{MaskValue}");
result.Add($"^FD{RenderErrorCorrectionLevel(ErrorCorrectionLevel)}A,{Content}^FS");

return result;
Expand Down
3 changes: 1 addition & 2 deletions src/BinaryKits.Zpl.Viewer/BinaryKits.Zpl.Viewer.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<TargetFrameworks>net472;netstandard2.0;net6.0</TargetFrameworks>
<Description>This package provides a rendering logic for ZPL data, as an alternative to labelary.com.</Description>
<Company>Binary Kits Pte. Ltd.</Company>
<Version>1.1.3</Version>
<Version>1.1.4</Version>
<Authors>Binary Kits Pte. Ltd.</Authors>
<PackageReleaseNotes></PackageReleaseNotes>
<PackageTags>Zebra ZPL ZPL2 ZPLEmulator ZPLVirtualPrinter ZPLViewer ZPLParser</PackageTags>
Expand All @@ -23,7 +23,6 @@

<ItemGroup>
<PackageReference Include="BarcodeLib" Version="2.4.0" />
<PackageReference Include="QRCoder" Version="1.4.1" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="6.0.1" />
<PackageReference Include="SkiaSharp" Version="2.80.3" />
<PackageReference Include="ZXing.Net" Version="0.16.8" />
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,21 @@
using System;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using BinaryKits.Zpl.Label;
using BinaryKits.Zpl.Label.Elements;
using BinaryKits.Zpl.Viewer.Models;
using ZXing;
using ZXing.Datamatrix.Encoder;

namespace BinaryKits.Zpl.Viewer.CommandAnalyzers
{
public class FieldDataZplCommandAnalyzer : ZplCommandAnalyzerBase
{
private static readonly Regex qrCodeFieldDataNormalRegex = new Regex(@"^(?<correction>[HQML])(?<input>[AM]),(?<data>.+)$", RegexOptions.Compiled);
private static readonly Regex qrCodeFieldDataMixedRegex = new Regex(@"^D\d{4}[0-9A-F-a-f]{2},(?<correction>[HQML])(?<input>[AM]),(?<data>.+)$", RegexOptions.Compiled);
private static readonly Regex qrCodeFieldDataModeRegex = new Regex(@"^(?:[ANK]|(?:B(?<count>\d{4})))(?<data>.+)$", RegexOptions.Compiled);

public FieldDataZplCommandAnalyzer(VirtualPrinter virtualPrinter) : base("^FD", virtualPrinter)
{ }

Expand Down Expand Up @@ -60,13 +71,10 @@ public override ZplElementBase Analyze(string zplCommand)
}
if (this.VirtualPrinter.NextElementFieldData is QrCodeBarcodeFieldData qrCode)
{
var indexOfComma = text.IndexOf(',');
if (indexOfComma != -1)
{
text = text.Substring(indexOfComma + 1);
}
(ErrorCorrectionLevel errorCorrection, string parsedText) = ParseQrCodeFieldData(qrCode, text);

return new ZplQrCode(text, x, y, qrCode.Model, qrCode.MagnificationFactor, qrCode.ErrorCorrection, qrCode.MaskValue);
// N.B.: always pass Field Orientation Normal to QR codes; the ZPL II standard does not allow rotation
return new ZplQrCode(parsedText, x, y, qrCode.Model, qrCode.MagnificationFactor, errorCorrection, qrCode.MaskValue, Label.FieldOrientation.Normal, bottomToTop);
}
}

Expand All @@ -92,6 +100,94 @@ public override ZplElementBase Analyze(string zplCommand)
return new ZplTextField(text, x, y, font, reversePrint: reversePrint, bottomToTop: bottomToTop);
}

private (ErrorCorrectionLevel, string) ParseQrCodeFieldData(QrCodeBarcodeFieldData qrCode, string text)
{
ErrorCorrectionLevel errorCorrection = qrCode.ErrorCorrection;
string parsedText = text;

Match normalMatch = qrCodeFieldDataNormalRegex.Match(text);
if (normalMatch.Success)
{
errorCorrection = this.ConvertErrorCorrectionLevel(normalMatch.Groups["correction"].Value);
string input = normalMatch.Groups["input"].Value;
string fullData = normalMatch.Groups["data"].Value;
if (input == "A")
{
parsedText = fullData;
}
else if (input == "M")
{
Match modeMatch = qrCodeFieldDataModeRegex.Match(fullData);
if (modeMatch.Success)
{
if (modeMatch.Groups["count"].Success)
{
int count = Math.Min(int.Parse(modeMatch.Groups["count"].Value), fullData.Length);
parsedText = modeMatch.Groups["data"].Value.Substring(0, count);
}
else
{
parsedText = modeMatch.Groups["data"].Value;
}
}
else
{
parsedText = fullData;
}
}
}
else
{
Match mixedMatch = qrCodeFieldDataMixedRegex.Match(text);
if (mixedMatch.Success)
{
errorCorrection = this.ConvertErrorCorrectionLevel(mixedMatch.Groups["correction"].Value);
string input = mixedMatch.Groups["input"].Value;
string fullData = mixedMatch.Groups["data"].Value;
if (input == "A")
{
string[] dataParts = fullData.Split(',');
parsedText = string.Join("", dataParts);
}
else if (input == "M")
{
StringBuilder builder = new StringBuilder();
while (fullData.Length > 0)
{
Match modeMatch = qrCodeFieldDataModeRegex.Match(fullData);
if (modeMatch.Success)
{
string data = modeMatch.Groups["data"].Value;
if (modeMatch.Groups["count"].Success)
{
int count = Math.Min(int.Parse(modeMatch.Groups["count"].Value), data.Length);
builder.Append(data.Substring(0, count));
fullData = data.Substring(count);
}
else
{
string[] dataParts = data.Split(new char[] { ',' }, 2);
builder.Append(dataParts[0]);
fullData = dataParts.Length > 1 ? dataParts[1] : string.Empty;
}
}
else
{
string[] dataParts = fullData.Split(new char[] { ',' }, 2);
builder.Append(dataParts[0]);
fullData = dataParts.Length > 1 ? dataParts[1] : string.Empty;
}
}

parsedText = builder.ToString();
}
}

}

return (errorCorrection, parsedText);
}

private ZplFont GetFontFromVirtualPrinter()
{
var fontWidth = this.VirtualPrinter.FontWidth;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,21 +35,7 @@ public override ZplElementBase Analyze(string zplCommand)
}
if (zplDataParts.Length > 3)
{
switch (zplDataParts[3])
{
case "H":
errorCorrection = ErrorCorrectionLevel.UltraHighReliability;
break;
case "Q":
errorCorrection = ErrorCorrectionLevel.HighReliability;
break;
case "M":
errorCorrection = ErrorCorrectionLevel.Standard;
break;
case "L":
errorCorrection = ErrorCorrectionLevel.HighDensity;
break;
}
errorCorrection = this.ConvertErrorCorrectionLevel(zplDataParts[3]);
}
if (zplDataParts.Length > 4)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,19 +29,26 @@ protected string[] SplitCommand(string zplCommand, int dataStartIndex = 0)

protected FieldOrientation ConvertFieldOrientation(string fieldOrientation)
{
switch (fieldOrientation)
return fieldOrientation switch
{
case "N":
return FieldOrientation.Normal;
case "R":
return FieldOrientation.Rotated90;
case "I":
return FieldOrientation.Rotated180;
case "B":
return FieldOrientation.Rotated270;
}
"N" => FieldOrientation.Normal,
"R" => FieldOrientation.Rotated90,
"I" => FieldOrientation.Rotated180,
"B" => FieldOrientation.Rotated270,
_ => FieldOrientation.Normal,
};
}

return FieldOrientation.Normal;
protected ErrorCorrectionLevel ConvertErrorCorrectionLevel(string errorCorrection)
{
return errorCorrection switch
{
"H" => ErrorCorrectionLevel.UltraHighReliability,
"Q" => ErrorCorrectionLevel.HighReliability,
"M" => ErrorCorrectionLevel.Standard,
"L" => ErrorCorrectionLevel.HighDensity,
_ => ErrorCorrectionLevel.Standard,
};
}

protected bool ConvertBoolean(string yesOrNo, string defaultValue = "N")
Expand Down
22 changes: 20 additions & 2 deletions src/BinaryKits.Zpl.Viewer/ElementDrawers/BarcodeDrawerBase.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
using SkiaSharp;
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
using ZXing.Common;

namespace BinaryKits.Zpl.Viewer.ElementDrawers
{
Expand All @@ -10,7 +12,7 @@ public byte[] GetImageData(Image image)
{
using (var memoryStream = new MemoryStream())
{
image.Save(memoryStream, System.Drawing.Imaging.ImageFormat.Png);
image.Save(memoryStream, ImageFormat.Png);
return memoryStream.ToArray();
}
}
Expand Down Expand Up @@ -72,8 +74,24 @@ public void DrawBarcode(
this._skCanvas.SetMatrix(matrix);
}

this._skCanvas.DrawBitmap(SKBitmap.Decode(barcodeImageData), x, y );
this._skCanvas.DrawBitmap(SKBitmap.Decode(barcodeImageData), x, y);
}
}

protected SKBitmap BitMatrixToSKBitmap(BitMatrix matrix, int pixelScale)
{
using var image = new SKBitmap(matrix.Width, matrix.Height);

for (int row = 0; row < matrix.Height; row++)
{
for (int col = 0; col < matrix.Width; col++)
{
var color = matrix[col, row] ? SKColors.Black : SKColors.Transparent;
image.SetPixel(col, row, color);
}
}

return image.Resize(new SKSizeI(image.Width * pixelScale, image.Height * pixelScale), SKFilterQuality.None);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,19 +29,7 @@ public override void Draw(ZplElementBase element)
};
var result = writer.encode(dataMatrix.Content, BarcodeFormat.DATA_MATRIX, 0, 0, hints);

int size = dataMatrix.Height;
using var image = new SKBitmap(result.Width, result.Height);

for (int row = 0; row < result.Height; row++)
{
for (int col = 0; col < result.Width; col++)
{
var color = result[col, row] ? SKColors.Black : SKColors.White;
image.SetPixel(col, row, color);
}
}

using var resizedImage = image.Resize(new SKSizeI(image.Width * size, image.Height * size), SKFilterQuality.None);
using var resizedImage = this.BitMatrixToSKBitmap(result, dataMatrix.Height);

var png = resizedImage.Encode(SKEncodedImageFormat.Png, 100).ToArray();
this.DrawBarcode(png, resizedImage.Height, resizedImage.Width, dataMatrix.FieldOrigin != null, x, y, 0, dataMatrix.FieldOrientation);
Expand Down
4 changes: 4 additions & 0 deletions src/BinaryKits.Zpl.Viewer/ElementDrawers/DrawerOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ public class DrawerOptions
{
public Func<string, SKTypeface> FontLoader { get; set; } = DefaultFontLoader;

public SKEncodedImageFormat RenderFormat { get; set; } = SKEncodedImageFormat.Png;

public int RenderQuality { get; set; } = 80;

public static Func<string, SKTypeface> DefaultFontLoader = fontName => {
var typeface = SKTypeface.Default;
if (fontName == "0")
Expand Down
Loading

0 comments on commit 93caafc

Please sign in to comment.