diff options
| author | AlexGeller1 <87009702+AlexGeller1@users.noreply.github.com> | 2022-03-05 17:07:37 +0300 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2022-03-05 17:07:37 +0300 |
| commit | 92854d4a5553465ff42e68927f3a161bb8977e98 (patch) | |
| tree | 71a36ad3199acb603d3a284c1031756d1ff7bdff /core/src/main | |
| parent | 4bd257e8c5577036b38352082bde646045bf95ee (diff) | |
| download | zxing-92854d4a5553465ff42e68927f3a161bb8977e98.tar.xz | |
Add support for multi-eci encoding for PDF417 (#1506)
* - Added multi-eci encoding for PDF417
- Fixed issue that the PDF417 decoder incorrectly decoded input with a leading ECI and no explicit latch to TEXT encoding
Diffstat (limited to 'core/src/main')
9 files changed, 730 insertions, 313 deletions
diff --git a/core/src/main/java/com/google/zxing/EncodeHintType.java b/core/src/main/java/com/google/zxing/EncodeHintType.java index 2c68f2af2..f94b575a5 100644 --- a/core/src/main/java/com/google/zxing/EncodeHintType.java +++ b/core/src/main/java/com/google/zxing/EncodeHintType.java @@ -104,6 +104,16 @@ public enum EncodeHintType { PDF417_DIMENSIONS, /** + * Specifies whether to automatically insert ECIs when encoding PDF417 (type {@link Boolean}, or "true" or "false" + * {@link String} value). + * Please note that in that case, the most compact character encoding is chosen for characters in + * the input that are not in the ISO-8859-1 character set. Based on experience, some scanners do not + * support encodings like cp-1256 (Arabic). In such cases the encoding can be forced to UTF-8 by + * means of the {@link #CHARACTER_SET} encoding hint. + */ + PDF417_AUTO_ECI, + + /** * Specifies the required number of layers for an Aztec code. * A negative number (-1, -2, -3, -4) specifies a compact Aztec code. * 0 indicates to use the minimum number of layers (the default). diff --git a/core/src/main/java/com/google/zxing/common/ECIInput.java b/core/src/main/java/com/google/zxing/common/ECIInput.java new file mode 100755 index 000000000..ad444e13c --- /dev/null +++ b/core/src/main/java/com/google/zxing/common/ECIInput.java @@ -0,0 +1,107 @@ +/* + * Copyright 2021 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.common; + +/** + * Interface to navigate a sequence of ECIs and bytes. + * + * @author Alex Geller + */ +public interface ECIInput { + + /** + * Returns the length of this input. The length is the number + * of {@code byte}s in or ECIs in the sequence. + * + * @return the number of {@code char}s in this sequence + */ + int length(); + + /** + * Returns the {@code byte} value at the specified index. An index ranges from zero + * to {@code length() - 1}. The first {@code byte} value of the sequence is at + * index zero, the next at index one, and so on, as for array + * indexing. + * + * @param index the index of the {@code byte} value to be returned + * + * @return the specified {@code byte} value as character or the FNC1 character + * + * @throws IndexOutOfBoundsException + * if the {@code index} argument is negative or not less than + * {@code length()} + * @throws IllegalArgumentException + * if the value at the {@code index} argument is an ECI (@see #isECI) + */ + char charAt(int index); + + /** + * Returns a {@code CharSequence} that is a subsequence of this sequence. + * The subsequence starts with the {@code char} value at the specified index and + * ends with the {@code char} value at index {@code end - 1}. The length + * (in {@code char}s) of the + * returned sequence is {@code end - start}, so if {@code start == end} + * then an empty sequence is returned. + * + * @param start the start index, inclusive + * @param end the end index, exclusive + * + * @return the specified subsequence + * + * @throws IndexOutOfBoundsException + * if {@code start} or {@code end} are negative, + * if {@code end} is greater than {@code length()}, + * or if {@code start} is greater than {@code end} + * @throws IllegalArgumentException + * if a value in the range {@code start}-{@code end} is an ECI (@see #isECI) + */ + CharSequence subSequence(int start, int end); + + /** + * Determines if a value is an ECI + * + * @param index the index of the value + * + * @return true if the value at position {@code index} is an ECI + * + * @throws IndexOutOfBoundsException + * if the {@code index} argument is negative or not less than + * {@code length()} + */ + boolean isECI(int index); + + /** + * Returns the {@code int} ECI value at the specified index. An index ranges from zero + * to {@code length() - 1}. The first {@code byte} value of the sequence is at + * index zero, the next at index one, and so on, as for array + * indexing. + * + * @param index the index of the {@code int} value to be returned + * + * @return the specified {@code int} ECI value. + * The ECI specified the encoding of all bytes with a higher index until the + * next ECI or until the end of the input if no other ECI follows. + * + * @throws IndexOutOfBoundsException + * if the {@code index} argument is negative or not less than + * {@code length()} + * @throws IllegalArgumentException + * if the value at the {@code index} argument is not an ECI (@see #isECI) + */ + int getECIValue(int index); + boolean haveNCharacters(int index, int n); +} diff --git a/core/src/main/java/com/google/zxing/common/MinimalECIInput.java b/core/src/main/java/com/google/zxing/common/MinimalECIInput.java new file mode 100755 index 000000000..ac204b832 --- /dev/null +++ b/core/src/main/java/com/google/zxing/common/MinimalECIInput.java @@ -0,0 +1,325 @@ +/* + * Copyright 2021 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.common; + +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.List; + +/** + * Class that converts a character string into a sequence of ECIs and bytes + * + * The implementation uses the Dijkstra algorithm to produce minimal encodings + * + * @author Alex Geller + */ +public class MinimalECIInput implements ECIInput { + + private static final int COST_PER_ECI = 3; // approximated (latch + 2 codewords) + private final int[] bytes; + private final int fnc1; + + /** + * Constructs a minimal input + * + * @param stringToEncode the character string to encode + * @param priorityCharset The preferred {@link Charset}. When the value of the argument is null, the algorithm + * chooses charsets that leads to a minimal representation. Otherwise the algorithm will use the priority + * charset to encode any character in the input that can be encoded by it if the charset is among the + * supported charsets. + * @param fnc1 denotes the character in the input that represents the FNC1 character or -1 if this is not GS1 + * input. + */ + public MinimalECIInput(String stringToEncode, Charset priorityCharset, int fnc1) { + this.fnc1 = fnc1; + ECIEncoderSet encoderSet = new ECIEncoderSet(stringToEncode, priorityCharset, fnc1); + if (encoderSet.length() == 1) { //optimization for the case when all can be encoded without ECI in ISO-8859-1 + bytes = new int[stringToEncode.length()]; + for (int i = 0; i < bytes.length; i++) { + char c = stringToEncode.charAt(i); + bytes[i] = c == fnc1 ? 1000 : (int) c; + } + } else { + bytes = encodeMinimally(stringToEncode, encoderSet, fnc1); + } + } + + public int getFNC1Character() { + return fnc1; + } + + /** + * Returns the length of this input. The length is the number + * of {@code byte}s, FNC1 characters or ECIs in the sequence. + * + * @return the number of {@code char}s in this sequence + */ + public int length() { + return bytes.length; + } + + public boolean haveNCharacters(int index, int n) { + if (index + n - 1 >= bytes.length) { + return false; + } + for (int i = 0; i < n; i++) { + if (isECI(index + i)) { + return false; + } + } + return true; + } + + /** + * Returns the {@code byte} value at the specified index. An index ranges from zero + * to {@code length() - 1}. The first {@code byte} value of the sequence is at + * index zero, the next at index one, and so on, as for array + * indexing. + * + * @param index the index of the {@code byte} value to be returned + * + * @return the specified {@code byte} value as character or the FNC1 character + * + * @throws IndexOutOfBoundsException + * if the {@code index} argument is negative or not less than + * {@code length()} + * @throws IllegalArgumentException + * if the value at the {@code index} argument is an ECI (@see #isECI) + */ + public char charAt(int index) { + if (index < 0 || index >= length()) { + throw new IndexOutOfBoundsException("" + index); + } + if (isECI(index)) { + throw new IllegalArgumentException("value at " + index + " is not a character but an ECI"); + } + return isFNC1(index) ? (char) fnc1 : (char) bytes[index]; + } + + /** + * Returns a {@code CharSequence} that is a subsequence of this sequence. + * The subsequence starts with the {@code char} value at the specified index and + * ends with the {@code char} value at index {@code end - 1}. The length + * (in {@code char}s) of the + * returned sequence is {@code end - start}, so if {@code start == end} + * then an empty sequence is returned. + * + * @param start the start index, inclusive + * @param end the end index, exclusive + * + * @return the specified subsequence + * + * @throws IndexOutOfBoundsException + * if {@code start} or {@code end} are negative, + * if {@code end} is greater than {@code length()}, + * or if {@code start} is greater than {@code end} + * @throws IllegalArgumentException + * if a value in the range {@code start}-{@code end} is an ECI (@see #isECI) + */ + public CharSequence subSequence(int start, int end) { + if (start < 0 || start > end || end > length()) { + throw new IndexOutOfBoundsException("" + start); + } + StringBuilder result = new StringBuilder(); + for (int i = start; i < end; i++) { + if (isECI(i)) { + throw new IllegalArgumentException("value at " + i + " is not a character but an ECI"); + } + result.append(charAt(i)); + } + return result; + } + + /** + * Determines if a value is an ECI + * + * @param index the index of the value + * + * @return true if the value at position {@code index} is an ECI + * + * @throws IndexOutOfBoundsException + * if the {@code index} argument is negative or not less than + * {@code length()} + */ + public boolean isECI(int index) { + if (index < 0 || index >= length()) { + throw new IndexOutOfBoundsException("" + index); + } + return bytes[index] > 255 && bytes[index] <= 999; + } + + /** + * Determines if a value is the FNC1 character + * + * @param index the index of the value + * + * @return true if the value at position {@code index} is the FNC1 character + * + * @throws IndexOutOfBoundsException + * if the {@code index} argument is negative or not less than + * {@code length()} + */ + public boolean isFNC1(int index) { + if (index < 0 || index >= length()) { + throw new IndexOutOfBoundsException("" + index); + } + return bytes[index] == 1000; + } + + /** + * Returns the {@code int} ECI value at the specified index. An index ranges from zero + * to {@code length() - 1}. The first {@code byte} value of the sequence is at + * index zero, the next at index one, and so on, as for array + * indexing. + * + * @param index the index of the {@code int} value to be returned + * + * @return the specified {@code int} ECI value. + * The ECI specified the encoding of all bytes with a higher index until the + * next ECI or until the end of the input if no other ECI follows. + * + * @throws IndexOutOfBoundsException + * if the {@code index} argument is negative or not less than + * {@code length()} + * @throws IllegalArgumentException + * if the value at the {@code index} argument is not an ECI (@see #isECI) + */ + public int getECIValue(int index) { + if (index < 0 || index >= length()) { + throw new IndexOutOfBoundsException("" + index); + } + if (!isECI(index)) { + throw new IllegalArgumentException("value at " + index + " is not an ECI but a character"); + } + return bytes[index] - 256; + } + + static void addEdge(InputEdge[][] edges, int to, InputEdge edge) { + if (edges[to][edge.encoderIndex] == null || + edges[to][edge.encoderIndex].cachedTotalSize > edge.cachedTotalSize) { + edges[to][edge.encoderIndex] = edge; + } + } + + static void addEdges(String stringToEncode, + ECIEncoderSet encoderSet, + InputEdge[][] edges, + int from, + InputEdge previous, + int fnc1) { + + char ch = stringToEncode.charAt(from); + + int start = 0; + int end = encoderSet.length(); + if (encoderSet.getPriorityEncoderIndex() >= 0 && (ch == fnc1 || encoderSet.canEncode(ch, + encoderSet.getPriorityEncoderIndex()))) { + start = encoderSet.getPriorityEncoderIndex(); + end = start + 1; + } + + for (int i = start; i < end; i++) { + if (ch == fnc1 || encoderSet.canEncode(ch,i)) { + addEdge(edges, from + 1, new InputEdge(ch, encoderSet, i, previous, fnc1)); + } + } + } + + static int[] encodeMinimally(String stringToEncode, ECIEncoderSet encoderSet, int fnc1) { + int inputLength = stringToEncode.length(); + + // Array that represents vertices. There is a vertex for every character and encoding. + InputEdge[][] edges = new InputEdge[inputLength + 1][encoderSet.length()]; + addEdges(stringToEncode, encoderSet, edges, 0, null, fnc1); + + for (int i = 1; i <= inputLength; i++) { + for (int j = 0; j < encoderSet.length(); j++) { + if (edges[i][j] != null && i < inputLength) { + addEdges(stringToEncode, encoderSet, edges, i, edges[i][j], fnc1); + } + } + //optimize memory by removing edges that have been passed. + for (int j = 0; j < encoderSet.length(); j++) { + edges[i - 1][j] = null; + } + } + int minimalJ = -1; + int minimalSize = Integer.MAX_VALUE; + for (int j = 0; j < encoderSet.length(); j++) { + if (edges[inputLength][j] != null) { + InputEdge edge = edges[inputLength][j]; + if (edge.cachedTotalSize < minimalSize) { + minimalSize = edge.cachedTotalSize; + minimalJ = j; + } + } + } + if (minimalJ < 0) { + throw new RuntimeException("Internal error: failed to encode \"" + stringToEncode + "\""); + } + List<Integer> intsAL = new ArrayList<>(); + InputEdge current = edges[inputLength][minimalJ]; + while (current != null) { + if (current.isFNC1()) { + intsAL.add(0, 1000); + } else { + byte[] bytes = encoderSet.encode(current.c,current.encoderIndex); + for (int i = bytes.length - 1; i >= 0; i--) { + intsAL.add(0, (bytes[i] & 0xFF)); + } + } + int previousEncoderIndex = current.previous == null ? 0 : current.previous.encoderIndex; + if (previousEncoderIndex != current.encoderIndex) { + intsAL.add(0,256 + encoderSet.getECIValue(current.encoderIndex)); + } + current = current.previous; + } + int[] ints = new int[intsAL.size()]; + for (int i = 0; i < ints.length; i++) { + ints[i] = intsAL.get(i); + } + return ints; + } + + private static final class InputEdge { + private final char c; + private final int encoderIndex; //the encoding of this edge + private final InputEdge previous; + private final int cachedTotalSize; + + private InputEdge(char c, ECIEncoderSet encoderSet, int encoderIndex, InputEdge previous, int fnc1) { + this.c = c == fnc1 ? 1000 : c; + this.encoderIndex = encoderIndex; + this.previous = previous; + + int size = this.c == 1000 ? 1 : encoderSet.encode(c, encoderIndex).length; + int previousEncoderIndex = previous == null ? 0 : previous.encoderIndex; + if (previousEncoderIndex != encoderIndex) { + size += COST_PER_ECI; + } + if (previous != null) { + size += previous.cachedTotalSize; + } + this.cachedTotalSize = size; + } + + boolean isFNC1() { + return c == 1000; + } + + } +} diff --git a/core/src/main/java/com/google/zxing/datamatrix/encoder/MinimalEncoder.java b/core/src/main/java/com/google/zxing/datamatrix/encoder/MinimalEncoder.java index 284d5d646..4d3dd8d3c 100755 --- a/core/src/main/java/com/google/zxing/datamatrix/encoder/MinimalEncoder.java +++ b/core/src/main/java/com/google/zxing/datamatrix/encoder/MinimalEncoder.java @@ -21,7 +21,7 @@ import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.List; -import com.google.zxing.common.ECIEncoderSet; +import com.google.zxing.common.MinimalECIInput; /** * Encoder that encodes minimally @@ -1022,32 +1022,15 @@ public final class MinimalEncoder { } - private static final class Input { + private static final class Input extends MinimalECIInput { - private static final int COST_PER_ECI = 3; // approximated (latch to ASCII + 2 codewords) - private final int[] bytes; - private final int fnc1; private final SymbolShapeHint shape; private final int macroId; private Input(String stringToEncode, Charset priorityCharset, int fnc1, SymbolShapeHint shape, int macroId) { - this.fnc1 = fnc1; + super(stringToEncode, priorityCharset, fnc1); this.shape = shape; this.macroId = macroId; - ECIEncoderSet encoderSet = new ECIEncoderSet(stringToEncode, priorityCharset, fnc1); - if (encoderSet.length() == 1) { //optimization for the case when all can be encoded without ECI in ISO-8859-1 - bytes = new int[stringToEncode.length()]; - for (int i = 0; i < bytes.length; i++) { - char c = stringToEncode.charAt(i); - bytes[i] = c == fnc1 ? 1000 : (int) c; - } - } else { - bytes = encodeMinimally(stringToEncode, encoderSet, fnc1); - } - } - - private int getFNC1Character() { - return fnc1; } private int getMacroId() { @@ -1057,154 +1040,5 @@ public final class MinimalEncoder { private SymbolShapeHint getShapeHint() { return shape; } - - private int length() { - return bytes.length; - } - - boolean haveNCharacters(int index, int n) { - if (index + n - 1 >= bytes.length) { - return false; - } - for (int i = 0; i < n; i++) { - if (isECI(index + i)) { - return false; - } - } - return true; - } - - private char charAt(int index) { - assert !isECI(index); - return isFNC1(index) ? (char) fnc1 : (char) bytes[index]; - } - - private boolean isECI(int index) { - return bytes[index] > 255 && bytes[index] <= 999; - } - - private boolean isFNC1(int index) { - return bytes[index] == 1000; - } - - private int getECIValue(int index) { - assert isECI(index); - return bytes[index] - 256; - } - - static void addEdge(InputEdge[][] edges, int to, InputEdge edge) { - if (edges[to][edge.encoderIndex] == null || - edges[to][edge.encoderIndex].cachedTotalSize > edge.cachedTotalSize) { - edges[to][edge.encoderIndex] = edge; - } - } - - static void addEdges(String stringToEncode, - ECIEncoderSet encoderSet, - InputEdge[][] edges, - int from, - InputEdge previous, - int fnc1) { - - char ch = stringToEncode.charAt(from); - - int start = 0; - int end = encoderSet.length(); - if (encoderSet.getPriorityEncoderIndex() >= 0 && (ch == fnc1 || encoderSet.canEncode(ch, - encoderSet.getPriorityEncoderIndex()))) { - start = encoderSet.getPriorityEncoderIndex(); - end = start + 1; - } - - for (int i = start; i < end; i++) { - if (ch == fnc1 || encoderSet.canEncode(ch,i)) { - addEdge(edges, from + 1, new InputEdge(ch, encoderSet, i, previous, fnc1)); - } - } - } - - static int[] encodeMinimally(String stringToEncode, ECIEncoderSet encoderSet, int fnc1) { - int inputLength = stringToEncode.length(); - - // Array that represents vertices. There is a vertex for every character and encoding. - InputEdge[][] edges = new InputEdge[inputLength + 1][encoderSet.length()]; - addEdges(stringToEncode, encoderSet, edges, 0, null, fnc1); - - for (int i = 1; i <= inputLength; i++) { - for (int j = 0; j < encoderSet.length(); j++) { - if (edges[i][j] != null && i < inputLength) { - addEdges(stringToEncode, encoderSet, edges, i, edges[i][j], fnc1); - } - } - //optimize memory by removing edges that have been passed. - for (int j = 0; j < encoderSet.length(); j++) { - edges[i - 1][j] = null; - } - } - int minimalJ = -1; - int minimalSize = Integer.MAX_VALUE; - for (int j = 0; j < encoderSet.length(); j++) { - if (edges[inputLength][j] != null) { - InputEdge edge = edges[inputLength][j]; - if (edge.cachedTotalSize < minimalSize) { - minimalSize = edge.cachedTotalSize; - minimalJ = j; - } - } - } - if (minimalJ < 0) { - throw new RuntimeException("Internal error: failed to encode \"" + stringToEncode + "\""); - } - List<Integer> intsAL = new ArrayList<>(); - InputEdge current = edges[inputLength][minimalJ]; - while (current != null) { - if (current.isFNC1()) { - intsAL.add(0, 1000); - } else { - byte[] bytes = encoderSet.encode(current.c,current.encoderIndex); - for (int i = bytes.length - 1; i >= 0; i--) { - intsAL.add(0, (bytes[i] & 0xFF)); - } - } - int previousEncoderIndex = current.previous == null ? 0 : current.previous.encoderIndex; - if (previousEncoderIndex != current.encoderIndex) { - intsAL.add(0,256 + encoderSet.getECIValue(current.encoderIndex)); - } - current = current.previous; - } - int[] ints = new int[intsAL.size()]; - for (int i = 0; i < ints.length; i++) { - ints[i] = intsAL.get(i); - } - return ints; - } - - private static final class InputEdge { - private final char c; - private final int encoderIndex; //the encoding of this edge - private final InputEdge previous; - private final int cachedTotalSize; - - private InputEdge(char c, ECIEncoderSet encoderSet, int encoderIndex, InputEdge previous, int fnc1) { - this.c = c == fnc1 ? 1000 : c; - this.encoderIndex = encoderIndex; - this.previous = previous; - - int size = this.c == 1000 ? 1 : encoderSet.encode(c, encoderIndex).length; - int previousEncoderIndex = previous == null ? 0 : previous.encoderIndex; - if (previousEncoderIndex != encoderIndex) { - size += COST_PER_ECI; - } - if (previous != null) { - size += previous.cachedTotalSize; - } - this.cachedTotalSize = size; - } - - boolean isFNC1() { - return c == 1000; - } - - } } } diff --git a/core/src/main/java/com/google/zxing/pdf417/PDF417Writer.java b/core/src/main/java/com/google/zxing/pdf417/PDF417Writer.java index 273e092bd..80e6ff7b3 100644 --- a/core/src/main/java/com/google/zxing/pdf417/PDF417Writer.java +++ b/core/src/main/java/com/google/zxing/pdf417/PDF417Writer.java @@ -57,6 +57,7 @@ public final class PDF417Writer implements Writer { PDF417 encoder = new PDF417(); int margin = WHITE_SPACE; int errorCorrectionLevel = DEFAULT_ERROR_CORRECTION_LEVEL; + boolean autoECI = false; if (hints != null) { if (hints.containsKey(EncodeHintType.PDF417_COMPACT)) { @@ -82,9 +83,11 @@ public final class PDF417Writer implements Writer { Charset encoding = Charset.forName(hints.get(EncodeHintType.CHARACTER_SET).toString()); encoder.setEncoding(encoding); } + autoECI = hints.containsKey(EncodeHintType.PDF417_AUTO_ECI) && + Boolean.parseBoolean(hints.get(EncodeHintType.PDF417_AUTO_ECI).toString()); } - return bitMatrixFromEncoder(encoder, contents, errorCorrectionLevel, width, height, margin); + return bitMatrixFromEncoder(encoder, contents, errorCorrectionLevel, width, height, margin, autoECI); } @Override @@ -103,8 +106,9 @@ public final class PDF417Writer implements Writer { int errorCorrectionLevel, int width, int height, - int margin) throws WriterException { - encoder.generateBarcodeLogic(contents, errorCorrectionLevel); + int margin, + boolean autoECI) throws WriterException { + encoder.generateBarcodeLogic(contents, errorCorrectionLevel, autoECI); int aspectRatio = 4; byte[][] originalScale = encoder.getBarcodeMatrix().getScaledMatrix(1, aspectRatio); diff --git a/core/src/main/java/com/google/zxing/pdf417/decoder/DecodedBitStreamParser.java b/core/src/main/java/com/google/zxing/pdf417/decoder/DecodedBitStreamParser.java index f732edd92..ab52e665d 100644 --- a/core/src/main/java/com/google/zxing/pdf417/decoder/DecodedBitStreamParser.java +++ b/core/src/main/java/com/google/zxing/pdf417/decoder/DecodedBitStreamParser.java @@ -104,7 +104,12 @@ final class DecodedBitStreamParser { static DecoderResult decode(int[] codewords, String ecLevel) throws FormatException { StringBuilder result = new StringBuilder(codewords.length * 2); Charset encoding = StandardCharsets.ISO_8859_1; - int codeIndex = textCompaction(codewords, 1, result); + int codeIndex = 1; + if (codewords[0] > 1 && codewords[codeIndex] == ECI_CHARSET) { + encoding = getECICharset(codewords[++codeIndex]); + codeIndex++; + } + codeIndex = textCompaction(codewords, codeIndex, result); PDF417ResultMetadata resultMetadata = new PDF417ResultMetadata(); while (codeIndex < codewords[0]) { int code = codewords[codeIndex++]; @@ -123,12 +128,7 @@ final class DecodedBitStreamParser { codeIndex = numericCompaction(codewords, codeIndex, result); break; case ECI_CHARSET: - CharacterSetECI charsetECI = - CharacterSetECI.getCharacterSetECIByValue(codewords[codeIndex++]); - if (charsetECI == null) { - throw FormatException.getFormatInstance(); - } - encoding = charsetECI.getCharset(); + encoding = getECICharset(codewords[codeIndex++]); break; case ECI_GENERAL_PURPOSE: // Can't do anything with generic ECI; skip its 2 characters @@ -162,6 +162,15 @@ final class DecodedBitStreamParser { return decoderResult; } + private static Charset getECICharset(int eciValue) throws FormatException { + CharacterSetECI charsetECI = + CharacterSetECI.getCharacterSetECIByValue(eciValue); + if (charsetECI == null) { + throw FormatException.getFormatInstance(); + } + return charsetECI.getCharset(); + } + @SuppressWarnings("deprecation") static int decodeMacroBlock(int[] codewords, int codeIndex, PDF417ResultMetadata resultMetadata) throws FormatException { diff --git a/core/src/main/java/com/google/zxing/pdf417/encoder/PDF417.java b/core/src/main/java/com/google/zxing/pdf417/encoder/PDF417.java index 8469c9e77..a9d63dc23 100644 --- a/core/src/main/java/com/google/zxing/pdf417/encoder/PDF417.java +++ b/core/src/main/java/com/google/zxing/pdf417/encoder/PDF417.java @@ -643,10 +643,20 @@ public final class PDF417 { * @throws WriterException if the contents cannot be encoded in this format */ public void generateBarcodeLogic(String msg, int errorCorrectionLevel) throws WriterException { + generateBarcodeLogic(msg, errorCorrectionLevel, false); + } + + /** + * @param msg message to encode + * @param errorCorrectionLevel PDF417 error correction level to use + * @param autoECI automatically insert ECIs if needed + * @throws WriterException if the contents cannot be encoded in this format + */ + public void generateBarcodeLogic(String msg, int errorCorrectionLevel, boolean autoECI) throws WriterException { //1. step: High-level encoding int errorCorrectionCodeWords = PDF417ErrorCorrection.getErrorCorrectionCodewordCount(errorCorrectionLevel); - String highLevel = PDF417HighLevelEncoder.encodeHighLevel(msg, compaction, encoding); + String highLevel = PDF417HighLevelEncoder.encodeHighLevel(msg, compaction, encoding, autoECI); int sourceCodeWords = highLevel.length(); int[] dimension = determineDimensions(sourceCodeWords, errorCorrectionCodeWords); diff --git a/core/src/main/java/com/google/zxing/pdf417/encoder/PDF417HighLevelEncoder.java b/core/src/main/java/com/google/zxing/pdf417/encoder/PDF417HighLevelEncoder.java index e58b2132e..607dea9d7 100644 --- a/core/src/main/java/com/google/zxing/pdf417/encoder/PDF417HighLevelEncoder.java +++ b/core/src/main/java/com/google/zxing/pdf417/encoder/PDF417HighLevelEncoder.java @@ -22,6 +22,8 @@ package com.google.zxing.pdf417.encoder; import com.google.zxing.WriterException; import com.google.zxing.common.CharacterSetECI; +import com.google.zxing.common.ECIInput; +import com.google.zxing.common.MinimalECIInput; import java.math.BigInteger; import java.nio.charset.Charset; @@ -159,71 +161,103 @@ final class PDF417HighLevelEncoder { * @param compaction compaction mode to use * @param encoding character encoding used to encode in default or byte compaction * or {@code null} for default / not applicable + * @param autoECI encode input minimally using multiple ECIs if needed + * If autoECI encoding is specified and additionally {@code encoding} is specified, then the encoder + * will use the specified {@link Charset} for any character that can be encoded by it, regardless + * if a different encoding would lead to a more compact encoding. When no {@code encoding} is specified + * then charsets will be chosen so that the byte representation is minimal. * @return the encoded message (the char values range from 0 to 928) */ - static String encodeHighLevel(String msg, Compaction compaction, Charset encoding) throws WriterException { + static String encodeHighLevel(String msg, Compaction compaction, Charset encoding, boolean autoECI) + throws WriterException { //the codewords 0..928 are encoded as Unicode characters StringBuilder sb = new StringBuilder(msg.length()); - if (encoding == null) { - encoding = DEFAULT_ENCODING; - } else if (!DEFAULT_ENCODING.equals(encoding)) { - CharacterSetECI eci = CharacterSetECI.getCharacterSetECI(encoding); - if (eci != null) { - encodingECI(eci.getValue(), sb); + ECIInput input; + if (autoECI) { + input = new MinimalECIInput(msg, encoding, -1); + } else { + input = new NoECIInput(msg); + if (encoding == null) { + encoding = DEFAULT_ENCODING; + } else if (!DEFAULT_ENCODING.equals(encoding)) { + CharacterSetECI eci = CharacterSetECI.getCharacterSetECI(encoding); + if (eci != null) { + encodingECI(eci.getValue(), sb); + } } } - int len = msg.length(); + int len = input.length(); int p = 0; int textSubMode = SUBMODE_ALPHA; // User selected encoding mode switch (compaction) { case TEXT: - encodeText(msg, p, len, sb, textSubMode); + encodeText(input, p, len, sb, textSubMode); break; case BYTE: - byte[] msgBytes = msg.getBytes(encoding); - encodeBinary(msgBytes, p, msgBytes.length, BYTE_COMPACTION, sb); + if (autoECI) { + encodeMultiECIBinary(input, 0, input.length(), TEXT_COMPACTION, sb); + } else { + byte[] msgBytes = input.toString().getBytes(encoding); + encodeBinary(msgBytes, p, msgBytes.length, BYTE_COMPACTION, sb); + } break; case NUMERIC: sb.append((char) LATCH_TO_NUMERIC); - encodeNumeric(msg, p, len, sb); + encodeNumeric(input, p, len, sb); break; default: int encodingMode = TEXT_COMPACTION; //Default mode, see 4.4.2.1 while (p < len) { - int n = determineConsecutiveDigitCount(msg, p); + while (p < len && input.isECI(p)) { + encodingECI(input.getECIValue(p), sb); + p++; + } + if (p >= len) { + break; + } + int n = determineConsecutiveDigitCount(input, p); if (n >= 13) { sb.append((char) LATCH_TO_NUMERIC); encodingMode = NUMERIC_COMPACTION; textSubMode = SUBMODE_ALPHA; //Reset after latch - encodeNumeric(msg, p, n, sb); + encodeNumeric(input, p, n, sb); p += n; } else { - int t = determineConsecutiveTextCount(msg, p); + int t = determineConsecutiveTextCount(input, p); if (t >= 5 || n == len) { if (encodingMode != TEXT_COMPACTION) { sb.append((char) LATCH_TO_TEXT); encodingMode = TEXT_COMPACTION; textSubMode = SUBMODE_ALPHA; //start with submode alpha after latch } - textSubMode = encodeText(msg, p, t, sb, textSubMode); + textSubMode = encodeText(input, p, t, sb, textSubMode); p += t; } else { - int b = determineConsecutiveBinaryCount(msg, p, encoding); + int b = determineConsecutiveBinaryCount(input, p, autoECI ? null : encoding); if (b == 0) { b = 1; } - byte[] bytes = msg.substring(p, p + b).getBytes(encoding); - if (bytes.length == 1 && encodingMode == TEXT_COMPACTION) { + byte[] bytes = autoECI ? null : input.subSequence(p, p + b).toString().getBytes(encoding); + if (((bytes == null && b == 1) || (bytes != null && bytes.length == 1)) + && encodingMode == TEXT_COMPACTION) { //Switch for one byte (instead of latch) - encodeBinary(bytes, 0, 1, TEXT_COMPACTION, sb); + if (autoECI) { + encodeMultiECIBinary(input, p, 1, TEXT_COMPACTION, sb); + } else { + encodeBinary(bytes, 0, 1, TEXT_COMPACTION, sb); + } } else { //Mode latch performed by encodeBinary() - encodeBinary(bytes, 0, bytes.length, encodingMode, sb); + if (autoECI) { + encodeMultiECIBinary(input, p, p + b, encodingMode, sb); + } else { + encodeBinary(bytes, 0, bytes.length, encodingMode, sb); + } encodingMode = BYTE_COMPACTION; textSubMode = SUBMODE_ALPHA; //Reset after latch } @@ -241,109 +275,113 @@ final class PDF417HighLevelEncoder { * Encode parts of the message using Text Compaction as described in ISO/IEC 15438:2001(E), * chapter 4.4.2. * - * @param msg the message + * @param input the input * @param startpos the start position within the message * @param count the number of characters to encode * @param sb receives the encoded codewords * @param initialSubmode should normally be SUBMODE_ALPHA * @return the text submode in which this method ends */ - private static int encodeText(CharSequence msg, + private static int encodeText(ECIInput input, int startpos, int count, StringBuilder sb, - int initialSubmode) { + int initialSubmode) throws WriterException { StringBuilder tmp = new StringBuilder(count); int submode = initialSubmode; int idx = 0; while (true) { - char ch = msg.charAt(startpos + idx); - switch (submode) { - case SUBMODE_ALPHA: - if (isAlphaUpper(ch)) { - if (ch == ' ') { - tmp.append((char) 26); //space + if (input.isECI(startpos + idx)) { + encodingECI(input.getECIValue(startpos + idx), sb); + idx++; + } else { + char ch = input.charAt(startpos + idx); + switch (submode) { + case SUBMODE_ALPHA: + if (isAlphaUpper(ch)) { + if (ch == ' ') { + tmp.append((char) 26); //space + } else { + tmp.append((char) (ch - 65)); + } } else { - tmp.append((char) (ch - 65)); + if (isAlphaLower(ch)) { + submode = SUBMODE_LOWER; + tmp.append((char) 27); //ll + continue; + } else if (isMixed(ch)) { + submode = SUBMODE_MIXED; + tmp.append((char) 28); //ml + continue; + } else { + tmp.append((char) 29); //ps + tmp.append((char) PUNCTUATION[ch]); + break; + } } - } else { + break; + case SUBMODE_LOWER: if (isAlphaLower(ch)) { - submode = SUBMODE_LOWER; - tmp.append((char) 27); //ll - continue; - } else if (isMixed(ch)) { - submode = SUBMODE_MIXED; - tmp.append((char) 28); //ml - continue; + if (ch == ' ') { + tmp.append((char) 26); //space + } else { + tmp.append((char) (ch - 97)); + } } else { - tmp.append((char) 29); //ps - tmp.append((char) PUNCTUATION[ch]); - break; + if (isAlphaUpper(ch)) { + tmp.append((char) 27); //as + tmp.append((char) (ch - 65)); + //space cannot happen here, it is also in "Lower" + break; + } else if (isMixed(ch)) { + submode = SUBMODE_MIXED; + tmp.append((char) 28); //ml + continue; + } else { + tmp.append((char) 29); //ps + tmp.append((char) PUNCTUATION[ch]); + break; + } } - } - break; - case SUBMODE_LOWER: - if (isAlphaLower(ch)) { - if (ch == ' ') { - tmp.append((char) 26); //space + break; + case SUBMODE_MIXED: + if (isMixed(ch)) { + tmp.append((char) MIXED[ch]); } else { - tmp.append((char) (ch - 97)); + if (isAlphaUpper(ch)) { + submode = SUBMODE_ALPHA; + tmp.append((char) 28); //al + continue; + } else if (isAlphaLower(ch)) { + submode = SUBMODE_LOWER; + tmp.append((char) 27); //ll + continue; + } else { + if (startpos + idx + 1 < count) { + if (!input.isECI(startpos + idx + 1) && isPunctuation(input.charAt(startpos + idx + 1))) { + submode = SUBMODE_PUNCTUATION; + tmp.append((char) 25); //pl + continue; + } + } + tmp.append((char) 29); //ps + tmp.append((char) PUNCTUATION[ch]); + } } - } else { - if (isAlphaUpper(ch)) { - tmp.append((char) 27); //as - tmp.append((char) (ch - 65)); - //space cannot happen here, it is also in "Lower" - break; - } else if (isMixed(ch)) { - submode = SUBMODE_MIXED; - tmp.append((char) 28); //ml - continue; - } else { - tmp.append((char) 29); //ps + break; + default: //SUBMODE_PUNCTUATION + if (isPunctuation(ch)) { tmp.append((char) PUNCTUATION[ch]); - break; - } - } - break; - case SUBMODE_MIXED: - if (isMixed(ch)) { - tmp.append((char) MIXED[ch]); - } else { - if (isAlphaUpper(ch)) { + } else { submode = SUBMODE_ALPHA; - tmp.append((char) 28); //al - continue; - } else if (isAlphaLower(ch)) { - submode = SUBMODE_LOWER; - tmp.append((char) 27); //ll + tmp.append((char) 29); //al continue; - } else { - if (startpos + idx + 1 < count) { - char next = msg.charAt(startpos + idx + 1); - if (isPunctuation(next)) { - submode = SUBMODE_PUNCTUATION; - tmp.append((char) 25); //pl - continue; - } - } - tmp.append((char) 29); //ps - tmp.append((char) PUNCTUATION[ch]); } - } + } + idx++; + if (idx >= count) { break; - default: //SUBMODE_PUNCTUATION - if (isPunctuation(ch)) { - tmp.append((char) PUNCTUATION[ch]); - } else { - submode = SUBMODE_ALPHA; - tmp.append((char) 29); //al - continue; - } - } - idx++; - if (idx >= count) { - break; + } } } char h = 0; @@ -364,6 +402,56 @@ final class PDF417HighLevelEncoder { } /** + * Encode all of the message using Byte Compaction as described in ISO/IEC 15438:2001(E) + * + * @param input the input + * @param startpos the start position within the message + * @param count the number of bytes to encode + * @param startmode the mode from which this method starts + * @param sb receives the encoded codewords + */ + private static void encodeMultiECIBinary(ECIInput input, + int startpos, + int count, + int startmode, + StringBuilder sb) throws WriterException { + final int end = Math.min(startpos + count, input.length()); + int localStart = startpos; + while (true) { + //encode all leading ECIs and advance localStart + while (localStart < end && input.isECI(localStart)) { + encodingECI(input.getECIValue(localStart), sb); + localStart++; + } + int localEnd = localStart; + //advance end until before the next ECI + while (localEnd < end && !input.isECI(localEnd)) { + localEnd++; + } + + final int localCount = localEnd - localStart; + if (localCount <= 0) { + //done + break; + } else { + //encode the segment + encodeBinary(subBytes(input, localStart, localEnd), + 0, localCount, localStart == startpos ? startmode : BYTE_COMPACTION, sb); + localStart = localEnd + 1; + } + } + } + + static byte[] subBytes(ECIInput input, int start, int end) { + final int count = end - start; + byte[] result = new byte[count]; + for (int i = start; i < end; i++) { + result[i - start] = (byte) (input.charAt(i) & 0xff); + } + return result; + } + + /** * Encode parts of the message using Byte Compaction as described in ISO/IEC 15438:2001(E), * chapter 4.4.3. The Unicode characters will be converted to binary using the cp437 * codepage. @@ -416,7 +504,7 @@ final class PDF417HighLevelEncoder { } } - private static void encodeNumeric(String msg, int startpos, int count, StringBuilder sb) { + private static void encodeNumeric(ECIInput input, int startpos, int count, StringBuilder sb) { int idx = 0; StringBuilder tmp = new StringBuilder(count / 3 + 1); BigInteger num900 = BigInteger.valueOf(900); @@ -424,7 +512,7 @@ final class PDF417HighLevelEncoder { while (idx < count) { tmp.setLength(0); int len = Math.min(44, count - idx); - String part = '1' + msg.substring(startpos + idx, startpos + idx + len); + String part = "1" + input.subSequence(startpos + idx, startpos + idx + len); BigInteger bigint = new BigInteger(part); do { tmp.append((char) bigint.mod(num900).intValue()); @@ -467,22 +555,18 @@ final class PDF417HighLevelEncoder { /** * Determines the number of consecutive characters that are encodable using numeric compaction. * - * @param msg the message - * @param startpos the start position within the message + * @param input the input + * @param startpos the start position within the input * @return the requested character count */ - private static int determineConsecutiveDigitCount(CharSequence msg, int startpos) { + private static int determineConsecutiveDigitCount(ECIInput input, int startpos) { int count = 0; - int len = msg.length(); + final int len = input.length(); int idx = startpos; if (idx < len) { - char ch = msg.charAt(idx); - while (isDigit(ch) && idx < len) { + while (idx < len && !input.isECI(idx) && isDigit(input.charAt(idx))) { count++; idx++; - if (idx < len) { - ch = msg.charAt(idx); - } } } return count; @@ -491,22 +575,18 @@ final class PDF417HighLevelEncoder { /** * Determines the number of consecutive characters that are encodable using text compaction. * - * @param msg the message - * @param startpos the start position within the message + * @param input the input + * @param startpos the start position within the input * @return the requested character count */ - private static int determineConsecutiveTextCount(CharSequence msg, int startpos) { - int len = msg.length(); + private static int determineConsecutiveTextCount(ECIInput input, int startpos) { + final int len = input.length(); int idx = startpos; while (idx < len) { - char ch = msg.charAt(idx); int numericCount = 0; - while (numericCount < 13 && isDigit(ch) && idx < len) { + while (numericCount < 13 && idx < len && !input.isECI(idx) && isDigit(input.charAt(idx))) { numericCount++; idx++; - if (idx < len) { - ch = msg.charAt(idx); - } } if (numericCount >= 13) { return idx - startpos - numericCount; @@ -515,10 +595,9 @@ final class PDF417HighLevelEncoder { //Heuristic: All text-encodable chars or digits are binary encodable continue; } - ch = msg.charAt(idx); //Check if character is encodable - if (!isText(ch)) { + if (input.isECI(idx) || !isText(input.charAt(idx))) { break; } idx++; @@ -529,35 +608,35 @@ final class PDF417HighLevelEncoder { /** * Determines the number of consecutive characters that are encodable using binary compaction. * - * @param msg the message + * @param input the input * @param startpos the start position within the message * @param encoding the charset used to convert the message to a byte array * @return the requested character count */ - private static int determineConsecutiveBinaryCount(String msg, int startpos, Charset encoding) + private static int determineConsecutiveBinaryCount(ECIInput input, int startpos, Charset encoding) throws WriterException { - CharsetEncoder encoder = encoding.newEncoder(); - int len = msg.length(); + CharsetEncoder encoder = encoding == null ? null : encoding.newEncoder(); + int len = input.length(); int idx = startpos; while (idx < len) { - char ch = msg.charAt(idx); int numericCount = 0; - while (numericCount < 13 && isDigit(ch)) { + int i = idx; + while (numericCount < 13 && !input.isECI(i) && isDigit(input.charAt(i))) { numericCount++; //textCount++; - int i = idx + numericCount; + i = idx + numericCount; if (i >= len) { break; } - ch = msg.charAt(i); } if (numericCount >= 13) { return idx - startpos; } - ch = msg.charAt(idx); - if (!encoder.canEncode(ch)) { + if (encoder != null && !encoder.canEncode(input.charAt(idx))) { + assert input instanceof NoECIInput; + char ch = input.charAt(idx); throw new WriterException("Non-encodable character detected: " + ch + " (Unicode: " + (int) ch + ')'); } idx++; @@ -581,4 +660,40 @@ final class PDF417HighLevelEncoder { } } + private static final class NoECIInput implements ECIInput { + + String input; + + private NoECIInput(String input) { + this.input = input; + } + + public int length() { + return input.length(); + } + + public char charAt(int index) { + return input.charAt(index); + } + + public boolean isECI(int index) { + return false; + } + + public int getECIValue(int index) { + return -1; + } + + public boolean haveNCharacters(int index, int n) { + return index + n <= input.length(); + } + + public CharSequence subSequence(int start, int end) { + return input.subSequence(start, end); + } + + public String toString() { + return input; + } + } } diff --git a/core/src/main/java/com/google/zxing/pdf417/encoder/PDF417HighLevelEncoderTestAdapter.java b/core/src/main/java/com/google/zxing/pdf417/encoder/PDF417HighLevelEncoderTestAdapter.java index ac6092a52..6ba7992d6 100644 --- a/core/src/main/java/com/google/zxing/pdf417/encoder/PDF417HighLevelEncoderTestAdapter.java +++ b/core/src/main/java/com/google/zxing/pdf417/encoder/PDF417HighLevelEncoderTestAdapter.java @@ -29,7 +29,10 @@ public final class PDF417HighLevelEncoderTestAdapter { private PDF417HighLevelEncoderTestAdapter() { } - public static String encodeHighLevel(String msg, Compaction compaction, Charset encoding) throws WriterException { - return PDF417HighLevelEncoder.encodeHighLevel(msg, compaction, encoding); + public static String encodeHighLevel(String msg, + Compaction compaction, + Charset encoding, + boolean autoECI) throws WriterException { + return PDF417HighLevelEncoder.encodeHighLevel(msg, compaction, encoding, autoECI); } } |
