Skip to content

Commit 7074460

Browse files
committed
Support for PNG generation
1 parent 7c134d9 commit 7074460

File tree

7 files changed

+347
-1
lines changed

7 files changed

+347
-1
lines changed

QrCodeGenerator/PngBuilder.cs

Lines changed: 259 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,259 @@
1+
/*
2+
* QR code generator library (.NET)
3+
*
4+
* Copyright (c) Manuel Bleichenbacher (MIT License)
5+
* https://github.com/manuelbl/QrCodeGenerator
6+
*
7+
*/
8+
9+
using System;
10+
using System.IO;
11+
using System.IO.Compression;
12+
13+
namespace Net.Codecrete.QrCodeGenerator
14+
{
15+
/// <summary>
16+
/// Creates a PNG file from a given QR code.
17+
/// </summary>
18+
internal sealed class PngBuilder
19+
{
20+
private static readonly byte[] Signature = { 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A };
21+
private static readonly uint[] CrcTable =
22+
{
23+
0x00000000, 0x77073096, 0xEE0E612C, 0x990951BA, 0x076DC419, 0x706AF48F, 0xE963A535, 0x9E6495A3, 0x0EDB8832, 0x79DCB8A4, 0xE0D5E91E, 0x97D2D988, 0x09B64C2B, 0x7EB17CBD, 0xE7B82D07, 0x90BF1D91, 0x1DB71064, 0x6AB020F2, 0xF3B97148, 0x84BE41DE, 0x1ADAD47D, 0x6DDDE4EB, 0xF4D4B551, 0x83D385C7, 0x136C9856, 0x646BA8C0, 0xFD62F97A, 0x8A65C9EC, 0x14015C4F, 0x63066CD9, 0xFA0F3D63, 0x8D080DF5, 0x3B6E20C8, 0x4C69105E, 0xD56041E4, 0xA2677172, 0x3C03E4D1, 0x4B04D447, 0xD20D85FD, 0xA50AB56B, 0x35B5A8FA, 0x42B2986C, 0xDBBBC9D6, 0xACBCF940, 0x32D86CE3, 0x45DF5C75, 0xDCD60DCF, 0xABD13D59, 0x26D930AC, 0x51DE003A, 0xC8D75180, 0xBFD06116, 0x21B4F4B5, 0x56B3C423, 0xCFBA9599, 0xB8BDA50F, 0x2802B89E, 0x5F058808, 0xC60CD9B2, 0xB10BE924, 0x2F6F7C87, 0x58684C11, 0xC1611DAB, 0xB6662D3D,
24+
0x76DC4190, 0x01DB7106, 0x98D220BC, 0xEFD5102A, 0x71B18589, 0x06B6B51F, 0x9FBFE4A5, 0xE8B8D433, 0x7807C9A2, 0x0F00F934, 0x9609A88E, 0xE10E9818, 0x7F6A0DBB, 0x086D3D2D, 0x91646C97, 0xE6635C01, 0x6B6B51F4, 0x1C6C6162, 0x856530D8, 0xF262004E, 0x6C0695ED, 0x1B01A57B, 0x8208F4C1, 0xF50FC457, 0x65B0D9C6, 0x12B7E950, 0x8BBEB8EA, 0xFCB9887C, 0x62DD1DDF, 0x15DA2D49, 0x8CD37CF3, 0xFBD44C65, 0x4DB26158, 0x3AB551CE, 0xA3BC0074, 0xD4BB30E2, 0x4ADFA541, 0x3DD895D7, 0xA4D1C46D, 0xD3D6F4FB, 0x4369E96A, 0x346ED9FC, 0xAD678846, 0xDA60B8D0, 0x44042D73, 0x33031DE5, 0xAA0A4C5F, 0xDD0D7CC9, 0x5005713C, 0x270241AA, 0xBE0B1010, 0xC90C2086, 0x5768B525, 0x206F85B3, 0xB966D409, 0xCE61E49F, 0x5EDEF90E, 0x29D9C998, 0xB0D09822, 0xC7D7A8B4, 0x59B33D17, 0x2EB40D81, 0xB7BD5C3B, 0xC0BA6CAD,
25+
0xEDB88320, 0x9ABFB3B6, 0x03B6E20C, 0x74B1D29A, 0xEAD54739, 0x9DD277AF, 0x04DB2615, 0x73DC1683, 0xE3630B12, 0x94643B84, 0x0D6D6A3E, 0x7A6A5AA8, 0xE40ECF0B, 0x9309FF9D, 0x0A00AE27, 0x7D079EB1, 0xF00F9344, 0x8708A3D2, 0x1E01F268, 0x6906C2FE, 0xF762575D, 0x806567CB, 0x196C3671, 0x6E6B06E7, 0xFED41B76, 0x89D32BE0, 0x10DA7A5A, 0x67DD4ACC, 0xF9B9DF6F, 0x8EBEEFF9, 0x17B7BE43, 0x60B08ED5, 0xD6D6A3E8, 0xA1D1937E, 0x38D8C2C4, 0x4FDFF252, 0xD1BB67F1, 0xA6BC5767, 0x3FB506DD, 0x48B2364B, 0xD80D2BDA, 0xAF0A1B4C, 0x36034AF6, 0x41047A60, 0xDF60EFC3, 0xA867DF55, 0x316E8EEF, 0x4669BE79, 0xCB61B38C, 0xBC66831A, 0x256FD2A0, 0x5268E236, 0xCC0C7795, 0xBB0B4703, 0x220216B9, 0x5505262F, 0xC5BA3BBE, 0xB2BD0B28, 0x2BB45A92, 0x5CB36A04, 0xC2D7FFA7, 0xB5D0CF31, 0x2CD99E8B, 0x5BDEAE1D,
26+
0x9B64C2B0, 0xEC63F226, 0x756AA39C, 0x026D930A, 0x9C0906A9, 0xEB0E363F, 0x72076785, 0x05005713, 0x95BF4A82, 0xE2B87A14, 0x7BB12BAE, 0x0CB61B38, 0x92D28E9B, 0xE5D5BE0D, 0x7CDCEFB7, 0x0BDBDF21, 0x86D3D2D4, 0xF1D4E242, 0x68DDB3F8, 0x1FDA836E, 0x81BE16CD, 0xF6B9265B, 0x6FB077E1, 0x18B74777, 0x88085AE6, 0xFF0F6A70, 0x66063BCA, 0x11010B5C, 0x8F659EFF, 0xF862AE69, 0x616BFFD3, 0x166CCF45, 0xA00AE278, 0xD70DD2EE, 0x4E048354, 0x3903B3C2, 0xA7672661, 0xD06016F7, 0x4969474D, 0x3E6E77DB, 0xAED16A4A, 0xD9D65ADC, 0x40DF0B66, 0x37D83BF0, 0xA9BCAE53, 0xDEBB9EC5, 0x47B2CF7F, 0x30B5FFE9, 0xBDBDF21C, 0xCABAC28A, 0x53B39330, 0x24B4A3A6, 0xBAD03605, 0xCDD70693, 0x54DE5729, 0x23D967BF, 0xB3667A2E, 0xC4614AB8, 0x5D681B02, 0x2A6F2B94, 0xB40BBE37, 0xC30C8EA1, 0x5A05DF1B, 0x2D02EF8D
27+
};
28+
29+
private static readonly byte[] IHDR = { 73, 72, 68, 82 };
30+
private static readonly byte[] PLTE = { 80, 76, 84, 69 };
31+
private static readonly byte[] IDAT = { 73, 68, 65, 84 };
32+
private static readonly byte[] IEND = { 73, 69, 78, 68 };
33+
34+
/// <summary>
35+
/// Creates a PNG image for the given QR code.
36+
/// <para>
37+
/// The PNG image uses indexed colors, 1 bit per pixel and a palette with entries for the foreground and background colors.
38+
/// </para>
39+
/// </summary>
40+
/// <param name="qrCode">The QR code.</param>
41+
/// <param name="border">The border width, as a factor of the module (QR code pixel) size.</param>
42+
/// <param name="scale">The width and height, in pixels, of each module.</param>
43+
/// <param name="foreground">The foreground color (dark modules), in RGB value (little endian).</param>
44+
/// <param name="background">The background color (light modules), in RGB value (little endian).</param>
45+
/// <returns>A PNG image, as a byte array.</returns>
46+
///
47+
internal static byte[] ToImage(QrCode qrCode, int scale, int border, int foreground, int background)
48+
{
49+
var imageSize = (qrCode.Size + border * 2) * scale;
50+
var builder = new PngBuilder();
51+
builder.WriteHeader(imageSize, imageSize, 1, 3);
52+
builder.WritePalette(new int[] { background, foreground });
53+
builder.WriteData(CreateBitmap(qrCode, border, scale));
54+
builder.WriteEnd();
55+
return builder.GetBytes();
56+
}
57+
58+
/// <summary>
59+
/// Creates an uncompressed 1-bit per pixel bitmap of the QR code.
60+
/// </summary>
61+
/// <param name="qrCode">The QR code</param>
62+
/// <param name="border">The border</param>
63+
/// <param name="scale">The scale</param>
64+
/// <returns>Bitmap, as a byte array</returns>
65+
private static byte[] CreateBitmap(QrCode qrCode, int border, int scale)
66+
{
67+
var size = qrCode.Size;
68+
var imageSize = (size + border * 2) * scale;
69+
70+
var bytesPerLine = (imageSize + 7) / 8 + 1; // additional byte at the start for filter type
71+
var data = new byte[bytesPerLine * imageSize];
72+
73+
for (var y = 0; y < size; y++)
74+
{
75+
var offset = (border + y) * scale * bytesPerLine;
76+
77+
for (var x = 0; x < size; x++)
78+
{
79+
if (!qrCode.GetModule(x, y))
80+
continue;
81+
82+
var pos = (border + x) * scale;
83+
var end = pos + scale;
84+
85+
// set pixels for module ('scale' times)
86+
for (; pos < end; pos++)
87+
{
88+
var index = offset + pos / 8 + 1;
89+
data[index] |= (byte)(0x80U >> (pos % 8));
90+
}
91+
}
92+
93+
// replicate line 'scale' times
94+
for (var i = 1; i < scale; i++)
95+
Array.Copy(data, offset, data, offset + i * bytesPerLine, bytesPerLine);
96+
}
97+
98+
return data;
99+
}
100+
101+
private readonly MemoryStream stream = new MemoryStream();
102+
103+
/// <summary>
104+
/// Returns the resulting PNG bytes.
105+
/// </summary>
106+
/// <returns>PNG file, as a byte array.</returns>
107+
private byte[] GetBytes()
108+
{
109+
var bytes = stream.ToArray();
110+
SetCRC(bytes);
111+
return bytes;
112+
}
113+
114+
/// <summary>
115+
/// Writes the PNG header (IHDR chunk).
116+
/// </summary>
117+
/// <param name="width">The image width.</param>
118+
/// <param name="height">The image height.</param>
119+
/// <param name="bitDepth">The bits per pixel.</param>
120+
/// <param name="colorType">The color type (see PNG specification).</param>
121+
private void WriteHeader(int width, int height, byte bitDepth, byte colorType)
122+
{
123+
stream.Write(Signature, 0, Signature.Length);
124+
125+
WriteChunkStart(IHDR, 13);
126+
WriteIntBigEndian((uint)width);
127+
WriteIntBigEndian((uint)height);
128+
129+
stream.WriteByte(bitDepth);
130+
stream.WriteByte(colorType);
131+
stream.WriteByte(0);
132+
stream.WriteByte(0);
133+
stream.WriteByte(0);
134+
135+
WriteChunkEnd();
136+
}
137+
138+
/// <summary>
139+
/// Writes the palette (PLTE chunk).
140+
/// </summary>
141+
/// <param name="palette">The color palettes as an array of RGB values.</param>
142+
private void WritePalette(int[] palette)
143+
{
144+
WriteChunkStart(PLTE, palette.Length * 3);
145+
foreach (var color in palette)
146+
{
147+
stream.WriteByte((byte)((color >> 16) & 0xFF));
148+
stream.WriteByte((byte)((color >> 8) & 0xFF));
149+
stream.WriteByte((byte)(color & 0xFF));
150+
}
151+
WriteChunkEnd();
152+
}
153+
154+
/// <summary>
155+
/// Writes the pixel data (IDAT chunk).
156+
/// </summary>
157+
/// <param name="data">The pixel data.</param>
158+
private void WriteData(byte[] data)
159+
{
160+
var compressedData = Deflate(data);
161+
WriteChunkStart(IDAT, compressedData.Length + 6);
162+
163+
stream.WriteByte(0x78);
164+
stream.WriteByte(0x9C);
165+
166+
stream.Write(compressedData, 0, compressedData.Length);
167+
168+
var adler = CalcAdler32(data, 0, data.Length);
169+
WriteIntBigEndian(adler);
170+
WriteChunkEnd();
171+
}
172+
173+
/// <summary>
174+
/// Writes the end chunk (IEND).
175+
/// </summary>
176+
private void WriteEnd()
177+
{
178+
WriteChunkStart(IEND, 0);
179+
WriteChunkEnd();
180+
}
181+
182+
private static void SetCRC(byte[] bytes)
183+
{
184+
var chunkOffset = Signature.Length;
185+
while (chunkOffset < bytes.Length)
186+
{
187+
// calculate CRC
188+
var dataLength = (bytes[chunkOffset] << 24) | (bytes[chunkOffset + 1] << 16) | (bytes[chunkOffset + 2] << 8) | bytes[chunkOffset + 3];
189+
var crc = CalcCrc32(bytes, chunkOffset + 4, dataLength + 4);
190+
var crcOffset = chunkOffset + 8 + dataLength;
191+
192+
// set CRC
193+
bytes[crcOffset + 0] = (byte)(crc >> 24);
194+
bytes[crcOffset + 1] = (byte)(crc >> 16);
195+
bytes[crcOffset + 2] = (byte)(crc >> 8);
196+
bytes[crcOffset + 3] = (byte)crc;
197+
198+
chunkOffset = crcOffset + 4;
199+
}
200+
}
201+
202+
private void WriteChunkStart(byte[] type, int length)
203+
{
204+
WriteIntBigEndian((uint)length);
205+
stream.Write(type, 0, 4);
206+
}
207+
208+
private void WriteChunkEnd()
209+
{
210+
stream.SetLength(stream.Length + 4);
211+
stream.Position += 4;
212+
}
213+
214+
private void WriteIntBigEndian(uint value)
215+
{
216+
stream.WriteByte((byte)(value >> 24));
217+
stream.WriteByte((byte)(value >> 16));
218+
stream.WriteByte((byte)(value >> 8));
219+
stream.WriteByte((byte)value);
220+
}
221+
222+
private static byte[] Deflate(byte[] data)
223+
{
224+
var output = new MemoryStream();
225+
using (var deflater = new DeflateStream(output, CompressionLevel.Optimal))
226+
{
227+
deflater.Write(data, 0, data.Length);
228+
}
229+
return output.ToArray();
230+
}
231+
232+
private static uint CalcAdler32(byte[] data, int index, int length)
233+
{
234+
const uint Base = 65521;
235+
uint s1 = 1;
236+
uint s2 = 0;
237+
238+
var end = index + length;
239+
for (var n = index; n < end; n++)
240+
{
241+
s1 = (s1 + data[n]) % Base;
242+
s2 = (s2 + s1) % Base;
243+
}
244+
245+
return (s2 << 16) + s1;
246+
}
247+
248+
private static uint CalcCrc32(byte[] data, int index, int length)
249+
{
250+
var c = 0xffffffff;
251+
252+
var end = index + length;
253+
for (var n = index; n < end; n++)
254+
c = CrcTable[(c ^ data[n]) & 0xff] ^ (c >> 8);
255+
256+
return c ^ 0xffffffff;
257+
}
258+
}
259+
}

QrCodeGenerator/QrCode.cs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -482,6 +482,30 @@ public byte[] ToBmpBitmap(int border = 0, int scale = 1)
482482
return ToBmpBitmap(border, scale, 0x000000, 0xffffff);
483483
}
484484

485+
/// <summary>
486+
/// Creates bitmap in the PNG format data using specified foreground and background colors for dark and light modules.
487+
/// </summary>
488+
/// <param name="border">The border width, as a factor of the module (QR code pixel) size.</param>
489+
/// <param name="scale">The width and height, in pixels, of each module.</param>
490+
/// <param name="foreground">The foreground color (dark modules), in RGB value (little endian).</param>
491+
/// <param name="background">The background color (light modules), in RGB value (little endian).</param>
492+
/// <returns>Bitmap data</returns>
493+
public byte[] ToPngBitmap(int border, int scale, int foreground, int background)
494+
{
495+
return PngBuilder.ToImage(this, scale, border, foreground, background);
496+
}
497+
498+
/// <summary>
499+
/// Creates bitmap in the PNG format data using black for dark modules and white for light modules.
500+
/// </summary>
501+
/// <param name="border">The border width, as a factor of the module (QR code pixel) size.</param>
502+
/// <param name="scale">The width and height, in pixels, of each module.</param>
503+
/// <returns>Bitmap data</returns>
504+
public byte[] ToPngBitmap(int border = 0, int scale = 1)
505+
{
506+
return PngBuilder.ToImage(this, scale, border, 0x000000, 0xffffff);
507+
}
508+
485509
/// <summary>
486510
/// Creates an RGB color value in little endian format.
487511
/// </summary>

QrCodeGeneratorTest/PngTest.cs

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
/*
2+
* QR code generator library (.NET)
3+
*
4+
* Copyright (c) Manuel Bleichenbacher (MIT License)
5+
* https://github.com/manuelbl/QrCodeGenerator
6+
*
7+
*/
8+
9+
using System.IO;
10+
using System.Threading.Tasks;
11+
using VerifyTests;
12+
using VerifyXunit;
13+
using Xunit;
14+
using static Net.Codecrete.QrCodeGenerator.QrCode;
15+
16+
namespace Net.Codecrete.QrCodeGenerator.Test
17+
{
18+
public class PngTest
19+
{
20+
static PngTest()
21+
{
22+
VerifyImageMagick.Initialize();
23+
VerifyImageMagick.RegisterComparers(threshold: 0.005);
24+
}
25+
26+
private const string CodeText1 = "At vero eos et accusamus et iusto odio dignissimos ducimus qui blanditiis praesentium voluptatum deleniti atque corrupti quos dolores et quas molestias excepturi sint occaecati cupiditate non provident, similique sunt in culpa qui officia deserunt mollitia animi, id est laborum et dolorum fuga.";
27+
private const string CodeText2 = "The quick brown fox";
28+
29+
protected readonly VerifySettings Settings = new VerifySettings();
30+
31+
public PngTest()
32+
{
33+
Settings.UseDirectory("ReferenceFiles");
34+
}
35+
36+
[Fact]
37+
public Task PngImage()
38+
{
39+
var qrCode = EncodeText(CodeText1, Ecc.Medium);
40+
var pngData = qrCode.ToPngBitmap(5, 3);
41+
42+
using (var stream = new MemoryStream(pngData))
43+
{
44+
return Verifier.Verify(stream, "png", Settings);
45+
}
46+
}
47+
48+
49+
[Fact]
50+
public Task ColorPngImage()
51+
{
52+
var qrCode = EncodeText(CodeText2, Ecc.Medium);
53+
var pngData = qrCode.ToPngBitmap(3, 7, 0x171c80, 0xccbc9b);
54+
55+
using (var stream = new MemoryStream(pngData))
56+
{
57+
return Verifier.Verify(stream, "png", Settings);
58+
}
59+
}
60+
61+
}
62+
}

QrCodeGeneratorTest/QrCodeGeneratorTest.csproj

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818

1919
<ItemGroup>
2020
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.13.0" />
21+
<PackageReference Include="Verify.ImageMagick" Version="3.8.1" />
22+
<PackageReference Include="Verify.Xunit" Version="31.9.2" />
2123
<PackageReference Include="xunit" Version="2.9.3" />
2224
<PackageReference Include="xunit.runner.visualstudio" Version="3.0.2" PrivateAssets="all" />
2325
</ItemGroup>
378 Bytes
Loading
1.36 KB
Loading

QrCodeGeneratorTest/SvgTest.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@
2525
* IN THE SOFTWARE.
2626
*/
2727

28-
using System;
2928
using System.Globalization;
3029
using System.IO;
3130
using System.Text;

0 commit comments

Comments
 (0)