Skip to content

Commit 6b07a08

Browse files
committed
Add Barcode #181
1 parent 33cdee2 commit 6b07a08

19 files changed

Lines changed: 1846 additions & 29 deletions
Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
namespace DevWinUI;
2+
3+
/// <summary>
4+
/// Represents a generated barcode as a boolean matrix of modules.
5+
/// </summary>
6+
public sealed partial class Barcode
7+
{
8+
private readonly bool[,] _modules;
9+
10+
internal Barcode(bool[,] modules, BarcodeType type)
11+
{
12+
_modules = modules;
13+
Type = type;
14+
Height = modules.GetLength(0);
15+
Width = modules.GetLength(1);
16+
}
17+
18+
/// <summary>Gets the type of barcode.</summary>
19+
public BarcodeType Type { get; }
20+
21+
/// <summary>Gets the width (number of columns).</summary>
22+
public int Width { get; }
23+
24+
/// <summary>Gets the height (number of rows).</summary>
25+
public int Height { get; }
26+
27+
/// <summary>
28+
/// Gets whether the module at the specified position is dark.
29+
/// </summary>
30+
/// <param name="row">The row index (0-based).</param>
31+
/// <param name="column">The column index (0-based).</param>
32+
/// <returns><see langword="true"/> if the module is dark; otherwise, <see langword="false"/>.</returns>
33+
public bool this[int row, int column] => _modules[row, column];
34+
35+
/// <summary>
36+
/// Creates a Code 39 barcode from the specified data.
37+
/// </summary>
38+
/// <param name="data">The data to encode.</param>
39+
/// <param name="includeChecksum"><see langword="true"/> to append the optional Mod 43 checksum character; otherwise, <see langword="false"/>.</param>
40+
/// <returns>A new <see cref="Barcode"/> instance.</returns>
41+
public static Barcode CreateCode39(string data, bool includeChecksum = false)
42+
{
43+
ArgumentNullException.ThrowIfNull(data);
44+
if (data.Length == 0)
45+
{
46+
throw new ArgumentException("The data cannot be empty.", nameof(data));
47+
}
48+
49+
return Code39Encoder.Encode(data, includeChecksum);
50+
}
51+
52+
/// <summary>
53+
/// Creates a Code 128 barcode from the specified data.
54+
/// </summary>
55+
/// <param name="data">The data to encode.</param>
56+
/// <returns>A new <see cref="Barcode"/> instance.</returns>
57+
public static Barcode CreateCode128(string data)
58+
{
59+
ArgumentNullException.ThrowIfNull(data);
60+
if (data.Length == 0)
61+
{
62+
throw new ArgumentException("The data cannot be empty.", nameof(data));
63+
}
64+
65+
return Code128Encoder.Encode(data);
66+
}
67+
68+
/// <summary>
69+
/// Creates a Code 93 barcode from the specified data.
70+
/// </summary>
71+
/// <param name="data">The data to encode.</param>
72+
/// <returns>A new <see cref="Barcode"/> instance.</returns>
73+
public static Barcode CreateCode93(string data)
74+
{
75+
ArgumentNullException.ThrowIfNull(data);
76+
if (data.Length == 0)
77+
{
78+
throw new ArgumentException("The data cannot be empty.", nameof(data));
79+
}
80+
81+
return Code93Encoder.Encode(data);
82+
}
83+
84+
/// <summary>
85+
/// Creates an EAN-8 barcode from the specified data.
86+
/// </summary>
87+
/// <param name="data">The data to encode (7 digits without checksum, or 8 digits with checksum).</param>
88+
/// <param name="extension">Optional UPC/EAN 2-digit or 5-digit supplemental extension.</param>
89+
/// <returns>A new <see cref="Barcode"/> instance.</returns>
90+
public static Barcode CreateEan8(string data, string? extension = null)
91+
{
92+
ArgumentNullException.ThrowIfNull(data);
93+
if (data.Length == 0)
94+
{
95+
throw new ArgumentException("The data cannot be empty.", nameof(data));
96+
}
97+
98+
return EanUpcEncoder.EncodeEan8(data, extension);
99+
}
100+
101+
/// <summary>
102+
/// Creates an EAN-13 barcode from the specified data.
103+
/// </summary>
104+
/// <param name="data">The data to encode (12 digits without checksum, or 13 digits with checksum).</param>
105+
/// <param name="extension">Optional UPC/EAN 2-digit or 5-digit supplemental extension.</param>
106+
/// <returns>A new <see cref="Barcode"/> instance.</returns>
107+
public static Barcode CreateEan13(string data, string? extension = null)
108+
{
109+
ArgumentNullException.ThrowIfNull(data);
110+
if (data.Length == 0)
111+
{
112+
throw new ArgumentException("The data cannot be empty.", nameof(data));
113+
}
114+
115+
return EanUpcEncoder.EncodeEan13(data, extension);
116+
}
117+
118+
/// <summary>
119+
/// Creates a UPC-A barcode from the specified data.
120+
/// </summary>
121+
/// <param name="data">The data to encode (11 digits without checksum, or 12 digits with checksum).</param>
122+
/// <param name="extension">Optional UPC/EAN 2-digit or 5-digit supplemental extension.</param>
123+
/// <returns>A new <see cref="Barcode"/> instance.</returns>
124+
public static Barcode CreateUpcA(string data, string? extension = null)
125+
{
126+
ArgumentNullException.ThrowIfNull(data);
127+
if (data.Length == 0)
128+
{
129+
throw new ArgumentException("The data cannot be empty.", nameof(data));
130+
}
131+
132+
return EanUpcEncoder.EncodeUpcA(data, extension);
133+
}
134+
135+
/// <summary>
136+
/// Creates a Codabar barcode from the specified data.
137+
/// </summary>
138+
/// <param name="data">The data to encode.</param>
139+
/// <param name="startCharacter">The start character (<c>A</c>, <c>B</c>, <c>C</c>, or <c>D</c>).</param>
140+
/// <param name="stopCharacter">The stop character (<c>A</c>, <c>B</c>, <c>C</c>, or <c>D</c>).</param>
141+
/// <returns>A new <see cref="Barcode"/> instance.</returns>
142+
public static Barcode CreateCodabar(string data, char startCharacter = 'A', char stopCharacter = 'B')
143+
{
144+
ArgumentNullException.ThrowIfNull(data);
145+
if (data.Length == 0)
146+
{
147+
throw new ArgumentException("The data cannot be empty.", nameof(data));
148+
}
149+
150+
return CodabarEncoder.Encode(data, startCharacter, stopCharacter);
151+
}
152+
153+
/// <summary>
154+
/// Creates an Interleaved 2 of 5 (ITF) barcode from the specified data.
155+
/// </summary>
156+
/// <param name="data">The data to encode (digits only, even number of digits).</param>
157+
/// <returns>A new <see cref="Barcode"/> instance.</returns>
158+
public static Barcode CreateItf(string data)
159+
{
160+
ArgumentNullException.ThrowIfNull(data);
161+
if (data.Length == 0)
162+
{
163+
throw new ArgumentException("The data cannot be empty.", nameof(data));
164+
}
165+
166+
return ItfEncoder.Encode(data);
167+
}
168+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
namespace DevWinUI;
2+
/// <summary>
3+
/// Options for rendering a barcode as PNG.
4+
/// </summary>
5+
public sealed partial class BarcodePngOptions
6+
{
7+
/// <summary>Gets or sets the width of each module in pixels. Default is 2.</summary>
8+
public int ModuleWidth { get; set; } = 2;
9+
10+
/// <summary>Gets or sets the height of each module in pixels. Default is 80.</summary>
11+
public int ModuleHeight { get; set; } = 80;
12+
13+
/// <summary>Gets or sets the number of quiet zone modules on the left and right sides of the barcode. Default is 10.</summary>
14+
public int QuietZoneModules { get; set; } = 10;
15+
16+
/// <summary>Gets or sets the color for dark modules. Default is <see cref="Color.Black"/>.</summary>
17+
public Color DarkColor { get; set; } = Color.Black;
18+
19+
/// <summary>Gets or sets the color for light modules. Default is <see cref="Color.White"/>.</summary>
20+
public Color LightColor { get; set; } = Color.White;
21+
}
Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
using System.Buffers.Binary;
2+
using System.IO.Compression;
3+
4+
namespace DevWinUI;
5+
6+
/// <summary>
7+
/// Provides methods to render a barcode as a PNG image.
8+
/// </summary>
9+
public static partial class BarcodePngRenderer
10+
{
11+
private static readonly byte[] PngSignature = [137, 80, 78, 71, 13, 10, 26, 10];
12+
private static readonly byte[] IhdrChunkType = [73, 72, 68, 82];
13+
private static readonly byte[] IdatChunkType = [73, 68, 65, 84];
14+
private static readonly byte[] IendChunkType = [73, 69, 78, 68];
15+
private static readonly uint[] Crc32Table = InitializeCrc32Table();
16+
17+
/// <summary>Renders the barcode as PNG bytes with default options.</summary>
18+
public static byte[] ToPng(this Barcode barcode)
19+
{
20+
return ToPng(barcode, new BarcodePngOptions());
21+
}
22+
23+
/// <summary>Renders the barcode as PNG bytes with the specified options.</summary>
24+
public static byte[] ToPng(this Barcode barcode, BarcodePngOptions options)
25+
{
26+
using var stream = new MemoryStream();
27+
WriteToPng(barcode, stream, options);
28+
return stream.ToArray();
29+
}
30+
31+
/// <summary>Writes the barcode as PNG to the specified stream with default options.</summary>
32+
public static void WriteToPng(this Barcode barcode, Stream stream)
33+
{
34+
WriteToPng(barcode, stream, new BarcodePngOptions());
35+
}
36+
37+
/// <summary>Writes the barcode as PNG to the specified stream with the specified options.</summary>
38+
public static void WriteToPng(this Barcode barcode, Stream stream, BarcodePngOptions options)
39+
{
40+
ArgumentNullException.ThrowIfNull(barcode);
41+
ArgumentNullException.ThrowIfNull(stream);
42+
ArgumentNullException.ThrowIfNull(options);
43+
ArgumentOutOfRangeException.ThrowIfLessThan(options.ModuleWidth, 1);
44+
ArgumentOutOfRangeException.ThrowIfLessThan(options.ModuleHeight, 1);
45+
ArgumentOutOfRangeException.ThrowIfNegative(options.QuietZoneModules);
46+
47+
var width = GetTotalDimensionWithQuietZone(barcode.Width, options.QuietZoneModules, options.ModuleWidth, nameof(options.ModuleWidth));
48+
var height = GetTotalDimension(barcode.Height, options.ModuleHeight, nameof(options.ModuleHeight));
49+
var imageData = CreateImageData(barcode, width, height, options);
50+
var compressedImageData = CompressImageData(imageData);
51+
52+
WritePng(stream, width, height, compressedImageData);
53+
}
54+
55+
private static int GetTotalDimensionWithQuietZone(int size, int quietZoneModules, int moduleSize, string parameterName)
56+
{
57+
var value = ((long)size + (2L * quietZoneModules)) * moduleSize;
58+
if (value > int.MaxValue)
59+
{
60+
throw new ArgumentOutOfRangeException(parameterName, "The output image dimensions are too large.");
61+
}
62+
63+
return (int)value;
64+
}
65+
66+
private static int GetTotalDimension(int size, int moduleSize, string parameterName)
67+
{
68+
var value = (long)size * moduleSize;
69+
if (value > int.MaxValue)
70+
{
71+
throw new ArgumentOutOfRangeException(parameterName, "The output image dimensions are too large.");
72+
}
73+
74+
return (int)value;
75+
}
76+
77+
private static byte[] CreateImageData(Barcode barcode, int width, int height, BarcodePngOptions options)
78+
{
79+
var stride = (width * 4) + 1;
80+
var dataLength = (long)stride * height;
81+
if (dataLength > int.MaxValue)
82+
{
83+
throw new ArgumentOutOfRangeException(nameof(options), "The output image is too large.");
84+
}
85+
86+
var result = new byte[(int)dataLength];
87+
for (var row = 0; row < height; row++)
88+
{
89+
var rowOffset = row * stride;
90+
var sourceRow = row / options.ModuleHeight;
91+
for (var col = 0; col < width; col++)
92+
{
93+
var sourceCol = (col / options.ModuleWidth) - options.QuietZoneModules;
94+
var isDark = sourceRow >= 0
95+
&& sourceRow < barcode.Height
96+
&& sourceCol >= 0
97+
&& sourceCol < barcode.Width
98+
&& barcode[sourceRow, sourceCol];
99+
100+
var color = isDark ? options.DarkColor : options.LightColor;
101+
var pixelOffset = rowOffset + 1 + (col * 4);
102+
result[pixelOffset] = color.Red;
103+
result[pixelOffset + 1] = color.Green;
104+
result[pixelOffset + 2] = color.Blue;
105+
result[pixelOffset + 3] = color.Alpha;
106+
}
107+
}
108+
109+
return result;
110+
}
111+
112+
private static byte[] CompressImageData(ReadOnlySpan<byte> imageData)
113+
{
114+
using var output = new MemoryStream();
115+
using (var compressionStream = new ZLibStream(output, CompressionLevel.SmallestSize, leaveOpen: true))
116+
{
117+
compressionStream.Write(imageData);
118+
}
119+
120+
return output.ToArray();
121+
}
122+
123+
private static void WritePng(Stream stream, int width, int height, ReadOnlySpan<byte> compressedImageData)
124+
{
125+
stream.Write(PngSignature);
126+
127+
Span<byte> ihdrData = stackalloc byte[13];
128+
BinaryPrimitives.WriteUInt32BigEndian(ihdrData, (uint)width);
129+
BinaryPrimitives.WriteUInt32BigEndian(ihdrData[4..], (uint)height);
130+
ihdrData[8] = 8;
131+
ihdrData[9] = 6;
132+
ihdrData[10] = 0;
133+
ihdrData[11] = 0;
134+
ihdrData[12] = 0;
135+
136+
WriteChunk(stream, IhdrChunkType, ihdrData);
137+
WriteChunk(stream, IdatChunkType, compressedImageData);
138+
WriteChunk(stream, IendChunkType, ReadOnlySpan<byte>.Empty);
139+
}
140+
141+
private static void WriteChunk(Stream stream, ReadOnlySpan<byte> chunkType, ReadOnlySpan<byte> data)
142+
{
143+
Span<byte> uintBuffer = stackalloc byte[4];
144+
BinaryPrimitives.WriteUInt32BigEndian(uintBuffer, (uint)data.Length);
145+
stream.Write(uintBuffer);
146+
stream.Write(chunkType);
147+
stream.Write(data);
148+
149+
var crc = ComputeCrc32(chunkType, data);
150+
BinaryPrimitives.WriteUInt32BigEndian(uintBuffer, crc);
151+
stream.Write(uintBuffer);
152+
}
153+
154+
private static uint ComputeCrc32(ReadOnlySpan<byte> chunkType, ReadOnlySpan<byte> data)
155+
{
156+
var crc = uint.MaxValue;
157+
crc = UpdateCrc32(crc, chunkType);
158+
crc = UpdateCrc32(crc, data);
159+
160+
return ~crc;
161+
}
162+
163+
private static uint UpdateCrc32(uint crc, ReadOnlySpan<byte> data)
164+
{
165+
foreach (var value in data)
166+
{
167+
crc = Crc32Table[(int)((crc ^ value) & 0xFF)] ^ (crc >> 8);
168+
}
169+
170+
return crc;
171+
}
172+
173+
private static uint[] InitializeCrc32Table()
174+
{
175+
var table = new uint[256];
176+
for (uint index = 0; index < table.Length; index++)
177+
{
178+
var crc = index;
179+
for (var bit = 0; bit < 8; bit++)
180+
{
181+
crc = (crc & 1) == 0 ? (crc >> 1) : (0xEDB88320u ^ (crc >> 1));
182+
}
183+
184+
table[index] = crc;
185+
}
186+
187+
return table;
188+
}
189+
}

0 commit comments

Comments
 (0)