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: + * + * @throws OkapiException if no data or data is invalid + */ + public static List 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()); + } + } +}