Minimal encoding for Code-128 (2nd, less intrusive version) (#1484)

* 2nd version of Code128 minimal encoding.
This commit is contained in:
AlexGeller1 2022-01-20 19:25:45 +01:00 committed by GitHub
parent 6c2ea9e6bc
commit 94fb277607
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 356 additions and 21 deletions

View file

@ -145,4 +145,12 @@ public enum EncodeHintType {
* Valid values are "A", "B", "C".
*/
FORCE_CODE_SET,
/**
* Specifies whether to use compact mode for Code-128 code (type {@link Boolean}, or "true" or "false"
* This can yield slightly smaller bar codes. This option and {@link #FORCE_CODE_SET} are mutually
* exclusive options.
*/
CODE128_COMPACT,
}

View file

@ -72,6 +72,16 @@ public final class Code128Writer extends OneDimensionalCodeWriter {
@Override
protected boolean[] encode(String contents, Map<EncodeHintType,?> hints) {
int forcedCodeSet = check(contents, hints);
boolean hasCompactionHint = hints != null && hints.containsKey(EncodeHintType.CODE128_COMPACT) &&
Boolean.parseBoolean(hints.get(EncodeHintType.CODE128_COMPACT).toString());
return hasCompactionHint ? new MinimalEncoder().encode(contents) : encodeFast(contents, hints, forcedCodeSet);
}
private static int check(String contents, Map<EncodeHintType,?> hints) {
int length = contents.length();
// Check length
if (length < 1 || length > 80) {
@ -139,6 +149,11 @@ public final class Code128Writer extends OneDimensionalCodeWriter {
break;
}
}
return forcedCodeSet;
}
private static boolean[] encodeFast(String contents, Map<EncodeHintType,?> hints, int forcedCodeSet) {
int length = contents.length();
Collection<int[]> patterns = new ArrayList<>(); // temporary storage for patterns
int checkSum = 0;
@ -234,7 +249,10 @@ public final class Code128Writer extends OneDimensionalCodeWriter {
checkWeight++;
}
}
return produceResult(patterns, checkSum);
}
static boolean[] produceResult(Collection<int[]> patterns, int checkSum) {
// Compute and append checksum
checkSum %= 103;
patterns.add(Code128Reader.CODE_PATTERNS[checkSum]);
@ -344,4 +362,205 @@ public final class Code128Writer extends OneDimensionalCodeWriter {
return CODE_CODE_B;
}
/**
* Encodes minimally using Divide-And-Conquer with Memoization
**/
private static class MinimalEncoder {
private enum Charset { A, B, C, NONE };
private enum Latch { A, B, C, SHIFT, NONE };
static final String A = " !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_\u0000\u0001\u0002" +
"\u0003\u0004\u0005\u0006\u0007\u0008\u0009\n\u000B\u000C\r\u000E\u000F\u0010\u0011" +
"\u0012\u0013\u0014\u0015\u0016\u0017\u0018\u0019\u001A\u001B\u001C\u001D\u001E\u001F" +
"\u00FF";
static final String B = " !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqr" +
"stuvwxyz{|}~\u007F\u00FF";
private static final int CODE_SHIFT = 98;
private int[][] memoizedCost;
private Latch[][] minPath;
private boolean[] encode(String contents) {
memoizedCost = new int[4][contents.length()];
minPath = new Latch[4][contents.length()];
encode(contents, Charset.NONE, 0);
Collection<int[]> patterns = new ArrayList<>();
int[] checkSum = new int[] {0};
int[] checkWeight = new int[] {1};
int length = contents.length();
Charset charset = Charset.NONE;
for (int i = 0; i < length; i++) {
Latch latch = minPath[charset.ordinal()][i];
switch (latch) {
case A:
charset = Charset.A;
addPattern(patterns, i == 0 ? CODE_START_A : CODE_CODE_A, checkSum, checkWeight, i);
break;
case B:
charset = Charset.B;
addPattern(patterns, i == 0 ? CODE_START_B : CODE_CODE_B, checkSum, checkWeight, i);
break;
case C:
charset = Charset.C;
addPattern(patterns, i == 0 ? CODE_START_C : CODE_CODE_C, checkSum, checkWeight, i);
break;
case SHIFT:
addPattern(patterns, CODE_SHIFT, checkSum, checkWeight, i);
break;
}
if (charset == Charset.C) {
if (contents.charAt(i) == ESCAPE_FNC_1) {
addPattern(patterns, CODE_FNC_1, checkSum, checkWeight, i);
} else {
addPattern(patterns, Integer.parseInt(contents.substring(i, i + 2)), checkSum, checkWeight, i);
assert i + 1 < length; //the algorithm never leads to a single trailing digit in character set C
if (i + 1 < length) {
i++;
}
}
} else { // charset A or B
int patternIndex;
switch (contents.charAt(i)) {
case ESCAPE_FNC_1:
patternIndex = CODE_FNC_1;
break;
case ESCAPE_FNC_2:
patternIndex = CODE_FNC_2;
break;
case ESCAPE_FNC_3:
patternIndex = CODE_FNC_3;
break;
case ESCAPE_FNC_4:
if ((charset == Charset.A && latch != Latch.SHIFT) ||
(charset == Charset.B && latch == Latch.SHIFT)) {
patternIndex = CODE_FNC_4_A;
} else {
patternIndex = CODE_FNC_4_B;
}
break;
default:
patternIndex = contents.charAt(i) - ' ';
}
if ((charset == Charset.A && latch != Latch.SHIFT) ||
(charset == Charset.B && latch == Latch.SHIFT)) {
if (patternIndex < 0) {
patternIndex += '`';
}
}
addPattern(patterns, patternIndex, checkSum, checkWeight, i);
}
}
memoizedCost = null;
minPath = null;
return produceResult(patterns, checkSum[0]);
}
private static void addPattern(Collection<int[]> patterns,
int patternIndex,
int[] checkSum,
int[] checkWeight,
int position) {
patterns.add(Code128Reader.CODE_PATTERNS[patternIndex]);
if (position != 0) {
checkWeight[0]++;
}
checkSum[0] += patternIndex * checkWeight[0];
}
private static boolean isDigit(char c) {
return c >= '0' && c <= '9';
}
private boolean canEncode(CharSequence contents, Charset charset,int position) {
char c = contents.charAt(position);
switch (charset) {
case A: return c == ESCAPE_FNC_1 ||
c == ESCAPE_FNC_2 ||
c == ESCAPE_FNC_3 ||
c == ESCAPE_FNC_4 ||
A.indexOf(c) >= 0;
case B: return c == ESCAPE_FNC_1 ||
c == ESCAPE_FNC_2 ||
c == ESCAPE_FNC_3 ||
c == ESCAPE_FNC_4 ||
B.indexOf(c) >= 0;
case C: return c == ESCAPE_FNC_1 ||
(position + 1 < contents.length() &&
isDigit(c) &&
isDigit(contents.charAt(position + 1)));
default: return false;
}
}
/**
* Encode the string starting at position position starting with the character set charset
**/
private int encode(CharSequence contents, Charset charset, int position) {
assert position < contents.length();
int mCost = memoizedCost[charset.ordinal()][position];
if (mCost > 0) {
return mCost;
}
int minCost = Integer.MAX_VALUE;
Latch minLatch = Latch.NONE;
boolean atEnd = position + 1 >= contents.length();
final Charset[] sets = new Charset[] { Charset.A,Charset.B };
for (int i = 0; i <= 1; i++) {
if (canEncode(contents, sets[i], position)) {
int cost = 1;
Latch latch = Latch.NONE;
if (charset != sets[i]) {
cost++;
latch = Latch.valueOf(sets[i].toString());
}
if (!atEnd) {
cost += encode(contents, sets[i], position + 1);
}
if (cost < minCost) {
minCost = cost;
minLatch = latch;
}
cost = 1;
if (charset == sets[(i + 1) % 2]) {
cost++;
latch = Latch.SHIFT;
if (!atEnd) {
cost += encode(contents, charset, position + 1);
}
if (cost < minCost) {
minCost = cost;
minLatch = latch;
}
}
}
}
if (canEncode(contents, Charset.C, position)) {
int cost = 1;
Latch latch = Latch.NONE;
if (charset != Charset.C) {
cost++;
latch = Latch.C;
}
int advance = contents.charAt(position) == ESCAPE_FNC_1 ? 1 : 2;
if (position + advance < contents.length()) {
cost += encode(contents, Charset.C, position + advance);
}
if (cost < minCost) {
minCost = cost;
minLatch = latch;
}
}
if (minCost == Integer.MAX_VALUE) {
throw new IllegalArgumentException("Bad character in input: ASCII value=" + (int) contents.charAt(position));
}
memoizedCost[charset.ordinal()][position] = minCost;
minPath[charset.ordinal()][position] = minLatch;
return minCost;
}
}
}

View file

@ -25,7 +25,6 @@ import com.google.zxing.BarcodeFormat;
import com.google.zxing.EncodeHintType;
import com.google.zxing.Result;
import com.google.zxing.Writer;
import com.google.zxing.WriterException;
import com.google.zxing.common.BitArray;
import com.google.zxing.common.BitMatrix;
@ -61,42 +60,57 @@ public class Code128WriterTestCase extends Assert {
}
@Test
public void testEncodeWithFunc3() throws WriterException {
public void testEncodeWithFunc3() throws Exception {
String toEncode = "\u00f3" + "123";
String expected = QUIET_SPACE + START_CODE_B + FNC3 +
// "1" "2" "3" check digit 51
"10011100110" + "11001110010" + "11001011100" + "11101000110" + STOP + QUIET_SPACE;
BitMatrix result = writer.encode(toEncode, BarcodeFormat.CODE_128, 0, 0);
BitMatrix result = encode(toEncode, false, "123");
String actual = BitMatrixTestCase.matrixToString(result);
assertEquals(expected, actual);
int width = result.getWidth();
result = encode(toEncode, true, "123");
assertEquals(width, result.getWidth());
}
@Test
public void testEncodeWithFunc2() throws WriterException {
public void testEncodeWithFunc2() throws Exception {
String toEncode = "\u00f2" + "123";
String expected = QUIET_SPACE + START_CODE_B + FNC2 +
// "1" "2" "3" check digit 56
"10011100110" + "11001110010" + "11001011100" + "11100010110" + STOP + QUIET_SPACE;
BitMatrix result = writer.encode(toEncode, BarcodeFormat.CODE_128, 0, 0);
BitMatrix result = encode(toEncode, false, "123");
String actual = BitMatrixTestCase.matrixToString(result);
assertEquals(expected, actual);
int width = result.getWidth();
result = encode(toEncode, true, "123");
assertEquals(width, result.getWidth());
}
@Test
public void testEncodeWithFunc1() throws WriterException {
public void testEncodeWithFunc1() throws Exception {
String toEncode = "\u00f1" + "123";
String expected = QUIET_SPACE + START_CODE_C + FNC1 +
// "12" "3" check digit 92
"10110011100" + SWITCH_CODE_B + "11001011100" + "10101111000" + STOP + QUIET_SPACE;
BitMatrix result = writer.encode(toEncode, BarcodeFormat.CODE_128, 0, 0);
BitMatrix result = encode(toEncode, false, "123");
String actual = BitMatrixTestCase.matrixToString(result);
assertEquals(expected, actual);
int width = result.getWidth();
result = encode(toEncode, true, "123");
assertEquals(width, result.getWidth());
}
@Test
@ -104,24 +118,88 @@ public class Code128WriterTestCase extends Assert {
String toEncode = "\u00f1" + "10958" + "\u00f1" + "17160526";
String expected = "1095817160526";
BitMatrix encResult = writer.encode(toEncode, BarcodeFormat.CODE_128, 0, 0);
BitArray row = encResult.getRow(0, null);
Result rtResult = reader.decodeRow(0, row, null);
String actual = rtResult.getText();
assertEquals(expected, actual);
BitMatrix encResult = encode(toEncode, false, expected);
int width = encResult.getWidth();
encResult = encode(toEncode, true, expected);
//Compact encoding has one latch less and encodes as STARTA,FNC1,1,CODEC,09,58,FNC1,17,16,05,26
assertEquals(width, encResult.getWidth() + 11);
}
@Test
public void testEncodeWithFunc4() throws WriterException {
public void testLongCompact() throws Exception {
//test longest possible input
String toEncode = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
BitMatrix result = encode(toEncode, true, toEncode);
}
@Test
public void testShift() throws Exception {
//compare fast to compact
String toEncode = "a\na\na\na\na\na\na\na\na\na\na\na\na\na\na\na\na\na\na\na\na\na\na\na\n";
BitMatrix result = encode(toEncode, false, toEncode);
int width = result.getWidth();
result = encode(toEncode, true, toEncode);
//big difference since the fast algoritm doesn't make use of SHIFT
assertEquals(width, result.getWidth() + 253);
}
@Test
public void testDigitMixCompaction() throws Exception {
//compare fast to compact
String toEncode = "A1A12A123A1234A12345AA1AA12AA123AA1234AA1235";
BitMatrix result = encode(toEncode, false, toEncode);
int width = result.getWidth();
result = encode(toEncode, true, toEncode);
//very good, no difference
assertEquals(width, result.getWidth());
}
@Test
public void testCompaction1() throws Exception {
//compare fast to compact
String toEncode = "AAAAAAAAAAA12AAAAAAAAA";
BitMatrix result = encode(toEncode, false, toEncode);
int width = result.getWidth();
result = encode(toEncode, true, toEncode);
//very good, no difference
assertEquals(width, result.getWidth());
}
@Test
public void testCompaction2() throws Exception {
//compare fast to compact
String toEncode = "AAAAAAAAAAA1212aaaaaaaaa";
BitMatrix result = encode(toEncode, false, toEncode);
int width = result.getWidth();
result = encode(toEncode, true, toEncode);
//very good, no difference
assertEquals(width, result.getWidth());
}
@Test
public void testEncodeWithFunc4() throws Exception {
String toEncode = "\u00f4" + "123";
String expected = QUIET_SPACE + START_CODE_B + FNC4B +
// "1" "2" "3" check digit 59
"10011100110" + "11001110010" + "11001011100" + "11100011010" + STOP + QUIET_SPACE;
BitMatrix result = writer.encode(toEncode, BarcodeFormat.CODE_128, 0, 0);
BitMatrix result = encode(toEncode, false, null);
String actual = BitMatrixTestCase.matrixToString(result);
assertEquals(expected, actual);
int width = result.getWidth();
result = encode(toEncode, true, null);
assertEquals(width, result.getWidth());
}
@Test
@ -131,11 +209,15 @@ public class Code128WriterTestCase extends Assert {
String expected = QUIET_SPACE + START_CODE_A + LF + FNC1 + FNC4A +
"10011100110" + LF + "10101111000" + STOP + QUIET_SPACE;
BitMatrix result = writer.encode(toEncode, BarcodeFormat.CODE_128, 0, 0);
BitMatrix result = encode(toEncode, false, null);
String actual = BitMatrixTestCase.matrixToString(result);
assertEquals(expected, actual);
int width = result.getWidth();
result = encode(toEncode, true, null);
assertEquals(width, result.getWidth());
}
@Test
@ -148,6 +230,7 @@ public class Code128WriterTestCase extends Assert {
SWITCH_CODE_A + "10100111100" + "11001110100" + STOP + QUIET_SPACE);
// start with B switch to A and back to B
// the compact encoder encodes this shorter as STARTB,a,b,SHIFT,NUL,a,b
testEncode("ab\0ab", QUIET_SPACE + START_CODE_B +
// "a" "b" Switch to A "\0" Switch to B
"10010110000" + "10010000110" + SWITCH_CODE_A + "10100001100" + SWITCH_CODE_B +
@ -156,15 +239,15 @@ public class Code128WriterTestCase extends Assert {
}
private void testEncode(String toEncode, String expected) throws Exception {
BitMatrix result = writer.encode(toEncode, BarcodeFormat.CODE_128, 0, 0);
BitMatrix result = encode(toEncode, false, toEncode);
String actual = BitMatrixTestCase.matrixToString(result);
assertEquals(toEncode, expected, actual);
BitArray row = result.getRow(0, null);
Result rtResult = reader.decodeRow(0, row, null);
String actualRoundtripResultText = rtResult.getText();
assertEquals(toEncode, actualRoundtripResultText);
int width = result.getWidth();
result = encode(toEncode, true, toEncode);
assert result.getWidth() <= width;
}
@Test(expected = IllegalArgumentException.class)
@ -248,4 +331,29 @@ public class Code128WriterTestCase extends Assert {
String actual = BitMatrixTestCase.matrixToString(result);
assertEquals(expected, actual);
}
private BitMatrix encode(String toEncode, boolean compact, String expectedLoopback) throws Exception {
Map<EncodeHintType, Object> hints = new EnumMap<>(EncodeHintType.class);
if (compact) {
hints.put(EncodeHintType.CODE128_COMPACT, Boolean.TRUE);
}
BitMatrix encResult = writer.encode(toEncode, BarcodeFormat.CODE_128, 0, 0, hints);
if (expectedLoopback != null) {
BitArray row = encResult.getRow(0, null);
Result rtResult = reader.decodeRow(0, row, null);
String actual = rtResult.getText();
assertEquals(expectedLoopback, actual);
}
if (compact) {
//check that what is encoded compactly yields the same on loopback as what was encoded fast.
BitArray row = encResult.getRow(0, null);
Result rtResult = reader.decodeRow(0, row, null);
String actual = rtResult.getText();
BitMatrix encResultFast = writer.encode(toEncode, BarcodeFormat.CODE_128, 0, 0);
row = encResultFast.getRow(0, null);
rtResult = reader.decodeRow(0, row, null);
assertEquals(rtResult.getText(), actual);
}
return encResult;
}
}