mirror of
https://github.com/zxing/zxing.git
synced 2024-11-09 20:44:03 -08:00
Minimal encoding for Code-128 (2nd, less intrusive version) (#1484)
* 2nd version of Code128 minimal encoding.
This commit is contained in:
parent
6c2ea9e6bc
commit
94fb277607
|
@ -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,
|
||||
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue