Introduce hint to force specific code set for Code-128 encoding (#1411)
* 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 <gero@schwaericke.email>
diff --git a/core/src/main/java/com/google/zxing/EncodeHintType.java b/core/src/main/java/com/google/zxing/EncodeHintType.java
index 9a2fd57..98c66ee 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 @@
* {@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 58499f2..87359e3 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 @@
@Override
public boolean[] encode(String contents) {
+ return encode(contents, null);
+ }
+
+ @Override
+ protected boolean[] encode(String contents, Map<EncodeHintType,?> 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<int[]> patterns = new ArrayList<>(); // temporary storage for patterns
@@ -96,7 +148,12 @@
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 @@
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 3ad398e..d277d2e 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 @@
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<EncodeHintType,?> 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 @@
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 @@
// 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 a12d44d..9e7ca8f 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.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 @@
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<EncodeHintType, Object> 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<EncodeHintType, Object> 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<EncodeHintType, Object> 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<EncodeHintType, Object> 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<EncodeHintType, Object> 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<EncodeHintType, Object> 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<EncodeHintType, Object> 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);
+ }
}