Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Properly supports barcodes for GTIN-14 using the ITF format #973

Merged
merged 23 commits into from
Nov 29, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
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
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
import java.util.Optional;

/**
* Used by the XHTMLRenderer (creating PDFs) to replace img elements by their references image.
* Used by the XHTMLRenderer (creating PDFs) to replace img elements by their referenced image.
* <p>
* Alongside http different URI protocols are supported. These are handled by classes extending
* {@link PdfReplaceHandler}.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,13 @@

package sirius.web.templates.pdf.handlers;

import com.lowagie.text.pdf.Barcode;
import com.lowagie.text.pdf.Barcode128;
import com.lowagie.text.pdf.BarcodeEAN;
import com.lowagie.text.pdf.BarcodeInter25;
import org.xhtmlrenderer.extend.FSImage;
import org.xhtmlrenderer.extend.UserAgentCallback;
import org.xhtmlrenderer.pdf.ITextFSImage;
import sirius.kernel.commons.Strings;
import sirius.kernel.di.std.Register;
import sirius.web.util.BarcodeController;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.awt.Color;
import java.awt.Image;
Expand All @@ -33,11 +29,6 @@
@Register
public class BarcodePdfReplaceHandler extends PdfReplaceHandler {

private static final String BARCODE_TYPE_CODE128 = "code128";
private static final String BARCODE_TYPE_EAN = "ean";
private static final String BARCODE_TYPE_INTERLEAVED_2_OF_5 = "interleaved2of5";
private static final String BARCODE_TYPE_INTERLEAVED_2_OF_5_CHECKSUMMED = "interleaved2of5checksummed";

@Override
public boolean accepts(String protocol) {
return "barcode".equals(protocol);
Expand All @@ -53,16 +44,7 @@ public FSImage resolveUri(String uri, UserAgentCallback userAgentCallback, int c
throw new IllegalArgumentException("The URI is required to match the format 'barcode://type/content'");
}

Barcode code = createBarcode(barcodeInfo[0]);
code.setCode(padCodeIfNecessary(code, barcodeInfo[1]));

Image awtImage = code.createAwtImage(Color.BLACK, Color.WHITE);

int scaleFactor = calculateBarcodeScaleFactor(cssWidth, cssHeight, awtImage);

awtImage = awtImage.getScaledInstance(awtImage.getWidth(null) * scaleFactor,
awtImage.getHeight(null) * scaleFactor,
Image.SCALE_REPLICATE);
Image awtImage = BarcodeController.generateBarcodeImage(barcodeInfo[0], barcodeInfo[1], cssWidth, cssHeight);

FSImage fsImage = new ITextFSImage(com.lowagie.text.Image.getInstance(awtImage, Color.WHITE, true));

Expand All @@ -72,66 +54,4 @@ public FSImage resolveUri(String uri, UserAgentCallback userAgentCallback, int c

return fsImage;
}

private int calculateBarcodeScaleFactor(int cssWidth, int cssHeight, Image awtImage) {
return (int) Math.max(Math.ceil(cssWidth / (float) awtImage.getWidth(null)),
Math.ceil(cssHeight / (float) awtImage.getHeight(null)));
}

/**
* Creates an instance of {@link Barcode} that matches the given type descriptor.
*
* @param type the requested type
* @return the barcode
*/
@Nonnull
private Barcode createBarcode(String type) {
if (BARCODE_TYPE_CODE128.equalsIgnoreCase(type)) {
return new Barcode128();
}

if (BARCODE_TYPE_EAN.equalsIgnoreCase(type)) {
return new BarcodeEAN();
}

if (BARCODE_TYPE_INTERLEAVED_2_OF_5.equalsIgnoreCase(type)) {
return new BarcodeInter25();
}

if (BARCODE_TYPE_INTERLEAVED_2_OF_5_CHECKSUMMED.equalsIgnoreCase(type)) {
Barcode code = new BarcodeInter25();
code.setGenerateChecksum(true);
return code;
}

throw new UnsupportedOperationException(Strings.apply("Type '%s' is not supported", type));
}

/**
* Pads the code if necessary.
* <p>
* Unfortunately padding will not be added automatically when using <b>interleaved2of5</b> or
* <b>interleaved2of5checksummed</b>. Thus we manually prepend a zero if the code length is uneven.
*
* @param code the instance of the barcode
* @param src the code from the src attribute
* @return the padded code, or the original code if padding was not needed
*/
private String padCodeIfNecessary(Barcode code, String src) {
if (code instanceof BarcodeInter25) {
int length = BarcodeInter25.keepNumbers(src).length();

// Length is uneven and no checksum will be added
if (length % 2 != 0 && !code.isGenerateChecksum()) {
return "0" + src;
}

// Length is even but a checksum will be added
if (length % 2 == 0 && code.isGenerateChecksum()) {
return "0" + src;
}
}

return src;
}
}
122 changes: 103 additions & 19 deletions src/main/java/sirius/web/util/BarcodeController.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,39 +16,63 @@
import com.google.zxing.datamatrix.DataMatrixWriter;
import com.google.zxing.oned.Code128Writer;
import com.google.zxing.oned.EAN13Writer;
import com.google.zxing.oned.ITFWriter;
import com.google.zxing.qrcode.QRCodeWriter;
import com.lowagie.text.pdf.BarcodeInter25;
import io.netty.handler.codec.http.HttpResponseStatus;
import sirius.kernel.commons.Strings;
import sirius.kernel.commons.Value;
import sirius.kernel.di.std.Register;
import sirius.web.controller.BasicController;
import sirius.web.controller.Routed;
import sirius.web.http.MimeHelper;
import sirius.web.http.WebContext;

import java.awt.Image;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.io.OutputStream;
import java.util.regex.Pattern;

/**
* Used to generate barcodes by responding to "/qr" or "/barcode".
*/
@Register
public class BarcodeController extends BasicController {

private static final String TYPE_QR = "qr";
private static final String TYPE_DATAMATRIX = "datamatrix";
private static final String TYPE_CODE128 = "code128";
private static final String TYPE_EAN = "ean";
private static final String TYPE_ITF = "itf";
private static final String TYPE_INTERLEAVED_2_OF_5 = "interleaved2of5";
private static final String TYPE_INTERLEAVED_2_OF_5_CHECKSUMMED = "interleaved2of5checksummed";

private static final Pattern NUMERIC = Pattern.compile("[0-9]+");

/**
* Creates an QR code for the given content.
* Creates a QR code for the given content.
* <p>
* The parameter <tt>content</tt> determines the contents of the qr code. The parameters <tt>with</tt> and
* <tt>height</tt> its dimensions.
* </p>
* The parameter <tt>content</tt> determines the contents of the qr code. The parameters <tt>width</tt> and
* <tt>height</tt> determine its dimensions.
*
* @param ctx the current request
* @param webContext the current request
* @throws Exception in case an error occurred when generating the qr code
*/
@Routed(value = "/qr", priority = 999)
public void qr(WebContext ctx) throws Exception {
barcode(ctx, BarcodeFormat.QR_CODE);
public void qr(WebContext webContext) throws Exception {
barcode(webContext, BarcodeFormat.QR_CODE);
}

/**
* Creates a barcode for the given content.
* <p>
* The parameter <tt>content</tt> determines the contents of the barcode. The parameters <tt>width</tt> and
* <tt>height</tt> determine its dimensions.
*
* @param webContext the current request
* @throws Exception in case an error occurred when generating the barcode
*/
@Routed(value = "/barcode", priority = 999)
public void barcode(WebContext webContext) throws Exception {
barcode(webContext, determineFormat(webContext.get("type").asString()));
Expand All @@ -59,39 +83,99 @@ private void barcode(WebContext webContext, BarcodeFormat format) throws WriterE
int height = webContext.getFirstFilled("h", "height").asInt(200);
String content = webContext.getFirstFilled("c", "content").asString();
if (Strings.isEmpty(content)) {
webContext.respondWith().direct(HttpResponseStatus.BAD_REQUEST, "Usage: /barcode?type=qr&content=...&w=200&h=200");
webContext.respondWith()
.direct(HttpResponseStatus.BAD_REQUEST, "Usage: /barcode?type=qr&content=...&w=200&h=200");
return;
}

content = alignContentForItfFormat(content, webContext.get("type").asString());

format = useItfFormatForGtin14(content, format);

String fileType = webContext.getFirstFilled("fileType").asString("jpg");
Writer writer = determineWriter(format);
BitMatrix matrix = writer.encode(content, format, width, height);
try (OutputStream out = webContext.respondWith()
.infinitelyCached()
.outputStream(HttpResponseStatus.OK,
MimeHelper.guessMimeType("barcode." + fileType))) {
.infinitelyCached()
.outputStream(HttpResponseStatus.OK,
MimeHelper.guessMimeType("barcode." + fileType))) {
MatrixToImageWriter.writeToStream(matrix, fileType, out);
}
}

private BarcodeFormat determineFormat(String format) {
return switch (format) {
case "qr" -> BarcodeFormat.QR_CODE;
case "code128" -> BarcodeFormat.CODE_128;
case "ean" -> BarcodeFormat.EAN_13;
case "datamatrix" -> BarcodeFormat.DATA_MATRIX;
/**
* Generates an image of a barcode
*
* @param type the desired barcode type
* @param content the content of the barcode
* @param width the desired width
* @param height the desired height
* @return a barcode of the given data as an image
* @throws WriterException if generating the image fails
*/
public static Image generateBarcodeImage(String type, String content, int width, int height)
throws WriterException {
BarcodeFormat format = determineFormat(type);

if (!NUMERIC.matcher(content).matches() && format != BarcodeFormat.QR_CODE) {
// contains characters other than digits 0-9 -> directly return a blank image to prevent running into exception
return new BufferedImage(width != -1 ? width : 200,
height != -1 ? height : 200,
BufferedImage.TYPE_BYTE_GRAY);
}

content = alignContentForItfFormat(content, type);

format = useItfFormatForGtin14(content, format);

Writer writer = determineWriter(format);
BitMatrix matrix = writer.encode(content, format, width != -1 ? width : 200, height != -1 ? height : 200);
return MatrixToImageWriter.toBufferedImage(matrix);
}

private static BarcodeFormat determineFormat(String format) {
return switch (Value.of(format).toLowerCase()) {
case TYPE_QR -> BarcodeFormat.QR_CODE;
case TYPE_CODE128 -> BarcodeFormat.CODE_128;
case TYPE_EAN -> BarcodeFormat.EAN_13;
case TYPE_ITF, TYPE_INTERLEAVED_2_OF_5, TYPE_INTERLEAVED_2_OF_5_CHECKSUMMED -> BarcodeFormat.ITF;
case TYPE_DATAMATRIX -> BarcodeFormat.DATA_MATRIX;
default -> throw new IllegalArgumentException(
"Unsupported barcode type. Supported types are: qr, code128, ean, datamatrix");
"Unsupported barcode type. Supported types are: qr, code128, ean, interleaved2of5, interleaved2of5checksummed, datamatrix");
};
}

private Writer determineWriter(BarcodeFormat format) {
private static Writer determineWriter(BarcodeFormat format) {
return switch (format) {
case QR_CODE -> new QRCodeWriter();
case CODE_128 -> new Code128Writer();
case EAN_13 -> new EAN13Writer();
case ITF -> new ITFWriter();
case DATA_MATRIX -> new DataMatrixWriter();
default -> throw new IllegalArgumentException("Unsupported barcode type!");
};
}

private static BarcodeFormat useItfFormatForGtin14(String content, BarcodeFormat format) {
// Adjust the barcode format, if "type=ean" was submitted with the request and a GTIN-14 was given
if (BarcodeFormat.EAN_13 == format && content.length() == 14) {
return BarcodeFormat.ITF;
}

return format;
}

private static String alignContentForItfFormat(String content, String format) {
if (TYPE_INTERLEAVED_2_OF_5_CHECKSUMMED.equalsIgnoreCase(format)) {
content += BarcodeInter25.getChecksum(content);
}

if ((TYPE_INTERLEAVED_2_OF_5.equalsIgnoreCase(format) || TYPE_INTERLEAVED_2_OF_5_CHECKSUMMED.equalsIgnoreCase(
format)) && BarcodeInter25.keepNumbers(content).length() % 2 != 0) {
// Pads the code if the length is uneven
content = "0" + content;
}

return content;
}
}