From 94fb277607003c070ffd1413754a782f3f87cbcd Mon Sep 17 00:00:00 2001 From: AlexGeller1 <87009702+AlexGeller1@users.noreply.github.com> Date: Thu, 20 Jan 2022 19:25:45 +0100 Subject: [PATCH] Minimal encoding for Code-128 (2nd, less intrusive version) (#1484) * 2nd version of Code128 minimal encoding. --- .../java/com/google/zxing/EncodeHintType.java | 8 + .../com/google/zxing/oned/Code128Writer.java | 219 ++++++++++++++++++ .../zxing/oned/Code128WriterTestCase.java | 150 ++++++++++-- 3 files changed, 356 insertions(+), 21 deletions(-) diff --git a/core/src/main/java/com/google/zxing/EncodeHintType.java b/core/src/main/java/com/google/zxing/EncodeHintType.java index 5b861b5e1..e67f877fc 100644 --- a/core/src/main/java/com/google/zxing/EncodeHintType.java +++ b/core/src/main/java/com/google/zxing/EncodeHintType.java @@ -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, + } diff --git a/core/src/main/java/com/google/zxing/oned/Code128Writer.java b/core/src/main/java/com/google/zxing/oned/Code128Writer.java index 65268f382..2f818f342 100644 --- a/core/src/main/java/com/google/zxing/oned/Code128Writer.java +++ b/core/src/main/java/com/google/zxing/oned/Code128Writer.java @@ -72,6 +72,16 @@ public final class Code128Writer extends OneDimensionalCodeWriter { @Override protected boolean[] encode(String contents, Map 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 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 hints, int forcedCodeSet) { + int length = contents.length(); Collection 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 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 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 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; + } + } } diff --git a/core/src/test/java/com/google/zxing/oned/Code128WriterTestCase.java b/core/src/test/java/com/google/zxing/oned/Code128WriterTestCase.java index 7571b98d9..97719ad9d 100644 --- a/core/src/test/java/com/google/zxing/oned/Code128WriterTestCase.java +++ b/core/src/test/java/com/google/zxing/oned/Code128WriterTestCase.java @@ -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 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; + } }