Newer
Older
Sunfish / src / Sunfish.Core / Qr / QrCode.cs
@Ivan Dominguez Ivan Dominguez 21 hours ago 14 KB 3.0
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;
        }
    }
}