From 11516b1cca5ae9ef7a743e599b50e59af76223e6 Mon Sep 17 00:00:00 2001 From: Craig Everett Date: Sun, 23 Aug 2026 16:13:48 +0900 Subject: [PATCH] WIP --- .../qpq/gajumaru/core/encoding/Gf256.java | 87 +++++ .../swiss/qpq/gajumaru/core/encoding/QR.java | 347 ++++++++++++++++++ .../gajumaru/core/formatting/GajuFormat.java | 8 + 3 files changed, 442 insertions(+) create mode 100644 src/main/java/swiss/qpq/gajumaru/core/encoding/Gf256.java create mode 100644 src/main/java/swiss/qpq/gajumaru/core/encoding/QR.java diff --git a/src/main/java/swiss/qpq/gajumaru/core/encoding/Gf256.java b/src/main/java/swiss/qpq/gajumaru/core/encoding/Gf256.java new file mode 100644 index 0000000..bee9dd3 --- /dev/null +++ b/src/main/java/swiss/qpq/gajumaru/core/encoding/Gf256.java @@ -0,0 +1,87 @@ +package swiss.qpq.gajumaru.core.encoding; + +import java.util.Arrays; + +// Gf256 implements finite field arithmetic over GF(256). +// Ported from gf256.erl. + +public final class Gf256 { + + private final int[] exp; + private final int[] log; + + public Gf256(int primeModulus) { + this.exp = new int[512]; + this.log = new int[256]; + int x = 1; + for (int i = 0; i < 255; i++) { + exp[i] = x; + exp[i + 255] = x; + log[x] = i; + x <<= 1; + if (x > 255) { + x ^= primeModulus; + } + } + } + + public int add(int a, int b) { + return a ^ b; + } + + public int multiply(int a, int b) { + if (a == 0 || b == 0) return 0; + return exp[log[a] + log[b]]; + } + + public int inverse(int x) { + if (x == 0) throw new ArithmeticException("Division by zero"); + return exp[255 - log[x]]; + } + + public int exponent(int i) { + return exp[i % 255]; + } + + public int[] monomialProduct(int[] poly, int coeff, int degree) { + if (coeff == 0) return new int[]{0}; + int[] result = new int[poly.length + degree]; + for (int i = 0; i < poly.length; i++) { + result[i] = multiply(poly[i], coeff); + } + return result; + } + + public int[] polynomialProduct(int[] p1, int[] p2) { + if (isZero(p1) || isZero(p2)) return new int[]{0}; + int[] result = new int[p1.length + p2.length - 1]; + for (int i = 0; i < p1.length; i++) { + if (p1[i] == 0) continue; + for (int j = 0; j < p2.length; j++) { + result[i + j] ^= multiply(p1[i], p2[j]); + } + } + return result; + } + + public int[] divide(int[] a, int[] b) { + if (isZero(b)) throw new ArithmeticException("Division by zero"); + int inv = inverse(b[0]); + int[] r = Arrays.copyOf(a, a.length); + + int steps = a.length - b.length + 1; + for (int i = 0; i < steps; i++) { + if (r[i] == 0) continue; + int scale = multiply(r[i], inv); + for (int j = 0; j < b.length; j++) { + r[i + j] ^= multiply(b[j], scale); + } + } + // Remainder is the last b.length - 1 elements + return Arrays.copyOfRange(r, a.length - b.length + 1, a.length); + } + + private boolean isZero(int[] poly) { + return poly.length == 0 || (poly.length == 1 && poly[0] == 0); + } +} diff --git a/src/main/java/swiss/qpq/gajumaru/core/encoding/QR.java b/src/main/java/swiss/qpq/gajumaru/core/encoding/QR.java new file mode 100644 index 0000000..c65b001 --- /dev/null +++ b/src/main/java/swiss/qpq/gajumaru/core/encoding/QR.java @@ -0,0 +1,347 @@ +package swiss.qpq.gajumaru.core.encoding; + +import java.util.Arrays; + +// QR encoder for Byte mode. +// Ported from qr.erl. + +public final class QR { + + private QR() {} + + public enum ECC { + M(0); + final int bits; + ECC(int bits) { this.bits = bits; } + } + + private static final int PRIME_MODULUS = 285; + private static final int FORMAT_INFO_POLY = 1335; + private static final int FORMAT_INFO_MASK = 21522; + private static final int VERSION_INFO_POLY = 7973; + + private static final int[][] ALIGNMENT_COORDINATES = { + {}, // V0 + {}, // V1 + {6, 18}, {6, 22}, {6, 26}, {6, 30}, {6, 34}, {6, 22, 38}, {6, 24, 42}, {6, 26, 46}, + {6, 28, 50}, {6, 30, 54}, {6, 32, 58}, {6, 34, 62}, {6, 26, 46, 66}, {6, 26, 48, 70}, + {6, 26, 50, 74}, {6, 30, 54, 78}, {6, 30, 56, 82}, {6, 30, 58, 86}, {6, 34, 62, 90}, + {6, 28, 50, 72, 94}, {6, 26, 50, 74, 98}, {6, 30, 54, 78, 102}, {6, 28, 54, 80, 106}, + {6, 32, 58, 84, 110}, {6, 30, 58, 86, 114}, {6, 34, 62, 90, 118}, {6, 26, 50, 74, 98, 122}, + {6, 30, 54, 78, 102, 126}, {6, 26, 52, 78, 104, 130}, {6, 30, 56, 82, 108, 134}, + {6, 34, 60, 86, 112, 138}, {6, 30, 58, 86, 114, 142}, {6, 34, 62, 90, 118, 146}, + {6, 30, 54, 78, 102, 126, 150}, {6, 24, 50, 76, 102, 128, 154}, {6, 28, 54, 80, 106, 132, 158}, + {6, 32, 58, 84, 110, 136, 162}, {6, 26, 54, 82, 110, 138, 166}, {6, 30, 58, 86, 114, 142, 170} + }; + + private record VersionInfo(ECC ecc, int version, int byteCapacity, int[][] blocks, int remainder) {} + + private static final VersionInfo[] VERSION_TABLE = { + new VersionInfo(ECC.M, 1, 14, new int[][]{{1, 26, 16}}, 0), + new VersionInfo(ECC.M, 2, 26, new int[][]{{1, 44, 28}}, 7), + new VersionInfo(ECC.M, 3, 42, new int[][]{{1, 70, 44}}, 7), + new VersionInfo(ECC.M, 4, 62, new int[][]{{2, 50, 32}}, 7), + new VersionInfo(ECC.M, 5, 84, new int[][]{{2, 67, 43}}, 7), + new VersionInfo(ECC.M, 6, 106, new int[][]{{4, 43, 27}}, 7), + new VersionInfo(ECC.M, 7, 122, new int[][]{{4, 49, 31}}, 0), + new VersionInfo(ECC.M, 8, 152, new int[][]{{2, 60, 38}, {2, 61, 39}}, 0), + new VersionInfo(ECC.M, 9, 180, new int[][]{{3, 58, 36}, {2, 59, 37}}, 0), + new VersionInfo(ECC.M, 10, 213, new int[][]{{4, 69, 43}, {1, 70, 44}}, 0), + new VersionInfo(ECC.M, 11, 251, new int[][]{{1, 80, 50}, {4, 81, 51}}, 0), + new VersionInfo(ECC.M, 12, 287, new int[][]{{6, 58, 36}, {2, 59, 37}}, 0), + new VersionInfo(ECC.M, 13, 331, new int[][]{{8, 59, 37}, {1, 60, 38}}, 0), + new VersionInfo(ECC.M, 14, 362, new int[][]{{4, 64, 40}, {5, 65, 41}}, 3), + new VersionInfo(ECC.M, 15, 412, new int[][]{{5, 65, 41}, {5, 66, 42}}, 3), + new VersionInfo(ECC.M, 40, 3391, new int[][]{{1, 3391, 2953}}, 0) + }; + + public static boolean[][] encode(String text) { + return encode(text.getBytes(java.nio.charset.StandardCharsets.UTF_8), ECC.M); + } + + public static boolean[][] encode(byte[] data, ECC ecc) { + VersionInfo vi = chooseVersion(data.length, ecc); + if (vi == null) throw new IllegalArgumentException("Data too large for supported versions"); + + BitBuffer buffer = new BitBuffer(); + buffer.add(4, 4); // Byte mode + int cciBits = vi.version <= 9 ? 8 : 16; + buffer.add(data.length, cciBits); + for (byte b : data) buffer.add(b & 0xFF, 8); + buffer.add(0, 4); // Terminator + + int totalDataBytesCount = 0; + for (int[] b : vi.blocks) totalDataBytesCount += b[0] * b[2]; + + if (buffer.bitCount() % 8 != 0) buffer.add(0, 8 - (buffer.bitCount() % 8)); + + boolean toggle = true; + while (buffer.bitCount() / 8 < totalDataBytesCount) { + buffer.add(toggle ? 0xEC : 0x11, 8); + toggle = !toggle; + } + + byte[] codewords = generateCodewords(vi, buffer.toByteArray()); + + int dim = 17 + vi.version * 4; + boolean[][] matrix = new boolean[dim][dim]; + boolean[][] reserved = new boolean[dim][dim]; + + placeFixedPatterns(vi, matrix, reserved); + placeData(vi, matrix, reserved, codewords, vi.remainder); + + applyMask(matrix, reserved, 0); + placeFormatInfo(vi, matrix, 0); + if (vi.version >= 7) placeVersionInfo(vi, matrix); + + return matrix; + } + + private static VersionInfo chooseVersion(int length, ECC ecc) { + for (VersionInfo vi : VERSION_TABLE) { + if (vi.ecc == ecc && vi.byteCapacity >= length) return vi; + } + return null; + } + + private static byte[] generateCodewords(VersionInfo vi, byte[] data) { + Gf256 field = new Gf256(PRIME_MODULUS); + int totalBlocks = 0; + for (int[] b : vi.blocks) totalBlocks += b[0]; + + byte[][] dataBlocks = new byte[totalBlocks][]; + byte[][] eccBlocks = new byte[totalBlocks][]; + + int blockIdx = 0; + int dataOffset = 0; + for (int[] b : vi.blocks) { + int count = b[0]; + int dataBytes = b[2]; + int totalBytes = b[1]; + int eccBytes = totalBytes - dataBytes; + int[] gen = generator(field, eccBytes); + + for (int i = 0; i < count; i++) { + byte[] block = Arrays.copyOfRange(data, dataOffset, dataOffset + dataBytes); + dataBlocks[blockIdx] = block; + eccBlocks[blockIdx] = encodeRS(field, block, gen, eccBytes); + dataOffset += dataBytes; + blockIdx++; + } + } + + int totalCodewords = 0; + for (int[] b : vi.blocks) totalCodewords += b[0] * b[1]; + byte[] result = new byte[totalCodewords]; + int resOffset = 0; + + int maxData = 0; + for (byte[] b : dataBlocks) maxData = Math.max(maxData, b.length); + for (int i = 0; i < maxData; i++) { + for (byte[] b : dataBlocks) { + if (i < b.length) result[resOffset++] = b[i]; + } + } + int eccLen = eccBlocks[0].length; + for (int i = 0; i < eccLen; i++) { + for (byte[] b : eccBlocks) { + result[resOffset++] = b[i]; + } + } + return result; + } + + private static int[] generator(Gf256 f, int degree) { + int[] g = {1}; + for (int i = 0; i < degree; i++) { + g = f.polynomialProduct(g, new int[]{1, f.exponent(i)}); + } + return g; + } + + private static byte[] encodeRS(Gf256 f, byte[] data, int[] gen, int eccBytes) { + int[] poly = new int[data.length + eccBytes]; + for (int i = 0; i < data.length; i++) poly[i] = data[i] & 0xFF; + int[] rem = f.divide(poly, gen); + byte[] res = new byte[eccBytes]; + int offset = eccBytes - rem.length; + for (int i = 0; i < rem.length; i++) res[offset + i] = (byte) rem[i]; + return res; + } + + private static void placeFixedPatterns(VersionInfo vi, boolean[][] matrix, boolean[][] reserved) { + int dim = matrix.length; + placeFinder(matrix, reserved, 0, 0); + placeFinder(matrix, reserved, dim - 7, 0); + placeFinder(matrix, reserved, 0, dim - 7); + + // Separators around finders + for (int i = 0; i < 8; i++) { + reserved[7][i] = reserved[i][7] = true; + reserved[7][dim - 1 - i] = reserved[i][dim - 8] = true; + reserved[dim - 8][i] = reserved[dim - 1 - i][7] = true; + } + + // Timing patterns + for (int i = 8; i < dim - 8; i++) { + matrix[6][i] = matrix[i][6] = (i % 2 == 0); + reserved[6][i] = reserved[i][6] = true; + } + + // Alignment patterns + int[] coords = ALIGNMENT_COORDINATES[vi.version]; + for (int y : coords) { + for (int x : coords) { + if (isFinderArea(x, y, dim)) continue; + placeAlignment(matrix, reserved, x, y); + } + } + + // Dark module + matrix[4 * vi.version + 9][8] = true; + reserved[4 * vi.version + 9][8] = true; + + // Reserved areas for format and version info + for (int i = 0; i < 9; i++) reserved[i][8] = reserved[8][i] = true; + for (int i = 0; i < 8; i++) reserved[dim - 1 - i][8] = reserved[8][dim - 1 - i] = true; + + if (vi.version >= 7) { + for (int i = 0; i < 6; i++) { + for (int j = 0; j < 3; j++) { + reserved[dim - 11 + j][i] = reserved[i][dim - 11 + j] = true; + } + } + } + } + + private static boolean isFinderArea(int x, int y, int dim) { + return (x < 9 && y < 9) || (x > dim - 10 && y < 9) || (x < 9 && y > dim - 10); + } + + private static void placeFinder(boolean[][] m, boolean[][] r, int x, int y) { + for (int j = 0; j < 7; j++) { + for (int i = 0; i < 7; i++) { + int px = x + i, py = y + j; + r[py][px] = true; + boolean black = (i == 0 || i == 6 || j == 0 || j == 6 || (i >= 2 && i <= 4 && j >= 2 && j <= 4)); + m[py][px] = black; + } + } + } + + private static void placeAlignment(boolean[][] m, boolean[][] r, int x, int y) { + for (int j = -2; j <= 2; j++) { + for (int i = -2; i <= 2; i++) { + r[y + j][x + i] = true; + m[y + j][x + i] = (Math.abs(i) == 2 || Math.abs(j) == 2 || (i == 0 && j == 0)); + } + } + } + + private static void placeData(VersionInfo vi, boolean[][] m, boolean[][] r, byte[] data, int remainderBits) { + int dim = m.length; + int bitIdx = 0; + int totalBits = data.length * 8 + remainderBits; + int x = dim - 1, y = dim - 1, dir = -1; + + while (x >= 0) { + if (x == 6) x--; + for (int i = 0; i < 2; i++) { + int cx = x - i; + if (!r[y][cx]) { + if (bitIdx < totalBits) { + int byteIdx = bitIdx / 8; + int shift = 7 - (bitIdx % 8); + boolean bit = byteIdx < data.length && ((data[byteIdx] >> shift) & 1) != 0; + m[y][cx] = bit; + bitIdx++; + } + } + } + y += dir; + if (y < 0 || y >= dim) { + y -= dir; x -= 2; dir = -dir; + } + } + } + + private static void applyMask(boolean[][] m, boolean[][] r, int pattern) { + for (int y = 0; y < m.length; y++) { + for (int x = 0; x < m.length; x++) { + if (!r[y][x] && isMasked(x, y, pattern)) m[y][x] = !m[y][x]; + } + } + } + + private static boolean isMasked(int x, int y, int pattern) { + return switch (pattern) { + case 0 -> (x + y) % 2 == 0; + case 1 -> y % 2 == 0; + case 2 -> x % 3 == 0; + case 3 -> (x + y) % 3 == 0; + case 4 -> (y / 2 + x / 3) % 2 == 0; + case 5 -> ((x * y) % 2) + ((x * y) % 3) == 0; + case 6 -> (((x * y) % 2) + ((x * y) % 3)) % 2 == 0; + case 7 -> (((x + y) % 2) + ((x * y) % 3)) % 2 == 0; + default -> false; + }; + } + + private static final int[][] FORMAT_INFO_COORDS_TL = { + {8, 0}, {8, 1}, {8, 2}, {8, 3}, {8, 4}, {8, 5}, {8, 7}, {8, 8}, {7, 8}, {5, 8}, {4, 8}, {3, 8}, {2, 8}, {1, 8}, {0, 8} + }; + + private static void placeFormatInfo(VersionInfo vi, boolean[][] m, int mask) { + int info = (vi.ecc.bits << 3) | mask; + int bch = bch(info, FORMAT_INFO_POLY, 10); + int full = ((info << 10) | bch) ^ FORMAT_INFO_MASK; + int dim = m.length; + + for (int i = 0; i < 15; i++) { + boolean bit = ((full >> i) & 1) != 0; + // Top-left strip + m[FORMAT_INFO_COORDS_TL[i][0]][FORMAT_INFO_COORDS_TL[i][1]] = bit; + + // Bottom-left / Top-right strips + if (i < 8) m[8][dim - 1 - i] = bit; + else m[dim - 15 + i][8] = bit; + } + } + + private static void placeVersionInfo(VersionInfo vi, boolean[][] m) { + int bch = bch(vi.version, VERSION_INFO_POLY, 12); + int full = (vi.version << 12) | bch; + int dim = m.length; + for (int i = 0; i < 18; i++) { + boolean bit = ((full >> i) & 1) != 0; + m[dim - 11 + i % 3][i / 3] = bit; + m[i / 3][dim - 11 + i % 3] = bit; + } + } + + private static int bch(int data, int poly, int degree) { + int d = data << degree; + int msb = 31 - Integer.numberOfLeadingZeros(poly); + for (int i = 31 - Integer.numberOfLeadingZeros(d); i >= degree; i--) { + if (((d >> i) & 1) != 0) d ^= (poly << (i - msb)); + } + return d; + } + + private static class BitBuffer { + private byte[] data = new byte[32]; + private int bitIdx = 0; + public void add(int value, int bits) { + ensureCapacity((bitIdx + bits + 7) / 8); + for (int i = bits - 1; i >= 0; i--) { + if (((value >> i) & 1) != 0) data[bitIdx / 8] |= (1 << (7 - (bitIdx % 8))); + bitIdx++; + } + } + public int bitCount() { return bitIdx; } + public byte[] toByteArray() { return Arrays.copyOf(data, (bitIdx + 7) / 8); } + private void ensureCapacity(int bytes) { + if (bytes > data.length) data = Arrays.copyOf(data, Math.max(data.length * 2, bytes)); + } + } +} diff --git a/src/main/java/swiss/qpq/gajumaru/core/formatting/GajuFormat.java b/src/main/java/swiss/qpq/gajumaru/core/formatting/GajuFormat.java index ebadf64..cbbd49a 100644 --- a/src/main/java/swiss/qpq/gajumaru/core/formatting/GajuFormat.java +++ b/src/main/java/swiss/qpq/gajumaru/core/formatting/GajuFormat.java @@ -39,6 +39,14 @@ public final class GajuFormat { public enum Unit { GAJU, PUCK } public record FormatSpec(Type type, Unit unit, char separator, int span) {} + public static FormatSpec standardSpec(Type type, Unit unit) { + return switch (type) { + case US -> new FormatSpec(type, unit, ',', 3); + case JP -> new FormatSpec(type, unit, ' ', 4); + case METRIC, LEGACY -> new FormatSpec(type, unit, ' ', 3); + }; + } + private GajuFormat() {} private static final String GAJU_MARK = "木";