From d2027d0e0f210d35feda20902315e89dedf311ad Mon Sep 17 00:00:00 2001 From: gschwaer Date: Thu, 29 Jul 2021 22:28:21 +0200 Subject: [PATCH] Introduce hint to force specific code set for Code-128 encoding (#1411) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Introduce hint to force specific code set for Code-128 encoding * Address review comments * Make character check more readable; address review comments * Address review comments Co-authored-by: Gero Schwäricke --- .../java/com/google/zxing/EncodeHintType.java | 5 ++ .../com/google/zxing/oned/Code128Writer.java | 67 ++++++++++++++- .../zxing/oned/OneDimensionalCodeWriter.java | 30 ++++--- .../zxing/oned/Code128WriterTestCase.java | 82 +++++++++++++++++++ 4 files changed, 171 insertions(+), 13 deletions(-) diff --git a/core/src/main/java/com/google/zxing/EncodeHintType.java b/core/src/main/java/com/google/zxing/EncodeHintType.java index 9a2fd57e6..98c66ee0e 100644 --- a/core/src/main/java/com/google/zxing/EncodeHintType.java +++ b/core/src/main/java/com/google/zxing/EncodeHintType.java @@ -115,4 +115,9 @@ public enum EncodeHintType { * {@link String } value). */ GS1_FORMAT, + + /** + * Forces which encoding will be used. Currently only used for Code-128 code sets (Type {@link String}). Valid values are "A", "B", "C". + */ + FORCE_CODE_SET, } 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 58499f28d..87359e3f3 100644 --- a/core/src/main/java/com/google/zxing/oned/Code128Writer.java +++ b/core/src/main/java/com/google/zxing/oned/Code128Writer.java @@ -17,11 +17,13 @@ package com.google.zxing.oned; import com.google.zxing.BarcodeFormat; +import com.google.zxing.EncodeHintType; import com.google.zxing.common.BitMatrix; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; +import java.util.Map; /** * This object renders a CODE128 code as a {@link BitMatrix}. @@ -65,27 +67,77 @@ public final class Code128Writer extends OneDimensionalCodeWriter { @Override public boolean[] encode(String contents) { + return encode(contents, null); + } + + @Override + protected boolean[] encode(String contents, Map hints) { int length = contents.length(); // Check length if (length < 1 || length > 80) { throw new IllegalArgumentException( "Contents length should be between 1 and 80 characters, but got " + length); } + + // Check for forced code set hint. + int forcedCodeSet = -1; + if (hints != null && hints.containsKey(EncodeHintType.FORCE_CODE_SET)) { + String codeSetHint = hints.get(EncodeHintType.FORCE_CODE_SET).toString(); + switch (codeSetHint) { + case "A": + forcedCodeSet = CODE_CODE_A; + break; + case "B": + forcedCodeSet = CODE_CODE_B; + break; + case "C": + forcedCodeSet = CODE_CODE_C; + break; + default: + throw new IllegalArgumentException("Unsupported code set hint: " + codeSetHint); + } + } + // Check content for (int i = 0; i < length; i++) { char c = contents.charAt(i); + // check for non ascii characters that are not special GS1 characters switch (c) { + // special function characters case ESCAPE_FNC_1: case ESCAPE_FNC_2: case ESCAPE_FNC_3: case ESCAPE_FNC_4: break; + // non ascii characters default: if (c > 127) { - // support for FNC4 isn't implemented, no full Latin-1 character set available at the moment - throw new IllegalArgumentException("Bad character in input: " + c); + // no full Latin-1 character set available at the moment + // shift and manual code change are not supported + throw new IllegalArgumentException("Bad character in input: ASCII value=" + (int) c); } } + // check characters for compatibility with forced code set + switch (forcedCodeSet) { + case CODE_CODE_A: + // allows no ascii above 95 (no lower caps, no special symbols) + if (c > 95 && c <= 127) { + throw new IllegalArgumentException("Bad character in input for forced code set A: ASCII value=" + (int) c); + } + break; + case CODE_CODE_B: + // allows no ascii below 32 (terminal symbols) + if (c <= 32) { + throw new IllegalArgumentException("Bad character in input for forced code set B: ASCII value=" + (int) c); + } + break; + case CODE_CODE_C: + // allows only numbers and no FNC 2/3/4 + if (c < 48 || (c > 57 && c <= 127) || c == ESCAPE_FNC_2 || c == ESCAPE_FNC_3 || c == ESCAPE_FNC_4) { + throw new IllegalArgumentException("Bad character in input for forced code set C: ASCII value=" + (int) c); + } + break; + } } Collection patterns = new ArrayList<>(); // temporary storage for patterns @@ -96,7 +148,12 @@ public final class Code128Writer extends OneDimensionalCodeWriter { while (position < length) { //Select code to use - int newCodeSet = chooseCode(contents, position, codeSet); + int newCodeSet; + if (forcedCodeSet == -1) { + newCodeSet = chooseCode(contents, position, codeSet); + } else { + newCodeSet = forcedCodeSet; + } //Get the pattern index int patternIndex; @@ -135,6 +192,10 @@ public final class Code128Writer extends OneDimensionalCodeWriter { break; default: // CODE_CODE_C + if (position + 1 == length) { + // this is the last character, but the encoding is C, which always encodes two characers + throw new IllegalArgumentException("Bad number of characters for digit only encoding."); + } patternIndex = Integer.parseInt(contents.substring(position, position + 2)); position++; // Also incremented below break; diff --git a/core/src/main/java/com/google/zxing/oned/OneDimensionalCodeWriter.java b/core/src/main/java/com/google/zxing/oned/OneDimensionalCodeWriter.java index 3ad398edb..d277d2e6f 100644 --- a/core/src/main/java/com/google/zxing/oned/OneDimensionalCodeWriter.java +++ b/core/src/main/java/com/google/zxing/oned/OneDimensionalCodeWriter.java @@ -33,6 +33,25 @@ import java.util.regex.Pattern; public abstract class OneDimensionalCodeWriter implements Writer { private static final Pattern NUMERIC = Pattern.compile("[0-9]+"); + /** + * Encode the contents to boolean array expression of one-dimensional barcode. + * Start code and end code should be included in result, and side margins should not be included. + * + * @param contents barcode contents to encode + * @return a {@code boolean[]} of horizontal pixels (false = white, true = black) + */ + public abstract boolean[] encode(String contents); + + /** + * Can be overwritten if the encode requires to read the hints map. Otherwise it defaults to {@code encode}. + * @param contents barcode contents to encode + * @param hints encoding hints + * @return a {@code boolean[]} of horizontal pixels (false = white, true = black) + */ + protected boolean[] encode(String contents, Map hints) { + return encode(contents); + } + @Override public final BitMatrix encode(String contents, BarcodeFormat format, int width, int height) { return encode(contents, format, width, height, null); @@ -70,7 +89,7 @@ public abstract class OneDimensionalCodeWriter implements Writer { sidesMargin = Integer.parseInt(hints.get(EncodeHintType.MARGIN).toString()); } - boolean[] code = encode(contents); + boolean[] code = encode(contents, hints); return renderResult(code, width, height, sidesMargin); } @@ -135,14 +154,5 @@ public abstract class OneDimensionalCodeWriter implements Writer { // This seems like a decent idea for a default for all formats. return 10; } - - /** - * Encode the contents to boolean array expression of one-dimensional barcode. - * Start code and end code should be included in result, and side margins should not be included. - * - * @param contents barcode contents to encode - * @return a {@code boolean[]} of horizontal pixels (false = white, true = black) - */ - public abstract boolean[] encode(String contents); } 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 a12d44de1..9e7ca8f5f 100644 --- a/core/src/test/java/com/google/zxing/oned/Code128WriterTestCase.java +++ b/core/src/test/java/com/google/zxing/oned/Code128WriterTestCase.java @@ -22,12 +22,16 @@ import org.junit.Before; import org.junit.Test; 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; +import java.util.Map; +import java.util.EnumMap; + /** * Tests {@link Code128Writer}. */ @@ -151,4 +155,82 @@ public class Code128WriterTestCase extends Assert { String actualRoundtripResultText = rtResult.getText(); assertEquals(toEncode, actualRoundtripResultText); } + + @Test(expected = IllegalArgumentException.class) + public void testEncodeWithForcedCodeSetFailureCodeSetABadCharacter() throws Exception { + // Lower case characters should not be accepted when the code set is forced to A. + String toEncode = "ASDFx0123"; + + Map hints = new EnumMap<>(EncodeHintType.class); + hints.put(EncodeHintType.FORCE_CODE_SET, "A"); + BitMatrix result = writer.encode(toEncode, BarcodeFormat.CODE_128, 0, 0, hints); + } + + @Test(expected = IllegalArgumentException.class) + public void testEncodeWithForcedCodeSetFailureCodeSetBBadCharacter() throws Exception { + String toEncode = "ASdf\00123"; // \0 (ascii value 0) + // Characters with ASCII value below 32 should not be accepted when the code set is forced to B. + + Map hints = new EnumMap<>(EncodeHintType.class); + hints.put(EncodeHintType.FORCE_CODE_SET, "B"); + BitMatrix result = writer.encode(toEncode, BarcodeFormat.CODE_128, 0, 0, hints); + } + + @Test(expected = IllegalArgumentException.class) + public void testEncodeWithForcedCodeSetFailureCodeSetCBadCharactersNonNum() throws Exception { + String toEncode = "123a5678"; + // Non-digit characters should not be accepted when the code set is forced to C. + + Map hints = new EnumMap<>(EncodeHintType.class); + hints.put(EncodeHintType.FORCE_CODE_SET, "C"); + BitMatrix result = writer.encode(toEncode, BarcodeFormat.CODE_128, 0, 0, hints); + } + + @Test(expected = IllegalArgumentException.class) + public void testEncodeWithForcedCodeSetFailureCodeSetCBadCharactersFncCode() throws Exception { + String toEncode = "123\u00f2a678"; + // Function codes other than 1 should not be accepted when the code set is forced to C. + + Map hints = new EnumMap<>(EncodeHintType.class); + hints.put(EncodeHintType.FORCE_CODE_SET, "C"); + BitMatrix result = writer.encode(toEncode, BarcodeFormat.CODE_128, 0, 0, hints); + } + + @Test(expected = IllegalArgumentException.class) + public void testEncodeWithForcedCodeSetFailureCodeSetCWrongAmountOfDigits() throws Exception { + String toEncode = "123456789"; + // An uneven amount of digits should not be accepted when the code set is forced to C. + + Map hints = new EnumMap<>(EncodeHintType.class); + hints.put(EncodeHintType.FORCE_CODE_SET, "C"); + BitMatrix result = writer.encode(toEncode, BarcodeFormat.CODE_128, 0, 0, hints); + } + + @Test + public void testEncodeWithForcedCodeSetFailureCodeSetA() throws Exception { + String toEncode = "AB123"; + // would default to B "A" "B" "1" "2" "3" check digit 10 + String expected = QUIET_SPACE + START_CODE_A + "10100011000" + "10001011000" + "10011100110" + "11001110010" + "11001011100" + "11001000100" + STOP + QUIET_SPACE; + + Map hints = new EnumMap<>(EncodeHintType.class); + hints.put(EncodeHintType.FORCE_CODE_SET, "A"); + BitMatrix result = writer.encode(toEncode, BarcodeFormat.CODE_128, 0, 0, hints); + + String actual = BitMatrixTestCase.matrixToString(result); + assertEquals(expected, actual); + } + + @Test + public void testEncodeWithForcedCodeSetFailureCodeSetB() throws Exception { + String toEncode = "1234"; + // would default to C "1" "2" "3" "4" check digit 88 + String expected = QUIET_SPACE + START_CODE_B + "10011100110" + "11001110010" + "11001011100" + "11001001110" + "11110010010" + STOP + QUIET_SPACE; + + Map hints = new EnumMap<>(EncodeHintType.class); + hints.put(EncodeHintType.FORCE_CODE_SET, "B"); + BitMatrix result = writer.encode(toEncode, BarcodeFormat.CODE_128, 0, 0, hints); + + String actual = BitMatrixTestCase.matrixToString(result); + assertEquals(expected, actual); + } }