using System.Text;
namespace Sunfish.Qr;
/// <summary>Nivel de correccion de errores: L 7 %, M 15 %, Q 25 %, H 30 %.</summary>
public enum QrEcc { Low, Medium, Quartile, High }
/// <summary>
/// Generador de codigos QR propio (ISO/IEC 18004), sin dependencias: modo byte (UTF-8), versiones
/// 1 a 40 y eleccion de mascara por penalizacion. Devuelve la matriz de modulos; la pintan la GUI
/// (Bitmap) y la TUI (medios bloques). Pensado para URL cortas, asi que se usa el nivel de
/// correccion mas alto que quepa en la version minima.
/// </summary>
public sealed class QrCode
{
public int Version { get; }
public int Size { get; }
public QrEcc Ecc { get; }
public int Mask { get; }
private readonly bool[,] _modules;
private readonly bool[,] _function;
/// <summary>Modulo oscuro en la columna x, fila y (0..Size-1).</summary>
public bool this[int x, int y] => _modules[y, x];
public static QrCode Encode(string text, QrEcc minimum = QrEcc.Medium) =>
EncodeBytes(Encoding.UTF8.GetBytes(text), minimum);
public static QrCode EncodeBytes(byte[] data, QrEcc minimum = QrEcc.Medium)
{
int version = 1;
int usedBits;
while (true)
{
usedBits = 4 + (version <= 9 ? 8 : 16) + data.Length * 8;
if (usedBits <= DataCodewords(version, minimum) * 8)
break;
if (++version > 40)
throw new ArgumentException("Data too long for a QR code");
}
var ecc = minimum;
for (var e = minimum + 1; e <= QrEcc.High; e++)
if (usedBits <= DataCodewords(version, e) * 8)
ecc = e;
var bits = new List<bool>();
void Append(int value, int count)
{
for (int i = count - 1; i >= 0; i--)
bits.Add(((value >> i) & 1) != 0);
}
Append(0b0100, 4); // modo byte
Append(data.Length, version <= 9 ? 8 : 16);
foreach (byte b in data)
Append(b, 8);
int capacity = DataCodewords(version, ecc) * 8;
Append(0, Math.Min(4, capacity - bits.Count));
Append(0, (8 - bits.Count % 8) % 8);
for (byte pad = 0xEC; bits.Count < capacity; pad ^= 0xEC ^ 0x11)
Append(pad, 8);
var codewords = new byte[bits.Count / 8];
for (int i = 0; i < bits.Count; i++)
if (bits[i])
codewords[i >> 3] |= (byte)(0x80 >> (i & 7));
return new QrCode(version, ecc, codewords);
}
private QrCode(int version, QrEcc ecc, byte[] dataCodewords)
{
Version = version;
Ecc = ecc;
Size = version * 4 + 17;
_modules = new bool[Size, Size];
_function = new bool[Size, Size];
DrawFunctionPatterns();
DrawCodewords(AddEccAndInterleave(dataCodewords));
int best = 0;
long bestPenalty = long.MaxValue;
for (int mask = 0; mask < 8; mask++)
{
ApplyMask(mask);
DrawFormatBits(mask);
long penalty = Penalty();
if (penalty < bestPenalty)
{
best = mask;
bestPenalty = penalty;
}
ApplyMask(mask); // XOR: deshace
}
Mask = best;
ApplyMask(best);
DrawFormatBits(best);
}
// ------------------------------------------------------------------ patrones fijos
private void DrawFunctionPatterns()
{
for (int i = 0; i < Size; i++)
{
SetFunction(6, i, i % 2 == 0);
SetFunction(i, 6, i % 2 == 0);
}
DrawFinder(3, 3);
DrawFinder(Size - 4, 3);
DrawFinder(3, Size - 4);
var positions = AlignmentPositions();
int n = positions.Length;
for (int i = 0; i < n; i++)
for (int j = 0; j < n; j++)
if (!((i == 0 && j == 0) || (i == 0 && j == n - 1) || (i == n - 1 && j == 0)))
DrawAlignment(positions[i], positions[j]);
DrawFormatBits(0); // reserva el sitio; se reescribe con la mascara elegida
DrawVersion();
}
private void DrawFinder(int x, int y)
{
for (int dy = -4; dy <= 4; dy++)
for (int dx = -4; dx <= 4; dx++)
{
int dist = Math.Max(Math.Abs(dx), Math.Abs(dy));
int xx = x + dx, yy = y + dy;
if (xx >= 0 && xx < Size && yy >= 0 && yy < Size)
SetFunction(xx, yy, dist != 2 && dist != 4);
}
}
private void DrawAlignment(int x, int y)
{
for (int dy = -2; dy <= 2; dy++)
for (int dx = -2; dx <= 2; dx++)
SetFunction(x + dx, y + dy, Math.Max(Math.Abs(dx), Math.Abs(dy)) != 1);
}
private int[] AlignmentPositions()
{
if (Version == 1)
return [];
int count = Version / 7 + 2;
int step = Version == 32 ? 26 : (Version * 4 + count * 2 + 1) / (count * 2 - 2) * 2;
var result = new int[count];
result[0] = 6;
for (int i = count - 1, pos = Size - 7; i >= 1; i--, pos -= step)
result[i] = pos;
return result;
}
private void DrawFormatBits(int mask)
{
int eccBits = Ecc switch { QrEcc.Low => 1, QrEcc.Medium => 0, QrEcc.Quartile => 3, _ => 2 };
int data = eccBits << 3 | mask;
int rem = data;
for (int i = 0; i < 10; i++)
rem = (rem << 1) ^ ((rem >> 9) * 0x537);
int bits = (data << 10 | rem) ^ 0x5412;
for (int i = 0; i <= 5; i++)
SetFunction(8, i, Bit(bits, i));
SetFunction(8, 7, Bit(bits, 6));
SetFunction(8, 8, Bit(bits, 7));
SetFunction(7, 8, Bit(bits, 8));
for (int i = 9; i < 15; i++)
SetFunction(14 - i, 8, Bit(bits, i));
for (int i = 0; i < 8; i++)
SetFunction(Size - 1 - i, 8, Bit(bits, i));
for (int i = 8; i < 15; i++)
SetFunction(8, Size - 15 + i, Bit(bits, i));
SetFunction(8, Size - 8, true); // modulo oscuro fijo
}
private void DrawVersion()
{
if (Version < 7)
return;
int rem = Version;
for (int i = 0; i < 12; i++)
rem = (rem << 1) ^ ((rem >> 11) * 0x1F25);
int bits = Version << 12 | rem;
for (int i = 0; i < 18; i++)
{
bool bit = Bit(bits, i);
int a = Size - 11 + i % 3, b = i / 3;
SetFunction(a, b, bit);
SetFunction(b, a, bit);
}
}
private void SetFunction(int x, int y, bool dark)
{
_modules[y, x] = dark;
_function[y, x] = true;
}
private static bool Bit(int value, int index) => ((value >> index) & 1) != 0;
// ------------------------------------------------------------------ datos
private byte[] AddEccAndInterleave(byte[] data)
{
int blocks = Blocks[(int)Ecc][Version];
int eccLength = EccPerBlock[(int)Ecc][Version];
int raw = RawDataModules(Version) / 8;
int shortBlocks = blocks - raw % blocks;
int shortLength = raw / blocks;
var divisor = ReedSolomon.Divisor(eccLength);
var all = new List<byte[]>();
for (int i = 0, k = 0; i < blocks; i++)
{
int dataLength = shortLength - eccLength + (i < shortBlocks ? 0 : 1);
var dat = data.AsSpan(k, dataLength).ToArray();
k += dataLength;
var ecc = ReedSolomon.Remainder(dat, divisor);
// Los bloques cortos llevan un hueco para que todos midan lo mismo al intercalar.
var block = new byte[shortLength + 1];
dat.CopyTo(block, 0);
ecc.CopyTo(block, shortLength + 1 - eccLength);
all.Add(block);
}
var result = new List<byte>(raw);
for (int i = 0; i < shortLength + 1; i++)
for (int j = 0; j < all.Count; j++)
if (i != shortLength - eccLength || j >= shortBlocks)
result.Add(all[j][i]);
return [.. result];
}
private void DrawCodewords(byte[] data)
{
int i = 0;
for (int right = Size - 1; right >= 1; right -= 2)
{
if (right == 6)
right = 5;
for (int vert = 0; vert < Size; vert++)
for (int j = 0; j < 2; j++)
{
int x = right - j;
bool upward = ((right + 1) & 2) == 0;
int y = upward ? Size - 1 - vert : vert;
if (!_function[y, x] && i < data.Length * 8)
{
_modules[y, x] = ((data[i >> 3] >> (7 - (i & 7))) & 1) != 0;
i++;
}
}
}
}
private void ApplyMask(int mask)
{
for (int y = 0; y < Size; y++)
for (int x = 0; x < Size; x++)
{
if (_function[y, x])
continue;
bool invert = mask switch
{
0 => (x + y) % 2 == 0,
1 => y % 2 == 0,
2 => x % 3 == 0,
3 => (x + y) % 3 == 0,
4 => (x / 3 + y / 2) % 2 == 0,
5 => x * y % 2 + x * y % 3 == 0,
6 => (x * y % 2 + x * y % 3) % 2 == 0,
_ => ((x + y) % 2 + x * y % 3) % 2 == 0,
};
_modules[y, x] ^= invert;
}
}
/// <summary>Penalizacion de la norma (rachas, bloques 2x2, patrones de buscador, equilibrio).</summary>
private long Penalty()
{
long result = 0;
for (int pass = 0; pass < 2; pass++)
for (int a = 0; a < Size; a++)
{
int run = 1;
for (int b = 1; b < Size; b++)
{
bool same = Get(pass, a, b) == Get(pass, a, b - 1);
if (same)
run++;
if (!same || b == Size - 1)
{
if (run >= 5)
result += 3 + run - 5;
run = 1;
}
}
for (int b = 0; b + 10 < Size; b++)
if (FinderLike(pass, a, b))
result += 40;
}
for (int y = 0; y < Size - 1; y++)
for (int x = 0; x < Size - 1; x++)
{
bool c = _modules[y, x];
if (c == _modules[y, x + 1] && c == _modules[y + 1, x] && c == _modules[y + 1, x + 1])
result += 3;
}
int dark = 0;
foreach (bool m in _modules)
if (m)
dark++;
int total = Size * Size;
int k = (Math.Abs(dark * 20 - total * 10) + total - 1) / total - 1;
return result + Math.Max(0, k) * 10;
}
private bool Get(int pass, int a, int b) => pass == 0 ? _modules[a, b] : _modules[b, a];
private static readonly bool[] FinderCore = [true, false, true, true, true, false, true];
/// <summary>1011101 con cuatro claros delante o detras.</summary>
private bool FinderLike(int pass, int a, int b)
{
bool[] core = FinderCore;
bool Matches(int start)
{
for (int i = 0; i < 7; i++)
if (Get(pass, a, start + i) != core[i])
return false;
return true;
}
bool Light(int start)
{
for (int i = 0; i < 4; i++)
if (Get(pass, a, start + i))
return false;
return true;
}
return (Matches(b) && Light(b + 7)) || (Matches(b + 4) && Light(b));
}
// ------------------------------------------------------------------ tablas
private static int RawDataModules(int version)
{
int result = (16 * version + 128) * version + 64;
if (version >= 2)
{
int align = version / 7 + 2;
result -= (25 * align - 10) * align - 55;
if (version >= 7)
result -= 36;
}
return result;
}
internal static int DataCodewords(int version, QrEcc ecc) =>
RawDataModules(version) / 8 - EccPerBlock[(int)ecc][version] * Blocks[(int)ecc][version];
private static readonly int[][] EccPerBlock =
[
[-1, 7, 10, 15, 20, 26, 18, 20, 24, 30, 18, 20, 24, 26, 30, 22, 24, 28, 30, 28, 28, 28, 28, 30, 30, 26, 28, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30],
[-1, 10, 16, 26, 18, 24, 16, 18, 22, 22, 26, 30, 22, 22, 24, 24, 28, 28, 26, 26, 26, 26, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28],
[-1, 13, 22, 18, 26, 18, 24, 18, 22, 20, 24, 28, 26, 24, 20, 30, 24, 28, 28, 26, 30, 28, 30, 30, 30, 30, 28, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30],
[-1, 17, 28, 22, 16, 22, 28, 26, 26, 24, 28, 24, 28, 22, 24, 24, 30, 28, 28, 26, 28, 30, 24, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30],
];
private static readonly int[][] Blocks =
[
[-1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 4, 4, 4, 4, 4, 6, 6, 6, 6, 7, 8, 8, 9, 9, 10, 12, 12, 12, 13, 14, 15, 16, 17, 18, 19, 19, 20, 21, 22, 24, 25],
[-1, 1, 1, 1, 2, 2, 4, 4, 4, 5, 5, 5, 8, 9, 9, 10, 10, 11, 13, 14, 16, 17, 17, 18, 20, 21, 23, 25, 26, 28, 29, 31, 33, 35, 37, 38, 40, 43, 45, 47, 49],
[-1, 1, 1, 2, 2, 4, 4, 6, 6, 8, 8, 8, 10, 12, 16, 12, 17, 16, 18, 21, 20, 23, 23, 25, 27, 29, 34, 34, 35, 38, 40, 43, 45, 48, 51, 53, 56, 59, 62, 65, 68],
[-1, 1, 1, 2, 4, 4, 4, 5, 6, 8, 8, 11, 11, 16, 16, 18, 16, 19, 21, 25, 25, 25, 34, 30, 32, 35, 37, 40, 42, 45, 48, 51, 54, 57, 60, 63, 66, 70, 74, 77, 81],
];
/// <summary>Reed-Solomon sobre GF(256) con el polinomio 0x11D.</summary>
private static class ReedSolomon
{
public static byte[] Divisor(int degree)
{
var result = new byte[degree];
result[degree - 1] = 1;
int root = 1;
for (int i = 0; i < degree; i++)
{
for (int j = 0; j < degree; j++)
{
result[j] = Multiply(result[j], root);
if (j + 1 < degree)
result[j] ^= result[j + 1];
}
root = Multiply(root, 0x02);
}
return result;
}
public static byte[] Remainder(byte[] data, byte[] divisor)
{
var result = new byte[divisor.Length];
foreach (byte b in data)
{
int factor = b ^ result[0];
Array.Copy(result, 1, result, 0, result.Length - 1);
result[^1] = 0;
for (int i = 0; i < result.Length; i++)
result[i] ^= Multiply(divisor[i], factor);
}
return result;
}
private static byte Multiply(int x, int y)
{
int z = 0;
for (int i = 7; i >= 0; i--)
{
z = (z << 1) ^ ((z >> 7) * 0x11D);
z ^= ((y >> i) & 1) * x;
}
return (byte)z;
}
}
}