Revamped Java QrSegmentAdvanced code to implement optimization of kanji text, and simplify existing algorithms.

This commit is contained in:
Project Nayuki 2018-08-28 18:31:44 +00:00
parent ce1f7d4c4d
commit 8800acf003
1 changed files with 111 additions and 98 deletions

View File

@ -25,6 +25,7 @@ package io.nayuki.qrcodegen;
import static io.nayuki.qrcodegen.QrSegment.Mode.ALPHANUMERIC; import static io.nayuki.qrcodegen.QrSegment.Mode.ALPHANUMERIC;
import static io.nayuki.qrcodegen.QrSegment.Mode.BYTE; import static io.nayuki.qrcodegen.QrSegment.Mode.BYTE;
import static io.nayuki.qrcodegen.QrSegment.Mode.KANJI;
import static io.nayuki.qrcodegen.QrSegment.Mode.NUMERIC; import static io.nayuki.qrcodegen.QrSegment.Mode.NUMERIC;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.ArrayList; import java.util.ArrayList;
@ -42,10 +43,10 @@ public final class QrSegmentAdvanced {
/** /**
* Returns a new mutable list of zero or more segments to represent the specified Unicode text string. * Returns a new mutable list of zero or more segments to represent the specified Unicode text string.
* The resulting list optimally minimizes the total encoded bit length, subjected to the constraints given * The resulting list optimally minimizes the total encoded bit length, subjected to the constraints given
* by the specified {error correction level, minimum version number, maximum version number}, plus the additional * by the specified {error correction level, minimum version number, maximum version number}.
* constraint that the segment modes {NUMERIC, ALPHANUMERIC, BYTE} can be used but KANJI cannot be used. * <p>This function can utilize all four text encoding modes: numeric, alphanumeric, byte, and kanji.
* <p>This function can be viewed as a significantly more sophisticated and slower replacement * This can be viewed as a significantly more sophisticated and slower replacement for
* for {@link QrSegment#makeSegments(String)}, but requiring more input parameters in a way * {@link QrSegment#makeSegments(String)}, but requiring more input parameters in a way
* that overlaps with {@link QrCode#encodeSegments(List,QrCode.Ecc,int,int,int,boolean)}.</p> * that overlaps with {@link QrCode#encodeSegments(List,QrCode.Ecc,int,int,int,boolean)}.</p>
* @param text the text to be encoded, which can be any Unicode string * @param text the text to be encoded, which can be any Unicode string
* @param ecl the error correction level to use * @param ecl the error correction level to use
@ -83,102 +84,104 @@ public final class QrSegmentAdvanced {
private static List<QrSegment> makeSegmentsOptimally(String text, int version) { private static List<QrSegment> makeSegmentsOptimally(String text, int version) {
if (text.length() == 0) if (text.length() == 0)
return new ArrayList<>(); return new ArrayList<>();
byte[] data = text.getBytes(StandardCharsets.UTF_8); int[] codePoints = toCodePoints(text);
int[][] bitCosts = computeBitCosts(data, version); Mode[] charModes = computeCharacterModes(codePoints, version);
Mode[] charModes = computeCharacterModes(data, version, bitCosts); return splitIntoSegments(codePoints, charModes);
return splitIntoSegments(data, charModes);
} }
private static int[][] computeBitCosts(byte[] data, int version) { private static Mode[] computeCharacterModes(int[] codePoints, int version) {
// Segment header sizes, measured in 1/6 bits if (codePoints.length == 0)
int bytesCost = (4 + BYTE .numCharCountBits(version)) * 6;
int alphnumCost = (4 + ALPHANUMERIC.numCharCountBits(version)) * 6;
int numberCost = (4 + NUMERIC .numCharCountBits(version)) * 6;
// result[mode][len] is the number of 1/6 bits to encode the first len characters of the text, ending in the mode
int[][] result = new int[3][data.length + 1];
Arrays.fill(result[1], Integer.MAX_VALUE / 2);
Arrays.fill(result[2], Integer.MAX_VALUE / 2);
result[0][0] = bytesCost;
result[1][0] = alphnumCost;
result[2][0] = numberCost;
// Calculate the cost table using dynamic programming
for (int i = 0; i < data.length; i++) {
// Encode a character
int j = i + 1;
char c = (char)data[i];
result[0][j] = result[0][i] + 48; // 8 bits per byte
if (isAlphanumeric(c))
result[1][j] = result[1][i] + 33; // 5.5 bits per alphanumeric char
if (isNumeric(c))
result[2][j] = result[2][i] + 20; // 3.33 bits per digit
// Switch modes, rounding up fractional bits
result[0][j] = Math.min(roundUp6(Math.min(result[1][j], result[2][j])) + bytesCost , result[0][j]);
result[1][j] = Math.min(roundUp6(Math.min(result[2][j], result[0][j])) + alphnumCost, result[1][j]);
result[2][j] = Math.min(roundUp6(Math.min(result[0][j], result[1][j])) + numberCost , result[2][j]);
}
return result;
}
private static Mode[] computeCharacterModes(byte[] data, int version, int[][] bitCosts) {
if (data.length == 0)
throw new IllegalArgumentException(); throw new IllegalArgumentException();
final Mode[] modeTypes = {BYTE, ALPHANUMERIC, NUMERIC, KANJI}; // Do not modify
final int numModes = modeTypes.length;
// Segment header sizes, measured in 1/6 bits // Segment header sizes, measured in 1/6 bits
int bytesCost = (4 + BYTE .numCharCountBits(version)) * 6; final int[] headCosts = new int[numModes];
int alphnumCost = (4 + ALPHANUMERIC.numCharCountBits(version)) * 6; for (int i = 0; i < numModes; i++)
int numberCost = (4 + NUMERIC .numCharCountBits(version)) * 6; headCosts[i] = (4 + modeTypes[i].numCharCountBits(version)) * 6;
// Infer the mode used for last character by taking the minimum // charModes[i][j] represents the mode to encode the code point at
Mode curMode; // index i such that the final segment ends in modeTypes[j] and the
int end = bitCosts[0].length - 1; // total number of bits is minimized over all possible choices
if (bitCosts[0][end] <= Math.min(bitCosts[1][end], bitCosts[2][end])) Mode[][] charModes = new Mode[codePoints.length][numModes];
curMode = BYTE;
else if (bitCosts[1][end] <= bitCosts[2][end])
curMode = ALPHANUMERIC;
else
curMode = NUMERIC;
// Work backwards to calculate optimal encoding mode for each character // At the beginning of each iteration of the loop below,
Mode[] result = new Mode[data.length]; // prevCosts[j] is the exact minimum number of 1/6 bits needed to
result[data.length - 1] = curMode; // encode the entire string prefix of length i, and end in modeTypes[j]
for (int i = data.length - 2; i >= 0; i--) { int[] prevCosts = headCosts.clone();
char c = (char)data[i];
if (curMode == NUMERIC) { // Calculate costs using dynamic programming
if (isNumeric(c)) for (int i = 0; i < codePoints.length; i++) {
curMode = NUMERIC; int c = codePoints[i];
else if (isAlphanumeric(c) && roundUp6(bitCosts[1][i] + 33) + numberCost == bitCosts[2][i + 1]) int[] curCosts = new int[numModes];
curMode = ALPHANUMERIC; { // Always extend a bytes segment
int b;
if (c < 0x80)
b = 1;
else if (c < 0x800)
b = 2;
else if (c < 0x10000)
b = 3;
else else
curMode = BYTE; b = 4;
} else if (curMode == ALPHANUMERIC) { curCosts[0] = prevCosts[0] + b * 8 * 6;
if (isNumeric(c) && roundUp6(bitCosts[2][i] + 20) + alphnumCost == bitCosts[1][i + 1]) charModes[i][0] = modeTypes[0];
curMode = NUMERIC; }
else if (isAlphanumeric(c)) // Extend a segment if possible
curMode = ALPHANUMERIC; if (isAlphanumeric(c)) {
else curCosts[1] = prevCosts[1] + 33; // 5.5 bits per alphanumeric char
curMode = BYTE; charModes[i][1] = modeTypes[1];
} else if (curMode == BYTE) { }
if (isNumeric(c) && roundUp6(bitCosts[2][i] + 20) + bytesCost == bitCosts[0][i + 1]) if (isNumeric(c)) {
curMode = NUMERIC; curCosts[2] = prevCosts[2] + 20; // 3.33 bits per digit
else if (isAlphanumeric(c) && roundUp6(bitCosts[1][i] + 33) + bytesCost == bitCosts[0][i + 1]) charModes[i][2] = modeTypes[2];
curMode = ALPHANUMERIC; }
else if (isKanji(c)) {
curMode = BYTE; curCosts[3] = prevCosts[3] + 104; // 13 bits per Shift JIS char
} else charModes[i][3] = modeTypes[3];
throw new AssertionError(); }
result[i] = curMode;
// Start new segment at the end to switch modes
for (int j = 0; j < numModes; j++) { // To mode
for (int k = 0; k < numModes; k++) { // From mode
int newCost = roundUp6(curCosts[k]) + headCosts[j];
if (charModes[i][k] != null && (charModes[i][j] == null || newCost < curCosts[j])) {
curCosts[j] = newCost;
charModes[i][j] = modeTypes[k];
}
}
}
prevCosts = curCosts;
}
// Find optimal ending mode
Mode curMode = null;
for (int i = 0, minCost = 0; i < numModes; i++) {
if (curMode == null || prevCosts[i] < minCost) {
minCost = prevCosts[i];
curMode = modeTypes[i];
}
}
// Get optimal mode for each code point by tracing backwards
Mode[] result = new Mode[charModes.length];
for (int i = result.length - 1; i >= 0; i--) {
for (int j = 0; j < numModes; j++) {
if (modeTypes[j] == curMode) {
curMode = charModes[i][j];
result[i] = curMode;
break;
}
}
} }
return result; return result;
} }
private static List<QrSegment> splitIntoSegments(byte[] data, Mode[] charModes) { private static List<QrSegment> splitIntoSegments(int[] codePoints, Mode[] charModes) {
if (data.length == 0) if (codePoints.length == 0)
throw new IllegalArgumentException(); throw new IllegalArgumentException();
List<QrSegment> result = new ArrayList<>(); List<QrSegment> result = new ArrayList<>();
@ -186,20 +189,20 @@ public final class QrSegmentAdvanced {
Mode curMode = charModes[0]; Mode curMode = charModes[0];
int start = 0; int start = 0;
for (int i = 1; ; i++) { for (int i = 1; ; i++) {
if (i < data.length && charModes[i] == curMode) if (i < codePoints.length && charModes[i] == curMode)
continue; continue;
String s = new String(codePoints, start, i - start);
if (curMode == BYTE) if (curMode == BYTE)
result.add(QrSegment.makeBytes(Arrays.copyOfRange(data, start, i))); result.add(QrSegment.makeBytes(s.getBytes(StandardCharsets.UTF_8)));
else { else if (curMode == NUMERIC)
String temp = new String(data, start, i - start, StandardCharsets.US_ASCII); result.add(QrSegment.makeNumeric(s));
if (curMode == NUMERIC) else if (curMode == ALPHANUMERIC)
result.add(QrSegment.makeNumeric(temp)); result.add(QrSegment.makeAlphanumeric(s));
else if (curMode == ALPHANUMERIC) else if (curMode == KANJI)
result.add(QrSegment.makeAlphanumeric(temp)); result.add(makeKanji(s));
else else
throw new AssertionError(); throw new AssertionError();
} if (i >= codePoints.length)
if (i >= data.length)
return result; return result;
curMode = charModes[i]; curMode = charModes[i];
start = i; start = i;
@ -207,6 +210,16 @@ public final class QrSegmentAdvanced {
} }
private static int[] toCodePoints(String s) {
int[] result = s.codePoints().toArray();
for (int c : result) {
if (Character.isSurrogate((char)c))
throw new IllegalArgumentException("Invalid UTF-16 string");
}
return result;
}
private static boolean isAlphanumeric(int c) { private static boolean isAlphanumeric(int c) {
return isNumeric(c) || 'A' <= c && c <= 'Z' || " $%*+./:-".indexOf(c) != -1; return isNumeric(c) || 'A' <= c && c <= 'Z' || " $%*+./:-".indexOf(c) != -1;
} }