From 5e139e3433e9ce0e80253fbcaa132be2ea3b6c28 Mon Sep 17 00:00:00 2001 From: Urs Wolfer Date: Sat, 17 Feb 2024 20:52:34 +0100 Subject: [PATCH] PDF417: Automated structured append Resolves #92 --- .../uk/org/okapibarcode/backend/Pdf417.java | 126 ++++++++++++++++++ .../Pdf417AutoStructuredAppendTest.java | 117 ++++++++++++++++ 2 files changed, 243 insertions(+) create mode 100644 src/test/java/uk/org/okapibarcode/backend/Pdf417AutoStructuredAppendTest.java diff --git a/src/main/java/uk/org/okapibarcode/backend/Pdf417.java b/src/main/java/uk/org/okapibarcode/backend/Pdf417.java index a3c07d22..ab9ed22e 100644 --- a/src/main/java/uk/org/okapibarcode/backend/Pdf417.java +++ b/src/main/java/uk/org/okapibarcode/backend/Pdf417.java @@ -16,6 +16,7 @@ package uk.org.okapibarcode.backend; +import static java.nio.charset.StandardCharsets.ISO_8859_1; import static uk.org.okapibarcode.util.Arrays.positionOf; import java.math.BigInteger; @@ -1847,4 +1848,129 @@ public String toString() { return mode + "x" + length; } } + + /** + * 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) { + List dataList = splitData(data, template); + return createStructuredAppendSymbols(dataList, template); + } + + /** + * Overloaded method of {@link #createStructuredAppendSymbols(String, Pdf417)} which + * accepts binary data. + * + *

NOTE:See linked method above for details. + */ + public static List createStructuredAppendSymbols(byte[] data, Pdf417 template) { + return createStructuredAppendSymbols(new String(data, ISO_8859_1), template); + } + + private static List splitData(String data, Pdf417 template) { + Pdf417 testSymbol = new Pdf417() { + @Override + protected void plotSymbol() { + } // expensive plotting is not required + }; + clone(template, testSymbol); + testSymbol.setStructuredAppendTotal(2); + + List dataList = new ArrayList<>(); + String remainingData = data; + while (!remainingData.isEmpty()) { + int low = 0; + int high = remainingData.length(); + + while (low <= high) { + int mid = low + high >>> 1; + int currentLength = Math.min(mid, remainingData.length()); + String currentData = remainingData.substring(0, currentLength); + boolean fit = fits(currentData, testSymbol, false); + if (fit && mid < remainingData.length() && + fits(remainingData.substring(0, currentLength + 1), testSymbol, false)) { + low = mid + 1; + } else { + if (fit && currentLength == mid) { + dataList.add(currentData); + remainingData = remainingData.substring(mid); + } + high = mid - 1; + } + } + } + if (!dataList.isEmpty()) { + String lastData = dataList.get(dataList.size() - 1); + if (!fits(lastData, testSymbol, true)) { + int endIndex = lastData.length() - 1; + dataList.set(dataList.size() - 1, lastData.substring(0, endIndex)); + dataList.add(lastData.substring(endIndex)); + } + } + return dataList; + } + + private static boolean fits(String data, Pdf417 testSymbol, boolean last) { + testSymbol.setStructuredAppendPosition(last ? 2 : 1); + try { + if (!data.isEmpty()) { + testSymbol.setContent(data); + } + } catch (OkapiInputException e) { + return false; + } + return true; + } + + private static List createStructuredAppendSymbols(List dataList, Pdf417 template) { + int structuredAppendTotal = dataList.size(); + List symbols = new ArrayList<>(structuredAppendTotal); + for (int i = 0; i < structuredAppendTotal; i++) { + String data = dataList.get(i); + Pdf417 symbol = new Pdf417(); + symbols.add(symbol); + clone(template, symbol); + + symbol.setStructuredAppendPosition(i + 1); + symbol.setStructuredAppendTotal(structuredAppendTotal); + + symbol.setContent(data); + } + 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()); + target.setForceByteCompaction(template.getForceByteCompaction()); + } } 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..2b8555ae --- /dev/null +++ b/src/test/java/uk/org/okapibarcode/backend/Pdf417AutoStructuredAppendTest.java @@ -0,0 +1,117 @@ +/* + * Copyright 2024 Urs Wolfer + * + * 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.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +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 { + + @ParameterizedTest + @CsvSource({ + "0,0,0", + "1,1,1", + "2,1,1", + "3,1,1", + "270,2,2", + "271,2,2", + "282,2,3", + "283,2,3", + "288,2,3", + "289,2,3", + "290,2,3", + "291,3,3", + "292,3,3", + "293,3,3", + "294,3,3" + }) + public void testCreateStructuredAppendSymbols(int dataLength, int expectedSymbolsCount, int expectedSymbolsCountByteCompaction) throws Exception { + byte[] bytes = new byte[dataLength]; + for (int i = 0; i < bytes.length; i++) { + bytes[i] = (byte) i; + } + + Pdf417 symbolTemplate = new Pdf417(); + symbolTemplate.setPreferredEccLevel(4); + symbolTemplate.setBarHeight(1); + symbolTemplate.setRows(12); + symbolTemplate.setDataColumns(13); + symbolTemplate.setStructuredAppendIncludeSegmentCount(true); + + List symbols = Pdf417.createStructuredAppendSymbols(new String(bytes, ISO_8859_1), symbolTemplate); + assertions(symbols, bytes, expectedSymbolsCount); + + symbolTemplate.setForceByteCompaction(true); + List symbolsByteEncoded = Pdf417.createStructuredAppendSymbols(bytes, symbolTemplate); + assertions(symbolsByteEncoded, bytes, expectedSymbolsCountByteCompaction); + } + + private static void assertions(List symbols, byte[] bytes, int expectedSymbolsCount) throws IOException, NotFoundException, FormatException, ChecksumException { + assertEquals(expectedSymbolsCount, 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()); + } + } +}