WIP
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 = "木";
|
||||
|
||||
Reference in New Issue
Block a user