diff --git a/src/main/java/uk/org/okapibarcode/backend/Pdf417.java b/src/main/java/uk/org/okapibarcode/backend/Pdf417.java
index 350c75aa..9cc2e87b 100644
--- a/src/main/java/uk/org/okapibarcode/backend/Pdf417.java
+++ b/src/main/java/uk/org/okapibarcode/backend/Pdf417.java
@@ -68,6 +68,7 @@ private enum EncodingMode {
private boolean structuredAppendIncludeSegmentCount;
private static final int MAX_NUMERIC_COMPACTION_BLOCK_SIZE = 44;
+ protected static final int MAX_STRUCTURED_APPEND_TOTAL = 99_999;
private static final int[] COEFRS = {
/* k = 2 */
@@ -613,7 +614,7 @@ public void setVariant(int variant) {
* @param position the position of this PDF417 symbol in the structured append series
*/
public void setStructuredAppendPosition(int position) {
- if (position < 1 || position > 99_999) {
+ if (position < 1 || position > MAX_STRUCTURED_APPEND_TOTAL) {
throw new IllegalArgumentException("Invalid PDF417 structured append position: " + position);
}
this.structuredAppendPosition = position;
@@ -638,7 +639,7 @@ public int getStructuredAppendPosition() {
* @param total the total number of PDF417 symbols in the structured append series
*/
public void setStructuredAppendTotal(int total) {
- if (total < 1 || total > 99_999) {
+ if (total < 1 || total > MAX_STRUCTURED_APPEND_TOTAL) {
throw new IllegalArgumentException("Invalid PDF417 structured append total: " + total);
}
this.structuredAppendTotal = total;
diff --git a/src/main/java/uk/org/okapibarcode/backend/Pdf417AutoStructuredAppend.java b/src/main/java/uk/org/okapibarcode/backend/Pdf417AutoStructuredAppend.java
new file mode 100644
index 00000000..da5ad44c
--- /dev/null
+++ b/src/main/java/uk/org/okapibarcode/backend/Pdf417AutoStructuredAppend.java
@@ -0,0 +1,179 @@
+/*
+ * Copyright 2024 Daniel Gredler
+ *
+ * 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 uk.org.okapibarcode.backend;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import static java.nio.charset.StandardCharsets.ISO_8859_1;
+import static uk.org.okapibarcode.backend.Pdf417.MAX_STRUCTURED_APPEND_TOTAL;
+
+/**
+ * @author Urs Wolfer
+ */
+public class Pdf417AutoStructuredAppend {
+
+ /**
+ * Splits up the data into a series of structured append PDF417 symbols which include
+ * segment macro data total
and position
.
+ * Input data will be assumed to be of the type set by {@link Pdf417#setDataType(Symbol.DataType)}.
+ *
+ * @param data the data to encode
+ * @param template the PDF417 symbol template which will be used for all created
+ * symbols. The following properties will be ignored from the template:
+ *
createStructuredAppendSymbols(String data, Pdf417 template) {
+ return createStructuredAppendSymbols(data, template, false);
+ }
+
+ /**
+ * Overloaded method of {@link #createStructuredAppendSymbols(String, Pdf417)} which
+ * accepts binary data and passes it to {@link Pdf417#setContent(byte[])}.
+ *
+ * NOTE:See linked method above for details.
+ */
+ public static List createStructuredAppendSymbols(byte[] data, Pdf417 template) {
+ return createStructuredAppendSymbols(new String(data, ISO_8859_1), template, true);
+ }
+
+ private static List createStructuredAppendSymbols(String data, Pdf417 template, boolean forceByteCompaction) {
+ List contentList = calculateStructuredAppendContent(data, template, forceByteCompaction);
+ return plotStructuredAppendSymbols(contentList, template, forceByteCompaction);
+ }
+
+ private static List calculateStructuredAppendContent(String content, Pdf417 template, boolean forceByteCompaction) {
+ List contentList = new ArrayList<>();
+ String remainingContent = content;
+ while (!remainingContent.isEmpty()) {
+ Pdf417 symbol = new Pdf417() {
+ @Override
+ protected void plotSymbol() {
+ // expensive plotting is not required in this case
+ }
+ };
+ clone(template, symbol);
+
+ symbol.setStructuredAppendPosition(contentList.size() + 1);
+ symbol.setStructuredAppendTotal(MAX_STRUCTURED_APPEND_TOTAL);
+
+ String currentContent = remainingContent;
+ remainingContent = setContentAndGetRemainingWithBinarySearch(currentContent, symbol, forceByteCompaction);
+ contentList.add(currentContent.replace(remainingContent, ""));
+ }
+ return contentList;
+ }
+
+ private static List plotStructuredAppendSymbols(List contentList, Pdf417 template, boolean forceByteCompaction) {
+ int structuredAppendTotal = contentList.size();
+ List symbols = new ArrayList<>(structuredAppendTotal);
+ for (int i = 0; i < structuredAppendTotal; i++) {
+ String content = contentList.get(i);
+ Pdf417 symbol = new Pdf417();
+ symbols.add(symbol);
+ clone(template, symbol);
+
+ symbol.setStructuredAppendPosition(i + 1);
+ symbol.setStructuredAppendTotal(structuredAppendTotal);
+
+ setContent(symbol, content, forceByteCompaction);
+ }
+ return symbols;
+ }
+
+ private static void clone(Pdf417 template, Pdf417 target) {
+ target.setFontName(template.getFontName());
+ target.setFontSize(template.getFontSize());
+ target.setDataType(template.getDataType());
+ target.setEmptyContentAllowed(template.getEmptyContentAllowed());
+ target.setHumanReadableAlignment(template.getHumanReadableAlignment());
+ target.setHumanReadableLocation(template.getHumanReadableLocation());
+ target.setModuleWidth(template.getModuleWidth());
+ target.setQuietZoneHorizontal(template.getQuietZoneHorizontal());
+ target.setQuietZoneVertical(template.getQuietZoneVertical());
+ target.setReaderInit(template.getReaderInit());
+ target.setDataColumns(template.getDataColumns());
+ target.setRows(template.getRows());
+ target.setPreferredEccLevel(template.getPreferredEccLevel());
+ target.setStructuredAppendFileId(template.getStructuredAppendFileId());
+ target.setStructuredAppendFileName(template.getStructuredAppendFileName());
+ target.setBarHeight(template.getBarHeight());
+ target.setMode(template.getMode());
+ target.setStructuredAppendIncludeSegmentCount(template.getStructuredAppendIncludeSegmentCount());
+ }
+
+ private static String setContentAndGetRemainingWithBinarySearch(String remainingContent, Pdf417 symbol, boolean forceByteCompaction) {
+ int low = 0;
+ int high = remainingContent.length();
+
+ while (low <= high) {
+ int mid = low + high >>> 1;
+ SetContentStatus result = trySetContent(remainingContent, symbol, mid, forceByteCompaction);
+ if (result == SetContentStatus.TRY_MORE) {
+ low = mid + 1;
+ } else {
+ if (result == SetContentStatus.MAX_REACHED) {
+ return remainingContent.substring(mid);
+ }
+ high = mid - 1;
+ }
+ }
+
+ throw new RuntimeException("Failed to set content, this is a bug.");
+ }
+
+ private static SetContentStatus trySetContent(String remainingContent, Pdf417 symbol, int lengthToTry, boolean forceByteCompaction) {
+ int remainingContentLength = remainingContent.length();
+ int currentLength = Math.min(lengthToTry, remainingContentLength);
+ String currentContent = remainingContent.substring(0, currentLength);
+ try {
+ setContent(symbol, currentContent, forceByteCompaction);
+ if (currentLength == remainingContentLength) {
+ return SetContentStatus.MAX_REACHED;
+ }
+ currentContent = remainingContent.substring(0, currentLength + 1);
+ try {
+ setContent(symbol, currentContent, forceByteCompaction);
+ return SetContentStatus.TRY_MORE;
+ } catch (OkapiInputException e) {
+ return SetContentStatus.MAX_REACHED;
+ }
+ } catch (OkapiInputException e) {
+ return SetContentStatus.TOO_BIG;
+ }
+ }
+
+ private static void setContent(Pdf417 symbol, String currentContent, boolean forceByteCompaction) {
+ if (forceByteCompaction) {
+ symbol.setContent(currentContent.getBytes(ISO_8859_1));
+ } else {
+ symbol.setContent(currentContent);
+ }
+ }
+
+ private enum SetContentStatus {
+ TOO_BIG,
+ TRY_MORE,
+ MAX_REACHED
+ }
+}
diff --git a/src/test/java/uk/org/okapibarcode/backend/Pdf417AutoStructuredAppendTest.java b/src/test/java/uk/org/okapibarcode/backend/Pdf417AutoStructuredAppendTest.java
new file mode 100644
index 00000000..49b289b6
--- /dev/null
+++ b/src/test/java/uk/org/okapibarcode/backend/Pdf417AutoStructuredAppendTest.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright 2024 Daniel Gredler
+ *
+ * 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 uk.org.okapibarcode.backend;
+
+import com.google.zxing.BinaryBitmap;
+import com.google.zxing.ChecksumException;
+import com.google.zxing.FormatException;
+import com.google.zxing.LuminanceSource;
+import com.google.zxing.NotFoundException;
+import com.google.zxing.Result;
+import com.google.zxing.client.j2se.BufferedImageLuminanceSource;
+import com.google.zxing.common.HybridBinarizer;
+import com.google.zxing.pdf417.PDF417Reader;
+import org.junit.jupiter.api.Test;
+import uk.org.okapibarcode.output.Java2DRenderer;
+
+import java.awt.Graphics2D;
+import java.awt.image.BufferedImage;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.util.List;
+
+import static java.awt.image.BufferedImage.TYPE_BYTE_BINARY;
+import static java.nio.charset.StandardCharsets.ISO_8859_1;
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static uk.org.okapibarcode.graphics.Color.BLACK;
+import static uk.org.okapibarcode.graphics.Color.WHITE;
+
+/**
+ * @author Urs Wolfer
+ */
+class Pdf417AutoStructuredAppendTest {
+
+ @Test
+ public void testCreateStructuredAppendSymbols() throws Exception {
+ byte[] bytes = new byte[256];
+ for (int i = 0; i < bytes.length; i++) {
+ bytes[i] = (byte) i; // all possible byte values
+ }
+
+ Pdf417 symbolTemplate = new Pdf417();
+ symbolTemplate.setPreferredEccLevel(4);
+ symbolTemplate.setBarHeight(1);
+ symbolTemplate.setRows(12);
+ symbolTemplate.setDataColumns(13);
+ symbolTemplate.setStructuredAppendIncludeSegmentCount(true);
+
+ List symbols = Pdf417AutoStructuredAppend.createStructuredAppendSymbols(new String(bytes, ISO_8859_1), symbolTemplate);
+ assertions(symbols, bytes);
+
+ List symbolsByteEncoded = Pdf417AutoStructuredAppend.createStructuredAppendSymbols(bytes, symbolTemplate);
+ assertions(symbolsByteEncoded, bytes);
+ }
+
+ private static void assertions(List symbols, byte[] bytes) throws IOException, NotFoundException, FormatException, ChecksumException {
+ assertEquals(2, symbols.size());
+
+ try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
+ for (Pdf417 barcode : symbols) {
+ assertEquals(4, barcode.getPreferredEccLevel());
+ assertEquals(1, barcode.getBarHeight());
+ assertEquals(12, barcode.getRows());
+ assertEquals(13, barcode.getDataColumns());
+
+ BufferedImage img = new BufferedImage(barcode.getWidth(), barcode.getHeight(), TYPE_BYTE_BINARY);
+ Graphics2D g2d = img.createGraphics();
+ Java2DRenderer renderer = new Java2DRenderer(g2d, 1, WHITE, BLACK);
+ renderer.render(barcode);
+ g2d.dispose();
+
+ LuminanceSource source = new BufferedImageLuminanceSource(img);
+ BinaryBitmap bitmap = new BinaryBitmap(new HybridBinarizer(source));
+ PDF417Reader reader = new PDF417Reader();
+ Result result = reader.decode(bitmap);
+
+ byte[] output = result.getText().getBytes(ISO_8859_1);
+ outputStream.write(output);
+
+ }
+ assertArrayEquals(bytes, outputStream.toByteArray());
+ }
+ }
+}