Files
radzen-blazor/Radzen.Blazor/RadzenQREncoder.cs
Atanas Korchev 41e0f9944a Enforce csharp_prefer_braces at error severity
Adds braces to all single-statement if/else/for/foreach/while bodies in
Radzen.Blazor. Flipping the rule to error in .editorconfig and enabling
EnforceCodeStyleInBuild prevents future regressions.

Fixes generated mechanically via `dotnet format style --diagnostics IDE0011`.
The accompanying using-directive sort was reverted to keep this PR
braces-only.
2026-06-15 18:45:13 +03:00

1429 lines
45 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using Microsoft.AspNetCore.Components;
using System;
using System.Collections.Generic;
using System.Globalization;
using Radzen.Blazor.Rendering;
using System.Text;
namespace Radzen.Blazor;
/// <summary>
/// Error correction.
/// </summary>
public enum RadzenQREcc
{
/// <summary>
/// 7% recovery.
/// </summary>
Low,
/// <summary>
/// 15% recovery.
/// </summary>
Medium,
/// <summary>
/// 25% recovery.
/// </summary>
Quartile,
/// <summary>
/// 30% recovery.
/// </summary>
High
}
/// <summary>
/// Provides QR encoding utilities for UTF-8 strings and raw bytes.
/// </summary>
public static class RadzenQREncoder
{
/// <summary>Encode a UTF-8 string into a QR module matrix.</summary>
public static bool[,] EncodeUtf8(string value, RadzenQREcc ecc, int minVersion = 1, int maxVersion = 40)
{
var data = Encoding.UTF8.GetBytes(value ?? string.Empty);
for (int ver = minVersion; ver <= maxVersion; ver++)
{
// --- Compute capacity from EC params
var (dataCwCount, ecPerBlock, g1, g1dcw, g2, g2dcw) = EcParams(ver, ecc);
int capacityBits = dataCwCount * 8;
// --- Build header (BYTE mode) exactly as per spec
int charCountBits = ver <= 9 ? 8 : 16;
var bb = new BitBuffer();
bb.AppendBits(0b0100, 4); // mode: BYTE
bb.AppendBits(data.Length, charCountBits); // char count
foreach (var b in data)
{
bb.AppendBits(b, 8); // payload
}
// --- Fit math (this is the authoritative acceptance test)
int baseBits = bb.Length; // header+data bits
if (baseBits > capacityBits) // early reject
{
continue;
}
int needed = baseBits + Math.Min(4, capacityBits - baseBits); // add up to 4-bit terminator
needed += (8 - (needed % 8)) % 8; // pad to byte boundary
if (needed > capacityBits)
{
continue; // try next version
}
// --- Build data codewords (pads 0xEC/0x11 as needed)
var dataCwBytes = BuildDataCodewords(bb, dataCwCount);
// --- Split into blocks, make EC, interleave
var blocks = BuildBlocks(dataCwBytes, ecPerBlock, g1, g1dcw, g2, g2dcw);
var final = Interleave(blocks);
// --- Base matrix + place + mask + format (+version if v>=7)
var (m, reserved) = BuildBaseMatrix(ver);
PlaceData(m, reserved, final);
int bestMask = ChooseBestMask(m, reserved); // or force 0 while testing
ApplyMask(m, reserved, bestMask); // mask only NON-reserved
WriteFormatInfo(m, reserved, ecc, bestMask); // write both copies
if (ver >= 7)
{
WriteVersionInfo(m, reserved, ver);
}
//var sb = new StringBuilder();
//sb.AppendLine("DATA: " + BitConverter.ToString(dataCwBytes));
//sb.AppendLine("EC : " + BitConverter.ToString(blocks[0].Ec));
//int ecl = 0; // M=00
//int fmt = BchEncode((ecl << 3) | 0 /*mask 0*/, 0x537, 15, 5) ^ 0x5412;
//sb.AppendLine("FMT : " + Convert.ToString(fmt, 2).PadLeft(15, '0'));
return m;
}
// If we get here, nothing fit. Throw with diagnostics so you see why.
throw new ArgumentException($"Data too long for versions {minVersion}..{maxVersion} at ECC={ecc}.");
}
/// <summary>Encode raw bytes into a QR module matrix.</summary>
public static bool[,] EncodeBytes(byte[] data, RadzenQREcc ecc = RadzenQREcc.Medium, int minVersion = 1, int maxVersion = 40)
{
ArgumentNullException.ThrowIfNull(data);
if (minVersion < 1 || maxVersion > 40 || minVersion > maxVersion)
{
throw new ArgumentOutOfRangeException(nameof(minVersion), "Version range must be within 1..40");
}
// Try versions until payload (with headers) fits into available data bits
for (int ver = minVersion; ver <= maxVersion; ver++)
{
int charCountBits = (ver <= 9) ? 8 : 16; // byte mode: v1-9:8, v10-40:16
var bb = new BitBuffer();
// Mode = Byte (0100)
bb.AppendBits(0b0100, 4);
bb.AppendBits(data.Length, charCountBits);
foreach (byte b in data)
{
bb.AppendBits(b, 8);
}
var (dataCw, ecPerBlock, grp1Blocks, grp1DataCw, grp2Blocks, grp2DataCw) = EcParams(ver, ecc);
int capacityBits = dataCw * 8;
// Terminator up to 4 bits
int needed = bb.Length + Math.Min(4, Math.Max(0, capacityBits - bb.Length));
// Pad to byte
needed += (8 - (needed % 8)) % 8;
if (needed <= capacityBits)
{
// Build final data codewords (with pad bytes 0xEC, 0x11)
var dataCwBytes = BuildDataCodewords(bb, dataCw);
// Split into blocks and generate EC codewords
var blocks = BuildBlocks(dataCwBytes, ecPerBlock, grp1Blocks, grp1DataCw, grp2Blocks, grp2DataCw);
// Interleave data & EC codewords
var final = Interleave(blocks);
// Build base matrix with patterns
var (m, reserved) = BuildBaseMatrix(ver);
// Place data bits (zig-zag)
PlaceData(m, reserved, final);
// Choose best mask and apply
int bestMask = ChooseBestMask(m, reserved);
ApplyMask(m, reserved, bestMask);
WriteFormatInfo(m, reserved, ecc, bestMask);
ApplyMask(m, reserved, bestMask);
// Write format info (depends on ECC + mask)
WriteFormatInfo(m, reserved, ecc, bestMask);
// Write version info for v7+
if (ver >= 7)
{
WriteVersionInfo(m, reserved, ver);
}
return m;
}
}
throw new ArgumentException("Data too long for the given version range and ECC.");
}
/// <summary>Render a module matrix into an SVG string with a 4-module quiet zone.</summary>
public static string ToSvg(
bool[,] modules,
int moduleSize = 8,
string foreground = "#000000",
string background = "#FFFFFF",
QRCodeModuleShape moduleShape = QRCodeModuleShape.Square,
QRCodeEyeShape eyeShape = QRCodeEyeShape.Square,
QRCodeEyeShape? eyeShapeTopLeft = null,
QRCodeEyeShape? eyeShapeTopRight = null,
QRCodeEyeShape? eyeShapeBottomLeft = null,
string? eyeColor = null,
string? eyeColorTopLeft = null,
string? eyeColorTopRight = null,
string? eyeColorBottomLeft = null,
string? image = null,
string imageBackground = "#FFF")
{
ArgumentNullException.ThrowIfNull(modules);
ArgumentNullException.ThrowIfNull(foreground);
ArgumentNullException.ThrowIfNull(background);
ArgumentNullException.ThrowIfNull(imageBackground);
int n = modules.GetLength(0);
int vb = n + 8; // 4 modules of quiet zone on each side
int px = vb * moduleSize;
var sb = new StringBuilder(n * n + 1024);
sb.Append(CultureInfo.InvariantCulture, $"<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"{px}\" height=\"{px}\" viewBox=\"0 0 {vb} {vb}\" shape-rendering=\"crispEdges\">");
var backgroundFill = GetSvgFillParts(background);
sb.Append(CultureInfo.InvariantCulture, $"<rect x=\"0\" y=\"0\" width=\"{vb}\" height=\"{vb}\" fill=\"{backgroundFill.Color}\" fill-opacity=\"{Format(backgroundFill.Opacity)}\"/>");
var baseEyeColor = eyeColor ?? foreground;
AppendEye(sb, 4, 4, eyeShapeTopLeft ?? eyeShape, eyeColorTopLeft ?? baseEyeColor, "eye-mask-0");
AppendEye(sb, vb - 11, 4, eyeShapeTopRight ?? eyeShape, eyeColorTopRight ?? baseEyeColor, "eye-mask-1");
AppendEye(sb, 4, vb - 11, eyeShapeBottomLeft ?? eyeShape, eyeColorBottomLeft ?? baseEyeColor, "eye-mask-2");
for (int r = 0; r < n; r++)
{
for (int c = 0; c < n; c++)
{
if (!modules[r, c])
{
continue;
}
if (IsFinderCell(r, c, n))
{
continue;
}
var x = c + 4;
var y = r + 4;
if (moduleShape == QRCodeModuleShape.Circle)
{
sb.Append(CultureInfo.InvariantCulture, $"<circle cx=\"{Format(x + 0.5)}\" cy=\"{Format(y + 0.5)}\" r=\"0.5\" fill=\"{foreground}\"/>");
}
else if (moduleShape == QRCodeModuleShape.Rounded)
{
sb.Append(CultureInfo.InvariantCulture, $"<rect x=\"{Format(x)}\" y=\"{Format(y)}\" width=\"1\" height=\"1\" rx=\"0.25\" ry=\"0.25\" fill=\"{foreground}\"/>");
}
else
{
sb.Append(CultureInfo.InvariantCulture, $"<rect x=\"{Format(x)}\" y=\"{Format(y)}\" width=\"1\" height=\"1\" fill=\"{foreground}\"/>");
}
}
}
if (!string.IsNullOrWhiteSpace(image))
{
const double imageSizePercent = 20;
const double imagePaddingModules = 1.0;
const double imageCornerRadius = 0.75;
const double imageBackgroundOpacity = 1.0;
double pct = Math.Clamp(imageSizePercent, 5, 60);
double boxModules = Math.Max(5, Math.Round(n * (pct / 100.0)));
double pad = Math.Max(0, imagePaddingModules);
double cutoutW = boxModules + 2 * pad;
double cutoutH = boxModules + 2 * pad;
double centerX = vb / 2.0;
double centerY = vb / 2.0;
double cutoutX = centerX - cutoutW / 2.0;
double cutoutY = centerY - cutoutH / 2.0;
double imgX = centerX - boxModules / 2.0;
double imgY = centerY - boxModules / 2.0;
sb.Append(CultureInfo.InvariantCulture, $"<rect x=\"{Format(cutoutX)}\" y=\"{Format(cutoutY)}\" width=\"{Format(cutoutW)}\" height=\"{Format(cutoutH)}\" rx=\"{Format(imageCornerRadius)}\" ry=\"{Format(imageCornerRadius)}\" fill=\"{imageBackground}\" fill-opacity=\"{Format(Math.Clamp(imageBackgroundOpacity, 0, 1))}\"/>");
sb.Append(CultureInfo.InvariantCulture, $"<image x=\"{Format(imgX)}\" y=\"{Format(imgY)}\" width=\"{Format(boxModules)}\" height=\"{Format(boxModules)}\" preserveAspectRatio=\"xMidYMid meet\" href=\"{image}\"/>");
}
sb.Append("</svg>");
return sb.ToString();
}
// ---------- Core building ----------
private static (bool[,] m, bool[,] res) BuildBaseMatrix(int ver)
{
int n = 21 + 4 * (ver - 1);
var m = new bool[n, n];
var reserved = new bool[n, n];
// Finder patterns + separators + timing
DrawFinder(m, reserved, 0, 0);
DrawFinder(m, reserved, n - 7, 0);
DrawFinder(m, reserved, 0, n - 7);
// Timing
for (int i = 8; i < n - 8; i++)
{
m[6, i] = (i % 2) == 0; reserved[6, i] = true;
m[i, 6] = (i % 2) == 0; reserved[i, 6] = true;
}
// Alignment patterns
var ap = AlignmentPatternPositions(ver);
foreach (int y in ap)
{
foreach (int x in ap)
{
// Skip the corners with finders
bool corner = (x < 9 && y < 9) || (x > n - 9 && y < 9) || (x < 9 && y > n - 9);
if (!corner)
{
DrawAlignment(m, reserved, x, y);
}
}
}
// Dark module (always)
m[4 * ver + 9, 8] = true; reserved[4 * ver + 9, 8] = true;
// Reserve format info areas
ReserveFormat(reserved);
// Reserve version info (v7+)
if (ver >= 7)
{
ReserveVersion(reserved);
}
return (m, reserved);
}
// Finder with 1-module white separator around (as reserved)
private static void DrawFinder(bool[,] m, bool[,] res, int x, int y)
{
for (int r = -1; r <= 7; r++)
{
for (int c = -1; c <= 7; c++)
{
int rr = y + r, cc = x + c;
if (rr < 0 || cc < 0 || rr >= m.GetLength(0) || cc >= m.GetLength(1))
{
continue;
}
bool in7 = r >= 0 && r < 7 && c >= 0 && c < 7;
if (in7)
{
bool on = r == 0 || r == 6 || c == 0 || c == 6 || (r >= 2 && r <= 4 && c >= 2 && c <= 4);
m[rr, cc] = on;
res[rr, cc] = true;
}
else
{
// separator (light), just reserve
res[rr, cc] = true;
}
}
}
}
private static void DrawAlignment(bool[,] m, bool[,] res, int cx, int cy)
{
for (int r = -2; r <= 2; r++)
{
for (int c = -2; c <= 2; c++)
{
int rr = cy + r, cc = cx + c;
m[rr, cc] = Math.Max(Math.Abs(r), Math.Abs(c)) != 1; // dark outer, white ring, dark center
res[rr, cc] = true;
}
}
}
private static void ReserveFormat(bool[,] res)
{
int n = res.GetLength(0);
// Row 8, left of the timing cross (cols 0..5)
for (int c = 0; c <= 5; c++)
{
res[8, c] = true;
}
// Row 8, skip col 6 (timing), then 7 and 8 are format
res[8, 7] = true;
res[8, 8] = true;
// Column 8, above the timing cross (rows 0..5)
for (int r = 0; r <= 5; r++)
{
res[r, 8] = true;
}
// Column 8, row 7 is format (row 6 is timing, skip it)
res[7, 8] = true;
// Second copy:
// Column 8, bottom 7 cells (rows n-1 down to n-7) ← exactly 7 cells
for (int i = 0; i < 7; i++)
{
res[n - 1 - i, 8] = true;
}
// Row 8, right side 8 cells (cols n-8 .. n-1) ← exactly 8 cells
for (int i = 0; i < 8; i++)
{
res[8, n - 8 + i] = true;
}
}
private static void ReserveVersion(bool[,] res)
{
int n = res.GetLength(0);
// Bottom-left version info block: 3 rows x 6 columns
// Rows: n-11, n-10, n-9
// Cols: 0..5
for (int r = n - 11; r <= n - 9; r++)
{
for (int c = 0; c <= 5; c++)
{
res[r, c] = true;
}
}
// Top-right version info block: 6 rows x 3 columns
// Rows: 0..5
// Cols: n-11, n-10, n-9
for (int r = 0; r <= 5; r++)
{
for (int c = n - 11; c <= n - 9; c++)
{
res[r, c] = true;
}
}
}
private static void WriteVersionInfo(bool[,] m, bool[,] res, int ver)
{
// BCH(18,6) with generator 0x1F25
int bits = BchEncode(ver, 0x1F25, 18, 6);
int n = m.GetLength(0);
// ---- Bottom-left block (3 tall x 6 wide) ----
// Thonky table:
// 00 03 06 09 12 15
// 01 04 07 10 13 16
// 02 05 08 11 14 17
//
// Our bit index i = 0..17 is LSB-first, matching "0 is least significant bit".
// Map: bitIndex = row + col*3
for (int col = 0; col < 6; col++)
{
for (int row = 0; row < 3; row++)
{
int bitIndex = row + col * 3; // 0..17
bool bit = ((bits >> bitIndex) & 1) != 0;
int r = n - 11 + row; // rows n-11..n-9
int c = col; // cols 0..5
Set(m, res, r, c, bit);
}
}
// ---- Top-right block (6 tall x 3 wide) ----
// Thonky table:
// 00 01 02
// 03 04 05
// 06 07 08
// 09 10 11
// 12 13 14
// 15 16 17
//
// Map: bitIndex = row*3 + col
for (int row = 0; row < 6; row++)
{
for (int col = 0; col < 3; col++)
{
int bitIndex = row * 3 + col; // 0..17
bool bit = ((bits >> bitIndex) & 1) != 0;
int r = row; // rows 0..5
int c = n - 11 + col; // cols n-11..n-9
Set(m, res, r, c, bit);
}
}
}
// ---------- Data & EC ----------
private static byte[] BuildDataCodewords(BitBuffer bb, int dataCw)
{
int totalBits = dataCw * 8;
// Terminator
int remaining = totalBits - bb.Length;
bb.AppendBits(0, Math.Min(4, remaining));
// Pad to byte
while (bb.Length % 8 != 0)
{
bb.AppendBits(0, 1);
}
// Build bytes
var bytes = new List<byte>(dataCw);
for (int i = 0; i < bb.Length; i += 8)
{
if (bytes.Count == dataCw)
{
break;
}
int b = 0;
for (int j = 0; j < 8; j++)
{
b = (b << 1) | bb[i + j];
}
bytes.Add((byte)b);
}
// Pad bytes
byte[] pads = { 0xEC, 0x11 }; int p = 0;
while (bytes.Count < dataCw)
{
bytes.Add(pads[(p++) & 1]);
}
return bytes.ToArray();
}
private sealed class Block
{
public byte[] Data;
public byte[] Ec;
public Block(byte[] d, byte[] e) { Data = d; Ec = e; }
}
private static List<Block> BuildBlocks(byte[] dataCwBytes, int ecPerBlock,
int grp1Blocks, int grp1DataCw, int grp2Blocks, int grp2DataCw)
{
var blocks = new List<Block>(grp1Blocks + grp2Blocks);
int idx = 0;
for (int i = 0; i < grp1Blocks; i++)
{
var data = new byte[grp1DataCw];
Array.Copy(dataCwBytes, idx, data, 0, grp1DataCw);
idx += grp1DataCw;
var ec = ReedSolomon(data, ecPerBlock);
blocks.Add(new Block(data, ec));
}
for (int i = 0; i < grp2Blocks; i++)
{
var data = new byte[grp2DataCw];
Array.Copy(dataCwBytes, idx, data, 0, grp2DataCw);
idx += grp2DataCw;
var ec = ReedSolomon(data, ecPerBlock);
blocks.Add(new Block(data, ec));
}
return blocks;
}
private static byte[] Interleave(List<Block> blocks)
{
// Interleave data bytes then EC bytes
int totalLen = 0;
int maxData = 0, maxEc = 0;
foreach (var b in blocks)
{
totalLen += b.Data.Length + b.Ec.Length;
if (b.Data.Length > maxData)
{
maxData = b.Data.Length;
}
if (b.Ec.Length > maxEc)
{
maxEc = b.Ec.Length;
}
}
var r = new byte[totalLen];
int k = 0;
for (int i = 0; i < maxData; i++)
{
foreach (var b in blocks)
{
if (i < b.Data.Length)
{
r[k++] = b.Data[i];
}
}
}
for (int i = 0; i < maxEc; i++)
{
foreach (var b in blocks)
{
if (i < b.Ec.Length)
{
r[k++] = b.Ec[i];
}
}
}
return r;
}
private static byte[] RsGenerator(int ecCount)
{
var gen = new List<byte> { 1 };
for (int i = 0; i < ecCount; i++)
{
gen = PolyMul(gen, new List<byte> { 1, GfPow(2, i) }); // (x - α^i), α=2
}
return gen.ToArray(); // length = ecCount + 1 (leading 1 included)
}
private static byte[] ReedSolomon(byte[] data, int ecCount)
{
var gen = RsGenerator(ecCount);
var msg = new byte[data.Length + ecCount];
Array.Copy(data, msg, data.Length);
for (int i = 0; i < data.Length; i++)
{
int factor = msg[i];
if (factor == 0)
{
continue;
}
for (int j = 0; j < gen.Length; j++) // use full generator
{
msg[i + j] ^= GfMul((byte)factor, gen[j]); // start at i, not i+1
}
}
var ec = new byte[ecCount];
Array.Copy(msg, data.Length, ec, 0, ecCount);
return ec;
}
private static List<byte> PolyMul(List<byte> a, List<byte> b)
{
var r = new byte[a.Count + b.Count - 1];
for (int i = 0; i < a.Count; i++)
{
for (int j = 0; j < b.Count; j++)
{
r[i + j] ^= GfMul(a[i], b[j]);
}
}
return new List<byte>(r);
}
const int GF_POLY = 0x11D;
static byte GfMul(byte x, byte y)
{
int r = 0;
while (y > 0)
{
if ((y & 1) != 0)
{
r ^= x;
}
y >>= 1;
x = (byte)((x << 1) ^ ((x & 0x80) != 0 ? GF_POLY : 0));
}
return (byte)(r & 0xFF);
}
private static byte GfPow(byte a, int e)
{
byte r = 1;
for (int i = 0; i < e; i++)
{
r = GfMul(r, a);
}
return r;
}
// ---------- Placement, masking, penalties ----------
private static void PlaceData(bool[,] m, bool[,] res, byte[] codewords)
{
int n = m.GetLength(0);
int totalBits = codewords.Length * 8;
int bitIndex = 0;
int dir = -1; // up first
for (int col = n - 1; col > 0; col -= 2)
{
if (col == 6)
{
col--; // skip timing column
}
int rowStart = (dir < 0) ? n - 1 : 0;
for (int i = 0; i < n; i++)
{
int r = rowStart + i * dir;
for (int c = 0; c < 2; c++)
{
int cc = col - c;
if (res[r, cc])
{
continue;
}
if (bitIndex < totalBits)
{
m[r, cc] = ((codewords[bitIndex >> 3] >> (7 - (bitIndex & 7))) & 1) != 0;
bitIndex++;
}
}
}
dir *= -1;
}
}
private static int ChooseBestMask(bool[,] m, bool[,] reserved)
{
int bestMask = 0, bestPenalty = int.MaxValue;
for (int mask = 0; mask < 8; mask++)
{
var masked = CloneAndMask(m, reserved, mask);
int pen = Penalty(masked);
if (pen < bestPenalty) { bestPenalty = pen; bestMask = mask; }
}
return bestMask;
}
private static bool[,] CloneAndMask(bool[,] src, bool[,] res, int mask)
{
int n = src.GetLength(0);
var dst = new bool[n, n];
for (int r = 0; r < n; r++)
{
for (int c = 0; c < n; c++)
{
bool v = src[r, c];
if (!res[r, c] && Mask(mask, r, c))
{
v = !v;
}
dst[r, c] = v;
}
}
return dst;
}
private static void ApplyMask(bool[,] m, bool[,] res, int mask)
{
int n = m.GetLength(0);
for (int r = 0; r < n; r++)
{
for (int c = 0; c < n; c++)
{
if (!res[r, c] && Mask(mask, r, c))
{
m[r, c] = !m[r, c];
}
}
}
}
private static bool Mask(int mask, int r, int c) => mask switch
{
0 => ((r + c) & 1) == 0,
1 => (r & 1) == 0,
2 => (c % 3) == 0,
3 => ((r + c) % 3) == 0,
4 => (((r / 2) + (c / 3)) & 1) == 0,
5 => ((r * c) % 2 + (r * c) % 3) == 0,
6 => ((((r * c) % 2) + ((r * c) % 3)) & 1) == 0,
7 => ((((r + c) % 2) + ((r * c) % 3)) & 1) == 0,
_ => false
};
private static int Penalty(bool[,] m)
{
int n = m.GetLength(0);
int total = 0;
// N1: Runs
for (int r = 0; r < n; r++)
{
int run = 1;
for (int c = 1; c < n; c++)
{
if (m[r, c] == m[r, c - 1])
{
run++;
}
else { if (run >= 5) { total += 3 + (run - 5); } run = 1; }
}
if (run >= 5)
{
total += 3 + (run - 5);
}
}
for (int c = 0; c < n; c++)
{
int run = 1;
for (int r = 1; r < n; r++)
{
if (m[r, c] == m[r - 1, c])
{
run++;
}
else { if (run >= 5) { total += 3 + (run - 5); } run = 1; }
}
if (run >= 5)
{
total += 3 + (run - 5);
}
}
// N2: 2x2 blocks
for (int r = 0; r < n - 1; r++)
{
for (int c = 0; c < n - 1; c++)
{
if (m[r, c] == m[r, c + 1] && m[r, c] == m[r + 1, c] && m[r, c] == m[r + 1, c + 1])
{
total += 3;
}
}
}
// N3: Finder-like patterns
int[] p1 = { 1, 0, 1, 1, 1, 0, 1, 0, 0, 0, 0 };
int[] p2 = { 0, 0, 0, 0, 1, 0, 1, 1, 1, 0, 1 };
for (int r = 0; r < n; r++)
{
for (int c = 0; c <= n - 11; c++)
{
if (MatchRow(m, r, c, p1) || MatchRow(m, r, c, p2))
{
total += 40;
}
}
}
for (int c = 0; c < n; c++)
{
for (int r = 0; r <= n - 11; r++)
{
if (MatchCol(m, r, c, p1) || MatchCol(m, r, c, p2))
{
total += 40;
}
}
}
// N4: Balance
int dark = 0;
for (int r = 0; r < n; r++)
{
for (int c = 0; c < n; c++)
{
if (m[r, c])
{
dark++;
}
}
}
int percent = (dark * 100 + (n * n / 2)) / (n * n);
total += (Math.Abs(percent - 50) / 5) * 10;
return total;
}
private static bool MatchRow(bool[,] m, int r, int c, int[] pat)
{
for (int k = 0; k < pat.Length; k++)
{
if ((m[r, c + k] ? 1 : 0) != pat[k])
{
return false;
}
}
return true;
}
private static bool MatchCol(bool[,] m, int r, int c, int[] pat)
{
for (int k = 0; k < pat.Length; k++)
{
if ((m[r + k, c] ? 1 : 0) != pat[k])
{
return false;
}
}
return true;
}
// ---------- Format & Version info ----------
private static void WriteFormatInfo(bool[,] m, bool[,] res, RadzenQREcc ecc, int mask)
{
// ECL bits: L=01, M=00, Q=11, H=10
int ecl = ecc switch { RadzenQREcc.Low => 1, RadzenQREcc.Medium => 0, RadzenQREcc.Quartile => 3, RadzenQREcc.High => 2, _ => 0 };
int data = (ecl << 3) | (mask & 7);
// BCH(15,5) with generator 0x537, then XOR with 0x5412 (as you computed)
int fmt = BchEncode(data, 0x537, 15, 5) ^ 0x5412;
bool GetBit(int i) => ((fmt >> (14 - i)) & 1) != 0; // i=0 is MSB
int n = m.GetLength(0);
// ---- First copy (around TL finder/timing), bits 0..14 ----
// row 8, cols 0..5 (bits 0..5)
for (int i = 0; i <= 5; i++)
{
Set(m, res, 8, i, GetBit(i));
}
// row 8, col 7 (bit 6)
Set(m, res, 8, 7, GetBit(6));
// row 8, col 8 (bit 7)
Set(m, res, 8, 8, GetBit(7));
// row 7, col 8 (bit 8)
Set(m, res, 7, 8, GetBit(8));
// rows 5..0, col 8 (bits 9..14)
for (int i = 9; i <= 14; i++)
{
Set(m, res, 14 - i, 8, GetBit(i));
}
// ---- Second copy (right/bottom), bits 0..14 again ----
// col 8, rows n-1..n-7 (bits 0..6)
for (int i = 0; i <= 6; i++)
{
Set(m, res, n - 1 - i, 8, GetBit(i));
}
// row 8, cols n-8..n-1 (bits 7..14)
for (int i = 7; i <= 14; i++)
{
Set(m, res, 8, n - 15 + i, GetBit(i));
}
}
private static int BchEncode(int data, int gen, int totalBits, int dataBits)
{
int d = data << (totalBits - dataBits);
for (int i = totalBits - 1; i >= (totalBits - dataBits); i--)
{
if (((d >> i) & 1) != 0)
{
d ^= gen << (i - (totalBits - dataBits));
}
}
return (data << (totalBits - dataBits)) | (d & ((1 << (totalBits - dataBits)) - 1));
}
private static void Set(bool[,] m, bool[,] res, int y, int x, bool v) { m[y, x] = v; res[y, x] = true; }
// ---------- Tables ----------
// Alignment pattern positions per version. For v1: none.
private static readonly int[][] AlignPos = BuildAlignmentPositions();
private static int[] AlignmentPatternPositions(int ver) => AlignPos[ver];
private static int[][] BuildAlignmentPositions()
{
// From QR spec table (compressed).
// Each row is the list of center positions for that version.
// v1 has no alignment patterns.
return new int[][]
{
Array.Empty<int>(),
Array.Empty<int>(),
new[]{6,18},
new[]{6,22},
new[]{6,26},
new[]{6,30},
new[]{6,34},
new[]{6,22,38},
new[]{6,24,42},
new[]{6,26,46},
new[]{6,28,50},
new[]{6,30,54},
new[]{6,32,58},
new[]{6,34,62},
new[]{6,26,46,66},
new[]{6,26,48,70},
new[]{6,26,50,74},
new[]{6,30,54,78},
new[]{6,30,56,82},
new[]{6,30,58,86},
new[]{6,34,62,90},
new[]{6,28,50,72,94},
new[]{6,26,50,74,98},
new[]{6,30,54,78,102},
new[]{6,28,54,80,106},
new[]{6,32,58,84,110},
new[]{6,30,58,86,114},
new[]{6,34,62,90,118},
new[]{6,26,50,74,98,122},
new[]{6,30,54,78,102,126},
new[]{6,26,52,78,104,130},
new[]{6,30,56,82,108,134},
new[]{6,34,60,86,112,138},
new[]{6,30,58,86,114,142},
new[]{6,34,62,90,118,146},
new[]{6,30,54,78,102,126,150},
new[]{6,24,50,76,102,128,154},
new[]{6,28,54,80,106,132,158},
new[]{6,32,58,84,110,136,162},
new[]{6,26,54,82,110,138,166},
new[]{6,30,58,86,114,142,170}
};
}
// Error correction parameters for each version & ECC:
// Returns (totalDataCw, ecPerBlock, grp1Blocks, grp1DataCw, grp2Blocks, grp2DataCw)
static (int totalDataCw, int ecPerBlock, int grp1Blocks, int grp1DataCw, int grp2Blocks, int grp2DataCw)
EcParams(int ver, RadzenQREcc ecc)
{
if (ver < 1 || ver > 40)
{
throw new ArgumentOutOfRangeException(nameof(ver), "Version must be 1..40");
}
int eccIndex = ecc switch
{
RadzenQREcc.Low => 0, // L
RadzenQREcc.Medium => 1, // M
RadzenQREcc.Quartile => 2, // Q
RadzenQREcc.High => 3, // H
_ => throw new ArgumentOutOfRangeException(nameof(ecc))
};
// EcTable[ver-1][eccIndex] = { ecPerBlock, g1Blocks, g1DataCw, g2Blocks, g2DataCw }
var row = EcTable[ver - 1][eccIndex];
int ecPerBlock = row[0];
int grp1Blocks = row[1];
int grp1DataCw = row[2];
int grp2Blocks = row[3];
int grp2DataCw = row[4];
int totalDataCw = grp1Blocks * grp1DataCw + grp2Blocks * grp2DataCw;
return (totalDataCw, ecPerBlock, grp1Blocks, grp1DataCw, grp2Blocks, grp2DataCw);
}
private static readonly int[][][] EcTable = BuildEcTable();
private static int[][][] BuildEcTable()
{
// To keep this file reasonable, the table is still large but compact.
// Format per version:
// {
// L: {ecPerBlock, g1Blocks, g1DataCw, g2Blocks, g2DataCw},
// M: {...},
// Q: {...},
// H: {...}
// }
// Source: standard QR capacity tables (ISO/IEC 18004:2015), widely reproduced.
// NOTE: These entries are carefully transcribed. If anything seems off in your tests, ping me.
// Due to message size constraints, the full 40-version table is included but trimmed for readability here.
// (Its still complete.)
return new int[][][]
{
// V1
new[]{
new[]{7,1,19,0,0}, // L: 1 block, 19 data cw, 7 ec per block
new[]{10,1,16,0,0}, // M: 1x16
new[]{13,1,13,0,0}, // Q: 1x13
new[]{17,1,9,0,0}, // H: 1x9
},
// V2
new[]{
new[]{10,1,34,0,0},
new[]{16,1,28,0,0},
new[]{22,1,22,0,0},
new[]{28,1,16,0,0},
},
// V3
new[]{
new[]{15,1,55,0,0},
new[]{26,1,44,0,0},
new[]{18,2,17,0,0}, // 2 blocks of 17 (still single group in spec presentation)
new[]{22,2,13,0,0},
},
// V4
new[]{
new[]{20,1,80,0,0},
new[]{18,2,32,0,0}, // 2x32
new[]{26,2,24,0,0},
new[]{16,4,9,0,0}, // 4x9
},
// V5
new[]{
new[]{26,1,108,0,0},
new[]{24,2,43,0,0},
new[]{18,2,15,2,16}, // (2x15,2x16)
new[]{22,2,11,2,12},
},
// V6
new[]{
new[]{18,2,68,0,0}, // 2x68
new[]{16,4,27,0,0},
new[]{24,4,19,0,0},
new[]{28,4,15,0,0},
},
// V7
new[]{
new[]{20,2,78,0,0},
new[]{18,4,31,0,0},
new[]{18,2,14,4,15},
new[]{26,4,13,1,14},
},
// V8
new[]{
new[]{24,2,97,0,0},
new[]{22,2,38,2,39},
new[]{22,4,18,2,19},
new[]{26,4,14,2,15},
},
// V9
new[]{
new[]{30,2,116,0,0},
new[]{22,3,36,2,37},
new[]{20,4,16,4,17},
new[]{24,4,12,4,13},
},
// V10
new[]{
new[]{18,2,68,2,69},
new[]{26,4,43,1,44},
new[]{24,6,19,2,20},
new[]{28,6,15,2,16},
},
// V11
new[]{
new[]{20,4,81,0,0},
new[]{30,1,50,4,51},
new[]{28,4,22,4,23},
new[]{24,3,12,8,13},
},
// V12
new[]{
new[]{24,2,92,2,93},
new[]{22,6,36,2,37},
new[]{26,4,20,6,21},
new[]{28,7,14,4,15},
},
// V13
new[]{
new[]{26,4,107,0,0},
new[]{22,8,37,1,38},
new[]{24,8,20,4,21},
new[]{22,12,11,4,12},
},
// V14
new[]{
new[]{30,3,115,1,116},
new[]{24,4,40,5,41},
new[]{20,11,16,5,17},
new[]{24,11,12,5,13},
},
// V15
new[]{
new[]{22,5,87,1,88},
new[]{24,5,41,5,42},
new[]{30,5,24,7,25},
new[]{24,11,12,7,13},
},
// V16
new[]{
new[]{24,5,98,1,99},
new[]{28,7,45,3,46},
new[]{24,15,19,2,20},
new[]{30,3,15,13,16},
},
// V17
new[]{
new[]{28,1,107,5,108},
new[]{28,10,46,1,47},
new[]{28,1,22,15,23},
new[]{28,2,14,17,15},
},
// V18
new[]{
new[]{30,5,120,1,121},
new[]{26,9,43,4,44},
new[]{28,17,22,1,23},
new[]{28,2,14,19,15},
},
// V19
new[]{
new[]{28,3,113,4,114},
new[]{26,3,44,11,45},
new[]{26,17,21,4,22},
new[]{26,9,13,16,14},
},
// V20
new[]{
new[]{28,3,107,5,108},
new[]{26,3,41,13,42},
new[]{30,15,24,5,25},
new[]{28,15,15,10,16},
},
// V21
new[]{
new[]{28,4,116,4,117},
new[]{26,17,42,0,0},
new[]{28,17,22,6,23},
new[]{30,19,16,6,17},
},
// V22
new[]{
new[]{28,2,111,7,112},
new[]{28,17,46,0,0},
new[]{30,7,24,16,25},
new[]{24,34,13,0,0},
},
// V23
new[]{
new[]{30,4,121,5,122},
new[]{28,4,47,14,48},
new[]{30,11,24,14,25},
new[]{30,16,15,14,16},
},
// V24
new[]{
new[]{30,6,117,4,118},
new[]{28,6,45,14,46},
new[]{30,11,24,16,25},
new[]{30,30,16,2,17},
},
// V25
new[]{
new[]{26,8,106,4,107},
new[]{28,8,47,13,48},
new[]{30,7,24,22,25},
new[]{30,22,15,13,16},
},
// V26
new[]{
new[]{28,10,114,2,115},
new[]{28,19,46,4,47},
new[]{28,28,22,6,23},
new[]{30,33,16,4,17},
},
// V27
new[]{
new[]{30,8,122,4,123},
new[]{28,22,45,3,46},
new[]{30,8,23,26,24},
new[]{30,12,15,28,16},
},
// V28
new[]{
new[]{30,3,117,10,118},
new[]{28,3,45,23,46},
new[]{30,4,24,31,25},
new[]{30,11,15,31,16},
},
// V29
new[]{
new[]{30,7,116,7,117},
new[]{28,21,45,7,46},
new[]{30,1,23,37,24},
new[]{30,19,15,26,16},
},
// V30
new[]{
new[]{30,5,115,10,116},
new[]{28,19,47,10,48},
new[]{30,15,24,25,25},
new[]{30,23,15,25,16},
},
// V31
new[]{
new[]{30,13,115,3,116},
new[]{28,2,46,29,47},
new[]{30,42,24,1,25},
new[]{30,23,15,28,16},
},
// V32
new[]{
new[]{30,17,115,0,0},
new[]{28,10,46,23,47},
new[]{30,10,24,35,25},
new[]{30,19,15,35,16},
},
// V33
new[]{
new[]{30,17,115,1,116},
new[]{28,14,46,21,47},
new[]{30,29,24,19,25},
new[]{30,11,15,46,16},
},
// V34
new[]{
new[]{30,13,115,6,116},
new[]{28,14,46,23,47},
new[]{30,44,24,7,25},
new[]{30,59,16,1,17},
},
// V35
new[]{
new[]{30,12,121,7,122},
new[]{28,12,47,26,48},
new[]{30,39,24,14,25},
new[]{30,22,15,41,16},
},
// V36
new[]{
new[]{30,6,121,14,122},
new[]{28,6,47,34,48},
new[]{30,46,24,10,25},
new[]{30,2,15,64,16},
},
// V37
new[]{
new[]{30,17,122,4,123},
new[]{28,29,46,14,47},
new[]{30,49,24,10,25},
new[]{30,24,15,46,16},
},
// V38
new[]{
new[]{30,4,122,18,123},
new[]{28,13,46,32,47},
new[]{30,48,24,14,25},
new[]{30,42,15,32,16},
},
// V39
new[]{
new[]{30,20,117,4,118},
new[]{28,40,47,7,48},
new[]{30,43,24,22,25},
new[]{30,10,15,67,16},
},
// V40
new[]{
new[]{30,19,118,6,119},
new[]{28,18,47,31,48},
new[]{30,34,24,34,25},
new[]{30,20,15,61,16},
},
};
}
// ---------- Helpers ----------
private sealed class BitBuffer : List<int>
{
public int Length => Count;
public void AppendBits(int val, int len)
{
for (int i = len - 1; i >= 0; i--)
{
this.Add((val >> i) & 1);
}
}
}
private static bool IsFinderCell(int r, int c, int n)
{
bool inTL = r < 7 && c < 7;
bool inTR = r < 7 && c >= n - 7;
bool inBL = r >= n - 7 && c < 7;
return inTL || inTR || inBL;
}
private static void AppendEye(StringBuilder sb, double x, double y, QRCodeEyeShape shape, string color, string maskId)
{
switch (shape)
{
case QRCodeEyeShape.Rounded:
sb.Append(CultureInfo.InvariantCulture, $"<defs><mask id=\"{maskId}\" maskUnits=\"userSpaceOnUse\">");
sb.Append(CultureInfo.InvariantCulture, $"<rect x=\"{Format(x)}\" y=\"{Format(y)}\" width=\"7\" height=\"7\" rx=\"1.25\" ry=\"1.25\" fill=\"white\"/>");
sb.Append(CultureInfo.InvariantCulture, $"<rect x=\"{Format(x + 1)}\" y=\"{Format(y + 1)}\" width=\"5\" height=\"5\" rx=\"0.25\" ry=\"0.25\" fill=\"black\"/>");
sb.Append("</mask></defs>");
sb.Append(CultureInfo.InvariantCulture, $"<rect x=\"{Format(x)}\" y=\"{Format(y)}\" width=\"7\" height=\"7\" rx=\"1.25\" ry=\"1.25\" fill=\"{color}\" mask=\"url(#{maskId})\"/>");
sb.Append(CultureInfo.InvariantCulture, $"<rect x=\"{Format(x + 2)}\" y=\"{Format(y + 2)}\" width=\"3\" height=\"3\" rx=\"0.75\" ry=\"0.75\" fill=\"{color}\"/>");
break;
case QRCodeEyeShape.Framed:
sb.Append(CultureInfo.InvariantCulture, $"<defs><mask id=\"{maskId}\" maskUnits=\"userSpaceOnUse\">");
sb.Append(CultureInfo.InvariantCulture, $"<rect x=\"{Format(x)}\" y=\"{Format(y)}\" width=\"7\" height=\"7\" fill=\"white\"/>");
sb.Append(CultureInfo.InvariantCulture, $"<rect x=\"{Format(x + 1.2)}\" y=\"{Format(y + 1.2)}\" width=\"{Format(7 - 2 * 1.2)}\" height=\"{Format(7 - 2 * 1.2)}\" fill=\"black\"/>");
sb.Append("</mask></defs>");
sb.Append(CultureInfo.InvariantCulture, $"<rect x=\"{Format(x)}\" y=\"{Format(y)}\" width=\"7\" height=\"7\" fill=\"{color}\" mask=\"url(#{maskId})\"/>");
sb.Append(CultureInfo.InvariantCulture, $"<rect x=\"{Format(x + 2)}\" y=\"{Format(y + 2)}\" width=\"3\" height=\"3\" fill=\"{color}\"/>");
break;
case QRCodeEyeShape.Square:
default:
sb.Append(CultureInfo.InvariantCulture, $"<defs><mask id=\"{maskId}\" maskUnits=\"userSpaceOnUse\">");
sb.Append(CultureInfo.InvariantCulture, $"<rect x=\"{Format(x)}\" y=\"{Format(y)}\" width=\"7\" height=\"7\" fill=\"white\"/>");
sb.Append(CultureInfo.InvariantCulture, $"<rect x=\"{Format(x + 1)}\" y=\"{Format(y + 1)}\" width=\"5\" height=\"5\" fill=\"black\"/>");
sb.Append("</mask></defs>");
sb.Append(CultureInfo.InvariantCulture, $"<rect x=\"{Format(x)}\" y=\"{Format(y)}\" width=\"7\" height=\"7\" fill=\"{color}\" mask=\"url(#{maskId})\"/>");
sb.Append(CultureInfo.InvariantCulture, $"<rect x=\"{Format(x + 2)}\" y=\"{Format(y + 2)}\" width=\"3\" height=\"3\" fill=\"{color}\"/>");
break;
}
}
private static (string Color, double Opacity) GetSvgFillParts(string? color)
{
if (string.IsNullOrWhiteSpace(color))
{
return ("none", 1);
}
if (string.Equals(color, "transparent", StringComparison.OrdinalIgnoreCase))
{
return ("rgb(0, 0, 0)", 0);
}
var rgb = RGB.Parse(color);
if (rgb == null)
{
return (color, 1);
}
var opacity = Math.Clamp(rgb.Alpha, 0, 1);
var fill = $"rgb({Format(rgb.Red)}, {Format(rgb.Green)}, {Format(rgb.Blue)})";
return (fill, opacity);
}
private static string Format(double v) => v.ToString(CultureInfo.InvariantCulture);
}