Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
c021f45
Add structured append support to QR Code
repolevedavaj Jun 8, 2025
d7b173c
Use correct encoding while creating parity
repolevedavaj Jun 13, 2025
7d74b09
Add additional test cases for structured append QR Codes
repolevedavaj Jun 13, 2025
f361128
Improve Javadoc
repolevedavaj Jun 14, 2025
9266832
Improve Javadoc
repolevedavaj Jun 14, 2025
8e0a246
Improve Javadoc
repolevedavaj Jun 14, 2025
8a8fe5e
Apply formatting
repolevedavaj Jun 14, 2025
4498765
Apply formatting
repolevedavaj Jun 14, 2025
fe35bf2
Add additional test cases for structured append parity calculation
repolevedavaj Jun 14, 2025
31b0b43
Make MAX_STRUCTURED_APPEND_SYMBOLS private
repolevedavaj Jun 14, 2025
88da2ee
Simplify QR Code structured append test
repolevedavaj Jun 14, 2025
420e9eb
Fix structured append parity for Japanese text test
repolevedavaj Jun 14, 2025
ec05895
Improve Javadoc
repolevedavaj Jun 14, 2025
9000fc2
Improve Javadoc
repolevedavaj Jun 14, 2025
185cfe1
Improve Javadoc
repolevedavaj Jun 14, 2025
2989a85
Improve Javadoc
repolevedavaj Jun 14, 2025
bb79218
Apply formatting
repolevedavaj Jun 14, 2025
a7425e9
Apply formatting
repolevedavaj Jun 14, 2025
9f113a6
Apply formatting
repolevedavaj Jun 14, 2025
9cc025e
Apply formatting
repolevedavaj Jun 14, 2025
219e4d3
Apply formatting
repolevedavaj Jun 14, 2025
f98e5ca
Apply formatting
repolevedavaj Jun 14, 2025
7d406b9
Apply formatting
repolevedavaj Jun 14, 2025
bc953d2
Apply formatting
repolevedavaj Jun 14, 2025
d8d59d7
Apply formatting
repolevedavaj Jun 14, 2025
295ddbe
Improve exception message
repolevedavaj Jun 14, 2025
76f26d0
Use existing toBytes method to get bytes from String
repolevedavaj Jun 14, 2025
482d4fc
Replace wildcard imports with single class imports
repolevedavaj Jun 14, 2025
c56a412
Change byte array to int array in toBytes method for parity calculation
repolevedavaj Jun 14, 2025
c160b73
Remove forceStructuredAppendMode flag
repolevedavaj Jun 15, 2025
83ae618
Simplify structured append generation
repolevedavaj Jun 15, 2025
f5d8802
Add test case for structured append generation with too small template
repolevedavaj Jun 15, 2025
45c8a41
Update log of Japanese text structured append test case
repolevedavaj Jun 16, 2025
b298f54
Remove overloaded createStructuredAppendSymbols method
repolevedavaj Jun 16, 2025
c3230df
Update images of Japanese text structured append test case
repolevedavaj Jun 16, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
223 changes: 219 additions & 4 deletions src/main/java/uk/org/okapibarcode/backend/QrCode.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,17 @@

package uk.org.okapibarcode.backend;

import uk.org.okapibarcode.util.EciMode;

import static uk.org.okapibarcode.util.Arrays.positionOf;
import static uk.org.okapibarcode.util.Strings.binaryAppend;

import java.nio.CharBuffer;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

/**
* <p>Implements QR Code bar code symbology According to ISO/IEC 18004:2015.
Expand Down Expand Up @@ -175,12 +180,19 @@ private enum QrMode {
0x2542e, 0x26a64, 0x27541, 0x28c69
};

/* The max number of symbols in a structured append sequence. */
private static final int MAX_STRUCTURED_APPEND_SYMBOLS = 16;

protected int minVersion = 1;
protected int preferredVersion;
protected EccLevel preferredEccLevel = EccLevel.L;
protected boolean improveEccLevelIfPossible = true;
protected boolean forceByteCompaction;

protected int structuredAppendPosition = 1;
protected int structuredAppendTotal = 1;
protected int structuredAppendParity = 0;

/**
* Creates a new instance.
*/
Expand Down Expand Up @@ -295,6 +307,69 @@ public boolean getForceByteCompaction() {
return forceByteCompaction;
}

/**
* Sets the position of this QR Code symbol in a structured append sequence (1–16).
*
* @param position the position of this QR Code symbol in a structured append sequence (1–16)
*/
public void setStructuredAppendPosition(int position) {
if (position < 1 || position > MAX_STRUCTURED_APPEND_SYMBOLS) {
throw new IllegalArgumentException("Invalid QR Code structured append position: " + position);
}
this.structuredAppendPosition = position;
}

/**
* Returns the position of this QR Code symbol in a structured append sequence (1-16).
*
* @return the position of this QR Code symbol in a structured append sequence (1–16)
*/
public int getStructuredAppendPosition() {
return structuredAppendPosition;
}

/**
* Sets the total number of QR Code symbols in the structured append sequence (1–16).
*
* @param total the total number of QR Code symbols in the structured append sequence (1–16)
*/
public void setStructuredAppendTotal(int total) {
if (total < 1 || total > MAX_STRUCTURED_APPEND_SYMBOLS) {
throw new IllegalArgumentException("Invalid QR Code structured append total: " + total);
}
this.structuredAppendTotal = total;
}

/**
* Returns the total number of QR Code symbols in the structured append sequence (1-16).
*
* @return the total number of QR Code symbols in the structured append sequence (1–16)
*/
public int getStructuredAppendTotal() {
return structuredAppendTotal;
}

/**
* Sets the structured append parity (XOR of all bytes in the original message, 0–255).
*
* @param parity the parity value (0–255)
*/
public void setStructuredAppendParity(int parity) {
if (parity < 0 || parity > 255) {
throw new IllegalArgumentException("Invalid QR Code structured append parity: " + parity);
}
this.structuredAppendParity = parity;
}

/**
* Returns the structured append parity (XOR of all bytes in the original message, 0–255).
*
* @return the parity value (0–255)
*/
public int getStructuredAppendParity() {
return structuredAppendParity;
}

@Override
public boolean supportsGs1() {
return true;
Expand Down Expand Up @@ -344,7 +419,7 @@ protected void encode() {

QrMode[] inputMode = new QrMode[inputData.length];
defineMode(inputMode, inputData, forceByteCompaction);
est_binlen = getBinaryLength(40, inputMode, inputData, gs1, eciMode);
est_binlen = getBinaryLength(40, inputMode, inputData, gs1, eciMode, getStructuredAppendMode());

ecc_level = this.preferredEccLevel;
switch (ecc_level) {
Expand Down Expand Up @@ -390,7 +465,7 @@ protected void encode() {
dataCodewords = QR_DATA_CODEWORDS_H;
break;
}
int proposedBinLen = getBinaryLength(candidate, inputMode, inputData, gs1, eciMode);
int proposedBinLen = getBinaryLength(candidate, inputMode, inputData, gs1, eciMode, getStructuredAppendMode());
if ((8 * dataCodewords[candidate - 1]) >= proposedBinLen) {
version = candidate;
est_binlen = proposedBinLen;
Expand Down Expand Up @@ -481,7 +556,7 @@ protected void encode() {
*/
if (preferredVersion > version) {
version = preferredVersion;
est_binlen = getBinaryLength(preferredVersion, inputMode, inputData, gs1, eciMode);
est_binlen = getBinaryLength(preferredVersion, inputMode, inputData, gs1, eciMode, getStructuredAppendMode());
inputMode = applyOptimisation(version, inputMode);
}
if (preferredVersion < version) {
Expand Down Expand Up @@ -629,7 +704,7 @@ private static void defineMode(QrMode[] inputMode, int[] inputData, boolean forc
}

/** Calculate the actual bit length of the proposed binary string. */
private static int getBinaryLength(int version, QrMode[] inputModeUnoptimized, int[] inputData, boolean gs1, int eciMode) {
private static int getBinaryLength(int version, QrMode[] inputModeUnoptimized, int[] inputData, boolean gs1, int eciMode, boolean structuredAppendMode) {

int i, j;
QrMode currentMode;
Expand All @@ -645,6 +720,10 @@ private static int getBinaryLength(int version, QrMode[] inputModeUnoptimized, i

currentMode = QrMode.NULL;

if (structuredAppendMode) {
count += 20;
}

if (gs1) {
count += 4;
}
Expand Down Expand Up @@ -920,6 +999,13 @@ private void qrBinary(int[] datastream, int version, int target_binlen, QrMode[]
int reserved = est_binlen + 12;
StringBuilder binary = new StringBuilder(reserved);

if (getStructuredAppendMode()) {
binary.append("0011"); /* Structured append mode */
binaryAppend(binary, structuredAppendPosition - 1, 4);
binaryAppend(binary, structuredAppendTotal - 1, 4);
binaryAppend(binary, structuredAppendParity, 8);
}

if (gs1) {
binary.append("0101"); /* FNC1 */
}
Expand Down Expand Up @@ -1156,6 +1242,10 @@ private void qrBinary(int[] datastream, int version, int target_binlen, QrMode[]
assert binary.length() <= reserved;
}

private boolean getStructuredAppendMode() {
return structuredAppendTotal > 1;
}

/** Splits data into blocks, adds error correction and then interleaves the blocks and error correction data. */
private static void addEcc(int[] fullstream, int[] datastream, int version, int data_cw, int blocks) {

Expand Down Expand Up @@ -1761,4 +1851,129 @@ private static void addVersionInfo(int[] grid, int size, int version) {
protected void customize(int[] grid, int size) {
// empty
}

/**
* Creates a list of QR Code symbols for structured append from a string, using a template symbol.
* The template's settings are cloned for each symbol.
*
* @param data the input data
* @param template the template QrCode symbol
* @return a list of QrCode symbols with structured append set
* @throws OkapiException if no data or data is invalid
*/
public static List< QrCode > createStructuredAppendSymbols(String data, QrCode template) {
List< String > dataList = splitData(data, template);
int parity = calculateStructuredAppendParity(data, template);
return createStructuredAppendSymbols(dataList, parity, template);
}

private static List< String > splitData(String data, QrCode template) {

QrCode testSymbol = new QrCode() {
@Override
protected void plotSymbol() {
} // expensive plotting is not required
};
clone(template, testSymbol);
testSymbol.setStructuredAppendTotal(2);

List< String > split = new ArrayList<>();
while (!data.isEmpty()) {
int low = 0;
int high = data.length();
while (low <= high) {
int mid = (low + high) >>> 1;
String candidate = data.substring(0, mid);
if (fits(candidate, testSymbol)) {
low = mid + 1;
} else {
high = mid - 1;
}
}
if (high == 0) {
// This should never happen, as the template should be able to fit at least one character
throw new IllegalStateException("Failed to fit any data into the template symbol");
}
split.add(data.substring(0, high));
data = data.substring(high);
}

if (split.size() > MAX_STRUCTURED_APPEND_SYMBOLS) {
throw new OkapiInputException("The specified template is too small to hold the data and structured append metadata " +
"or the data is too large for structured append. Maximum number of symbols is " + MAX_STRUCTURED_APPEND_SYMBOLS + " but got " + split.size());
}

return split;
}

private static boolean fits(String data, QrCode testSymbol) {
if (!data.isEmpty()) {
try {
testSymbol.setContent(data);
} catch (OkapiInputException e) {
return false;
}
}
return true;
}

private static List< QrCode > createStructuredAppendSymbols(List< String > split, int parity, QrCode template) {
int count = split.size();
List< QrCode > symbols = new ArrayList<>(count);
for (int i = 0; i < count; i++) {
String data = split.get(i);
QrCode symbol = new QrCode();
symbols.add(symbol);
clone(template, symbol);
symbol.setStructuredAppendPosition(i + 1);
symbol.setStructuredAppendTotal(count);
symbol.setStructuredAppendParity(parity);
symbol.setContent(data);
}
return symbols;
}

private static int calculateStructuredAppendParity(String content, QrCode template) {
EciMode eci = selectEciModeForParity(content, template);
if (eci == EciMode.NONE) {
throw new OkapiInputException("Unable to determine ECI mode");
}

int[] bytes = toBytes(content, eci.charset);

int parity = 0;
for (int b : bytes) {
parity ^= b & 0xFF;
}

return parity;
}

private static EciMode selectEciModeForParity(String content, QrCode template) {
if (template.eciMode != -1) {
return EciMode.ECIS.stream().filter(e -> e.mode == template.eciMode).findFirst().orElse(EciMode.NONE);
} else {
return EciMode.chooseFor(content);
}
}

private static void clone(QrCode template, QrCode 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.setBarHeight(template.getBarHeight());
target.setForceByteCompaction(template.getForceByteCompaction());
target.setPreferredEccLevel(template.getPreferredEccLevel());
if (template.getPreferredVersion() != 0) {
target.setPreferredVersion(template.getPreferredVersion());
}
}

}
Loading
Loading