mirror of
https://github.com/zxing/zxing.git
synced 2024-11-09 20:44:03 -08:00
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
This commit is contained in:
parent
4bd257e8c5
commit
92854d4a55
|
@ -103,6 +103,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.
|
||||
|
|
107
core/src/main/java/com/google/zxing/common/ECIInput.java
Executable file
107
core/src/main/java/com/google/zxing/common/ECIInput.java
Executable file
|
@ -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);
|
||||
}
|
325
core/src/main/java/com/google/zxing/common/MinimalECIInput.java
Executable file
325
core/src/main/java/com/google/zxing/common/MinimalECIInput.java
Executable file
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
} else {
|
||||
tmp.append((char) (ch - 65));
|
||||
}
|
||||
} else {
|
||||
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;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case SUBMODE_LOWER:
|
||||
if (isAlphaLower(ch)) {
|
||||
if (ch == ' ') {
|
||||
tmp.append((char) 26); //space
|
||||
} else {
|
||||
tmp.append((char) (ch - 97));
|
||||
}
|
||||
} else {
|
||||
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)) {
|
||||
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_MIXED:
|
||||
if (isMixed(ch)) {
|
||||
tmp.append((char) MIXED[ch]);
|
||||
} else {
|
||||
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) {
|
||||
char next = msg.charAt(startpos + idx + 1);
|
||||
if (isPunctuation(next)) {
|
||||
submode = SUBMODE_PUNCTUATION;
|
||||
tmp.append((char) 25); //pl
|
||||
continue;
|
||||
}
|
||||
if (ch == ' ') {
|
||||
tmp.append((char) 26); //space
|
||||
} else {
|
||||
tmp.append((char) (ch - 65));
|
||||
}
|
||||
} else {
|
||||
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;
|
||||
}
|
||||
tmp.append((char) 29); //ps
|
||||
tmp.append((char) PUNCTUATION[ch]);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case SUBMODE_LOWER:
|
||||
if (isAlphaLower(ch)) {
|
||||
if (ch == ' ') {
|
||||
tmp.append((char) 26); //space
|
||||
} else {
|
||||
tmp.append((char) (ch - 97));
|
||||
}
|
||||
} 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
|
||||
tmp.append((char) PUNCTUATION[ch]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case SUBMODE_MIXED:
|
||||
if (isMixed(ch)) {
|
||||
tmp.append((char) MIXED[ch]);
|
||||
} else {
|
||||
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]);
|
||||
}
|
||||
}
|
||||
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;
|
||||
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;
|
||||
|
@ -363,6 +401,56 @@ final class PDF417HighLevelEncoder {
|
|||
return submode;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,6 +25,7 @@ import com.google.zxing.pdf417.encoder.PDF417HighLevelEncoderTestAdapter;
|
|||
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Random;
|
||||
|
||||
|
@ -283,21 +284,76 @@ public class PDF417DecoderTestCase extends Assert {
|
|||
assertEquals(4190044, total);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testECIEnglishHiragana() throws Exception {
|
||||
//multi ECI UTF-8, UTF-16 and ISO-8859-1
|
||||
performECITest(new char[] {'a', '1', '\u3040'}, new float[] {20f, 1f, 10f}, 102583, 110914);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testECIEnglishKatakana() throws Exception {
|
||||
//multi ECI UTF-8, UTF-16 and ISO-8859-1
|
||||
performECITest(new char[] {'a', '1', '\u30a0'}, new float[] {20f, 1f, 10f}, 104691, 110914);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testECIEnglishHalfWidthKatakana() throws Exception {
|
||||
//single ECI
|
||||
performECITest(new char[] {'a', '1', '\uff80'}, new float[] {20f, 1f, 10f}, 80463, 110914);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testECIEnglishChinese() throws Exception {
|
||||
//single ECI
|
||||
performECITest(new char[] {'a', '1', '\u4e00'}, new float[] {20f, 1f, 10f}, 95643, 110914);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testECIGermanCyrillic() throws Exception {
|
||||
//single ECI since the German Umlaut is in ISO-8859-1
|
||||
performECITest(new char[] {'a', '1', '\u00c4', '\u042f'}, new float[] {20f, 1f, 1f, 10f}, 80529, 96007);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testECIEnglishCzechCyrillic1() throws Exception {
|
||||
//multi ECI between ISO-8859-2 and ISO-8859-5
|
||||
performECITest(new char[] {'a', '1', '\u010c', '\u042f'}, new float[] {10f, 1f, 10f, 10f}, 91482, 124525);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testECIEnglishCzechCyrillic2() throws Exception {
|
||||
//multi ECI between ISO-8859-2 and ISO-8859-5
|
||||
performECITest(new char[] {'a', '1', '\u010c', '\u042f'}, new float[] {40f, 1f, 10f, 10f}, 79331, 88236);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testECIEnglishArabicCyrillic() throws Exception {
|
||||
//multi ECI between UTF-8 (ISO-8859-6 is excluded in CharacterSetECI) and ISO-8859-5
|
||||
performECITest(new char[] {'a', '1', '\u0620', '\u042f'}, new float[] {10f, 1f, 10f, 10f}, 111508, 124525);
|
||||
}
|
||||
|
||||
private static void encodeDecode(String input, int expectedLength) throws WriterException, FormatException {
|
||||
assertEquals(expectedLength, encodeDecode(input));
|
||||
}
|
||||
|
||||
private static int encodeDecode(String input) throws WriterException, FormatException {
|
||||
String s = PDF417HighLevelEncoderTestAdapter.encodeHighLevel(input, Compaction.AUTO, null);
|
||||
int[] codewords = new int[s.length() + 1];
|
||||
codewords[0] = codewords.length;
|
||||
for (int i = 1; i < codewords.length; i++) {
|
||||
codewords[i] = s.charAt(i - 1);
|
||||
}
|
||||
DecoderResult result = DecodedBitStreamParser.decode(codewords, "0");
|
||||
return encodeDecode(input, null, false, true);
|
||||
}
|
||||
|
||||
assertEquals(input, result.getText());
|
||||
return codewords.length;
|
||||
private static int encodeDecode(String input, Charset charset, boolean autoECI, boolean decode)
|
||||
throws WriterException, FormatException {
|
||||
String s = PDF417HighLevelEncoderTestAdapter.encodeHighLevel(input, Compaction.AUTO, charset, autoECI);
|
||||
if (decode) {
|
||||
int[] codewords = new int[s.length() + 1];
|
||||
codewords[0] = codewords.length;
|
||||
for (int i = 1; i < codewords.length; i++) {
|
||||
codewords[i] = s.charAt(i - 1);
|
||||
}
|
||||
DecoderResult result = DecodedBitStreamParser.decode(codewords, "0");
|
||||
|
||||
assertEquals(input, result.getText());
|
||||
}
|
||||
return s.length() + 1;
|
||||
}
|
||||
|
||||
private static int getEndIndex(int length, char[] chars) {
|
||||
|
@ -338,4 +394,61 @@ public class PDF417DecoderTestCase extends Assert {
|
|||
}
|
||||
}
|
||||
|
||||
private static void performECITest(char[] chars,
|
||||
float[] weights,
|
||||
int expectedMinLength,
|
||||
int expectedUTFLength) throws WriterException, FormatException {
|
||||
Random random = new Random(0);
|
||||
int minLength = 0;
|
||||
int utfLength = 0;
|
||||
for (int i = 0; i < 1000; i++) {
|
||||
String s = generateText(random, 100, chars, weights);
|
||||
minLength += encodeDecode(s, null, true, false);
|
||||
// TODO: Use this instead when the decoder supports multi ECI input
|
||||
//minLength += encodeDecode(s, null, true, true);
|
||||
utfLength += encodeDecode(s, StandardCharsets.UTF_8, false, true);
|
||||
}
|
||||
assertEquals(expectedMinLength, minLength);
|
||||
assertEquals(expectedUTFLength, utfLength);
|
||||
}
|
||||
|
||||
private static String generateText(Random random, int maxWidth, char[] chars, float[] weights) {
|
||||
StringBuilder result = new StringBuilder();
|
||||
final int maxWordWidth = 7;
|
||||
float total = 0;
|
||||
for (int i = 0; i < weights.length; i++) {
|
||||
total += weights[i];
|
||||
}
|
||||
for (int i = 0; i < weights.length; i++) {
|
||||
weights[i] /= total;
|
||||
}
|
||||
int cnt = 0;
|
||||
do {
|
||||
float maxValue = 0;
|
||||
int maxIndex = 0;
|
||||
for (int j = 0; j < weights.length; j++) {
|
||||
float value = random.nextFloat() * weights[j];
|
||||
if (value > maxValue) {
|
||||
maxValue = value;
|
||||
maxIndex = j;
|
||||
}
|
||||
}
|
||||
final float wordLength = maxWordWidth * random.nextFloat();
|
||||
if (wordLength > 0 && result.length() > 0) {
|
||||
result.append(' ');
|
||||
}
|
||||
for (int j = 0; j < wordLength; j++) {
|
||||
char c = chars[maxIndex];
|
||||
if (j == 0 && c >= 'a' && c <= 'z' && random.nextBoolean()) {
|
||||
c = (char) (c - 'a' + 'A');
|
||||
}
|
||||
result.append(c);
|
||||
}
|
||||
if (cnt % 2 != 0 && random.nextBoolean()) {
|
||||
result.append('.');
|
||||
}
|
||||
cnt++;
|
||||
} while (result.length() < maxWidth - maxWordWidth);
|
||||
return result.toString();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -29,7 +29,7 @@ public final class PDF417EncoderTestCase extends Assert {
|
|||
@Test
|
||||
public void testEncodeAuto() throws Exception {
|
||||
String encoded = PDF417HighLevelEncoder.encodeHighLevel(
|
||||
"ABCD", Compaction.AUTO, StandardCharsets.UTF_8);
|
||||
"ABCD", Compaction.AUTO, StandardCharsets.UTF_8, false);
|
||||
assertEquals("\u039f\u001A\u0385ABCD", encoded);
|
||||
}
|
||||
|
||||
|
@ -37,33 +37,33 @@ public final class PDF417EncoderTestCase extends Assert {
|
|||
public void testEncodeAutoWithSpecialChars() throws Exception {
|
||||
// Just check if this does not throw an exception
|
||||
PDF417HighLevelEncoder.encodeHighLevel(
|
||||
"1%§s ?aG$", Compaction.AUTO, StandardCharsets.UTF_8);
|
||||
"1%§s ?aG$", Compaction.AUTO, StandardCharsets.UTF_8, false);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testEncodeIso88591WithSpecialChars() throws Exception {
|
||||
// Just check if this does not throw an exception
|
||||
PDF417HighLevelEncoder.encodeHighLevel("asdfg§asd", Compaction.AUTO, StandardCharsets.ISO_8859_1);
|
||||
PDF417HighLevelEncoder.encodeHighLevel("asdfg§asd", Compaction.AUTO, StandardCharsets.ISO_8859_1, false);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testEncodeText() throws Exception {
|
||||
String encoded = PDF417HighLevelEncoder.encodeHighLevel(
|
||||
"ABCD", Compaction.TEXT, StandardCharsets.UTF_8);
|
||||
"ABCD", Compaction.TEXT, StandardCharsets.UTF_8, false);
|
||||
assertEquals("Ο\u001A\u0001?", encoded);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testEncodeNumeric() throws Exception {
|
||||
String encoded = PDF417HighLevelEncoder.encodeHighLevel(
|
||||
"1234", Compaction.NUMERIC, StandardCharsets.UTF_8);
|
||||
"1234", Compaction.NUMERIC, StandardCharsets.UTF_8, false);
|
||||
assertEquals("\u039f\u001A\u0386\f\u01b2", encoded);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testEncodeByte() throws Exception {
|
||||
String encoded = PDF417HighLevelEncoder.encodeHighLevel(
|
||||
"abcd", Compaction.BYTE, StandardCharsets.UTF_8);
|
||||
"abcd", Compaction.BYTE, StandardCharsets.UTF_8, false);
|
||||
assertEquals("\u039f\u001A\u0385abcd", encoded);
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue