Skip to content

Commit

Permalink
PDF417: Automated structured append
Browse files Browse the repository at this point in the history
Resolves #92
  • Loading branch information
uwolfer committed Feb 28, 2024
1 parent 41dd7fb commit 41b073b
Show file tree
Hide file tree
Showing 3 changed files with 280 additions and 2 deletions.
5 changes: 3 additions & 2 deletions src/main/java/uk/org/okapibarcode/backend/Pdf417.java
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand Down Expand Up @@ -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;
Expand All @@ -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;
Expand Down
Original file line number Diff line number Diff line change
@@ -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 <code>total</code> and <code>position</code>.
* 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:
* <ul>
* <li>{@link Pdf417#setContent(String)}</li>
* <li>{@link Pdf417#setContent(byte[])}</li>
* <li>{@link Pdf417#setStructuredAppendTotal(int)}</li>
* <li>{@link Pdf417#setStructuredAppendPosition(int)}</li>
* </ul>
* @throws OkapiException if no data or data is invalid
*/
public static List<Pdf417> 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[])}.
*
* <p><b>NOTE:</b>See linked method above for details.
*/
public static List<Pdf417> createStructuredAppendSymbols(byte[] data, Pdf417 template) {
return createStructuredAppendSymbols(new String(data, ISO_8859_1), template, true);
}

private static List<Pdf417> createStructuredAppendSymbols(String data, Pdf417 template, boolean forceByteCompaction) {
List<String> contentList = calculateStructuredAppendContent(data, template, forceByteCompaction);
return plotStructuredAppendSymbols(contentList, template, forceByteCompaction);
}

private static List<String> calculateStructuredAppendContent(String content, Pdf417 template, boolean forceByteCompaction) {
List<String> 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<Pdf417> plotStructuredAppendSymbols(List<String> contentList, Pdf417 template, boolean forceByteCompaction) {
int structuredAppendTotal = contentList.size();
List<Pdf417> 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
}
}
Original file line number Diff line number Diff line change
@@ -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<Pdf417> symbols = Pdf417AutoStructuredAppend.createStructuredAppendSymbols(new String(bytes, ISO_8859_1), symbolTemplate);
assertions(symbols, bytes);

List<Pdf417> symbolsByteEncoded = Pdf417AutoStructuredAppend.createStructuredAppendSymbols(bytes, symbolTemplate);
assertions(symbolsByteEncoded, bytes);
}

private static void assertions(List<Pdf417> 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());
}
}
}

0 comments on commit 41b073b

Please sign in to comment.