From c6f0d51a08b2e964935bdce94f5301fd4418ac19 Mon Sep 17 00:00:00 2001 From: Machac Date: Wed, 8 Apr 2026 12:18:54 +0200 Subject: [PATCH 1/6] [NAE-2405] QRService dependency rework - Replace `qrgen` library with `zxing` for QR code generation. - Add `QrImageType` enum for supported image format handling. - Refactor `QrService` to improve error handling and scaling logic. - Introduce helper methods for `BitMatrix` creation and image format resolution. - Update dependencies and clean up unused repository configuration in `pom.xml`. --- application-engine/pom.xml | 13 +- .../engine/business/qr/QrCode.java | 4 +- .../engine/business/qr/QrImageType.java | 20 +++ .../engine/business/qr/QrService.java | 142 +++++++++++++----- 4 files changed, 137 insertions(+), 42 deletions(-) create mode 100644 application-engine/src/main/java/com/netgrif/application/engine/business/qr/QrImageType.java diff --git a/application-engine/pom.xml b/application-engine/pom.xml index 583df212b1a..b22f9aaec13 100644 --- a/application-engine/pom.xml +++ b/application-engine/pom.xml @@ -70,10 +70,6 @@ true - - jitpack.io - https://jitpack.io - @@ -359,9 +355,14 @@ - com.github.kenglxn.qrgen + com.google.zxing + core + 3.5.4 + + + com.google.zxing javase - 3.0.1 + 3.5.4 diff --git a/application-engine/src/main/java/com/netgrif/application/engine/business/qr/QrCode.java b/application-engine/src/main/java/com/netgrif/application/engine/business/qr/QrCode.java index 8c6d3ee1b30..3870c937f2c 100644 --- a/application-engine/src/main/java/com/netgrif/application/engine/business/qr/QrCode.java +++ b/application-engine/src/main/java/com/netgrif/application/engine/business/qr/QrCode.java @@ -7,11 +7,13 @@ @Data public class QrCode { + private static final double DEFAULT_LOGO_RATIO = 0.22; + private String content; private String fileName; - private ImageType imageType = ImageType.JPG; + private QrImageType imageType = QrImageType.PNG; private int width = 250; diff --git a/application-engine/src/main/java/com/netgrif/application/engine/business/qr/QrImageType.java b/application-engine/src/main/java/com/netgrif/application/engine/business/qr/QrImageType.java new file mode 100644 index 00000000000..0fcc4e7d9cd --- /dev/null +++ b/application-engine/src/main/java/com/netgrif/application/engine/business/qr/QrImageType.java @@ -0,0 +1,20 @@ +package com.netgrif.application.engine.business.qr; + +public enum QrImageType { + + PNG("png"), + JPG("jpg"), + JPEG("jpeg"), + GIF("gif"), + BMP("bmp"); + + private final String format; + + QrImageType(String format) { + this.format = format; + } + + public String getFormat() { + return format; + } +} \ No newline at end of file diff --git a/application-engine/src/main/java/com/netgrif/application/engine/business/qr/QrService.java b/application-engine/src/main/java/com/netgrif/application/engine/business/qr/QrService.java index 17a337360de..1bf1fc12e57 100644 --- a/application-engine/src/main/java/com/netgrif/application/engine/business/qr/QrService.java +++ b/application-engine/src/main/java/com/netgrif/application/engine/business/qr/QrService.java @@ -1,34 +1,42 @@ package com.netgrif.application.engine.business.qr; import com.google.zxing.BarcodeFormat; +import com.google.zxing.EncodeHintType; import com.google.zxing.WriterException; import com.google.zxing.client.j2se.MatrixToImageConfig; import com.google.zxing.client.j2se.MatrixToImageWriter; import com.google.zxing.common.BitMatrix; -import net.glxn.qrgen.javase.QRCode; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import com.google.zxing.qrcode.QRCodeWriter; +import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import javax.imageio.ImageIO; -import java.awt.*; +import java.awt.AlphaComposite; +import java.awt.Graphics2D; import java.awt.image.BufferedImage; -import java.io.*; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; import java.nio.file.Files; -import java.nio.file.Paths; +import java.nio.file.Path; import java.nio.file.StandardCopyOption; +import java.util.EnumMap; +import java.util.Map; import java.util.Optional; +@Slf4j @Service public class QrService implements IQrService { - private static Logger log = LoggerFactory.getLogger(QrService.class); - @Override public Optional generateToStream(QrCode code) { try { return Optional.of(new FileInputStream(generateFile(code))); - } catch (FileNotFoundException e) { + } catch (WriterException | IOException e) { log.error("Error creating qr code.", e); return Optional.empty(); } @@ -37,7 +45,7 @@ public Optional generateToStream(QrCode code) { @Override public Optional generateToFile(QrCode code) { try { - return Optional.ofNullable(generateFile(code)); + return Optional.of(generateFile(code)); } catch (Exception e) { log.error("Error creating qr code.", e); return Optional.empty(); @@ -48,30 +56,62 @@ public Optional generateToFile(QrCode code) { public Optional generateWithLogo(QrCode code, InputStream imageStream) { try { ByteArrayOutputStream os = new ByteArrayOutputStream(); - BitMatrix bitMatrix = QRCode.from(code.getContent()) - .to(code.getImageType()) - .withSize(code.getWidth(), code.getHeight()) - .withColor(code.getOnColor(), code.getOffColor()) - .withErrorCorrection(code.getErrorCorrectionLevel()) - .withCharset(code.getCharset()) - .getQrWriter().encode(code.getContent(), BarcodeFormat.QR_CODE, code.getWidth(), code.getHeight()); + + BitMatrix bitMatrix = createBitMatrix(code); MatrixToImageConfig config = new MatrixToImageConfig(code.getOnColor(), code.getOffColor()); BufferedImage qrImage = MatrixToImageWriter.toBufferedImage(bitMatrix, config); - BufferedImage overly = ImageIO.read(imageStream); + BufferedImage overlay = ImageIO.read(imageStream); + + if (overlay == null) { + log.error("Error creating qr code. Overlay image could not be read."); + return Optional.empty(); + } + + int maxLogoSize = (int) (Math.min(qrImage.getWidth(), qrImage.getHeight()) * 0.22); + int logoWidth = overlay.getWidth(); + int logoHeight = overlay.getHeight(); - int deltaHeight = qrImage.getHeight() - overly.getHeight(); - int deltaWidth = qrImage.getWidth() - overly.getWidth(); + if (logoWidth > maxLogoSize || logoHeight > maxLogoSize) { + double scale = Math.min((double) maxLogoSize / logoWidth, (double) maxLogoSize / logoHeight); + logoWidth = (int) (logoWidth * scale); + logoHeight = (int) (logoHeight * scale); + } - BufferedImage combined = new BufferedImage(qrImage.getHeight(), qrImage.getWidth(), BufferedImage.TYPE_INT_ARGB); - Graphics2D g = (Graphics2D) combined.getGraphics(); + BufferedImage scaledOverlay = new BufferedImage(logoWidth, logoHeight, BufferedImage.TYPE_INT_ARGB); + Graphics2D gScaled = scaledOverlay.createGraphics(); + try { + gScaled.setRenderingHint(java.awt.RenderingHints.KEY_INTERPOLATION, + java.awt.RenderingHints.VALUE_INTERPOLATION_BILINEAR); + gScaled.drawImage(overlay, 0, 0, logoWidth, logoHeight, null); + } finally { + gScaled.dispose(); + } - g.drawImage(qrImage, 0, 0, null); - g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 1f)); - g.drawImage(overly, Math.round(deltaWidth / 2), Math.round(deltaHeight / 2), null); + int x = (qrImage.getWidth() - logoWidth) / 2; + int y = (qrImage.getHeight() - logoHeight) / 2; - ImageIO.write(combined, "png", os); - Files.copy(new ByteArrayInputStream(os.toByteArray()), Paths.get(code.getFileName()), StandardCopyOption.REPLACE_EXISTING); + BufferedImage combined = new BufferedImage( + qrImage.getWidth(), + qrImage.getHeight(), + BufferedImage.TYPE_INT_ARGB + ); + + Graphics2D g = combined.createGraphics(); + try { + g.drawImage(qrImage, 0, 0, null); + g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 1f)); + g.drawImage(scaledOverlay, x, y, null); + } finally { + g.dispose(); + } + + ImageIO.write(combined, resolveImageFormat(code), os); + Files.copy( + new ByteArrayInputStream(os.toByteArray()), + Path.of(code.getFileName()), + StandardCopyOption.REPLACE_EXISTING + ); return Optional.of(new File(code.getFileName())); } catch (WriterException | IOException e) { @@ -80,13 +120,45 @@ public Optional generateWithLogo(QrCode code, InputStream imageStream) { } } - private File generateFile(QrCode code) { - return QRCode.from(code.getContent()) - .to(code.getImageType()) - .withSize(code.getWidth(), code.getHeight()) - .withColor(code.getOnColor(), code.getOffColor()) - .withErrorCorrection(code.getErrorCorrectionLevel()) - .withCharset(code.getCharset()) - .file(code.getFileName()); + private File generateFile(QrCode code) throws WriterException, IOException { + BitMatrix bitMatrix = createBitMatrix(code); + MatrixToImageConfig config = new MatrixToImageConfig(code.getOnColor(), code.getOffColor()); + + Path outputPath = Path.of(code.getFileName()); + + try (OutputStream outputStream = Files.newOutputStream(outputPath)) { + MatrixToImageWriter.writeToStream( + bitMatrix, + resolveImageFormat(code), + outputStream, + config + ); + } + + return outputPath.toFile(); + } + + private BitMatrix createBitMatrix(QrCode code) throws WriterException { + Map hints = new EnumMap<>(EncodeHintType.class); + hints.put(EncodeHintType.CHARACTER_SET, code.getCharset()); + hints.put(EncodeHintType.ERROR_CORRECTION, code.getErrorCorrectionLevel()); + hints.put(EncodeHintType.MARGIN, 1); + + QRCodeWriter writer = new QRCodeWriter(); + return writer.encode( + code.getContent(), + BarcodeFormat.QR_CODE, + code.getWidth(), + code.getHeight(), + hints + ); + } + + private String resolveImageFormat(QrCode code) { + if (code.getImageType() == null) { + return QrImageType.PNG.getFormat(); + } + + return code.getImageType().getFormat(); } } \ No newline at end of file From e56a9f76ccf6ec6df5462a4bd3c7b6eda9743125 Mon Sep 17 00:00:00 2001 From: Machac Date: Wed, 8 Apr 2026 13:22:37 +0200 Subject: [PATCH 2/6] [NAE-2405] QRService dependency rework - Add error correction level validation for embedded logo support. - Implement scaling and compositing methods for QR codes with logos. - Enhance logging for QR generation, including dimensions and errors. - Refactor `QrService` by adding helper methods for scaling and file writing. - Update `QrCode` to include defaults for logo ratio, background padding, and arc. --- .../engine/business/qr/QrCode.java | 15 +- .../engine/business/qr/QrService.java | 251 ++++++++++++------ 2 files changed, 185 insertions(+), 81 deletions(-) diff --git a/application-engine/src/main/java/com/netgrif/application/engine/business/qr/QrCode.java b/application-engine/src/main/java/com/netgrif/application/engine/business/qr/QrCode.java index 3870c937f2c..5423dcc14b9 100644 --- a/application-engine/src/main/java/com/netgrif/application/engine/business/qr/QrCode.java +++ b/application-engine/src/main/java/com/netgrif/application/engine/business/qr/QrCode.java @@ -7,13 +7,16 @@ @Data public class QrCode { - private static final double DEFAULT_LOGO_RATIO = 0.22; + public static final double DEFAULT_LOGO_RATIO = 0.15; + public static final int DEFAULT_LOGO_BACKGROUND_PADDING = 4; + public static final int DEFAULT_LOGO_BACKGROUND_ARC = 8; + public static final QrImageType DEFAULT_IMAGE_TYPE = QrImageType.PNG; private String content; private String fileName; - private QrImageType imageType = QrImageType.PNG; + private QrImageType imageType = DEFAULT_IMAGE_TYPE; private int width = 250; @@ -23,10 +26,16 @@ public class QrCode { private int onColor = 0xFF000000; - private int offColor = 0xFFFFFFFF; + private int offColor = 0xFFFF99FF; private String charset = "ISO-8859-1"; + private double logoRatio = DEFAULT_LOGO_RATIO; + + private int logoBackgroundPadding = DEFAULT_LOGO_BACKGROUND_PADDING; + + private int logoBackgroundArc = DEFAULT_LOGO_BACKGROUND_ARC; + public QrCode(String fileName, String content) { this.fileName = fileName; this.content = content; diff --git a/application-engine/src/main/java/com/netgrif/application/engine/business/qr/QrService.java b/application-engine/src/main/java/com/netgrif/application/engine/business/qr/QrService.java index 1bf1fc12e57..2c00ad2a5a0 100644 --- a/application-engine/src/main/java/com/netgrif/application/engine/business/qr/QrService.java +++ b/application-engine/src/main/java/com/netgrif/application/engine/business/qr/QrService.java @@ -7,23 +7,19 @@ import com.google.zxing.client.j2se.MatrixToImageWriter; import com.google.zxing.common.BitMatrix; import com.google.zxing.qrcode.QRCodeWriter; +import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import javax.imageio.ImageIO; import java.awt.AlphaComposite; +import java.awt.Color; import java.awt.Graphics2D; +import java.awt.RenderingHints; import java.awt.image.BufferedImage; -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; +import java.io.*; import java.nio.file.Files; import java.nio.file.Path; -import java.nio.file.StandardCopyOption; import java.util.EnumMap; import java.util.Map; import java.util.Optional; @@ -32,90 +28,73 @@ @Service public class QrService implements IQrService { + @Override public Optional generateToStream(QrCode code) { - try { - return Optional.of(new FileInputStream(generateFile(code))); + log.debug("Generating QR code to stream [file={}, size={}x{}, errorCorrection={}]", + code.getFileName(), code.getWidth(), code.getHeight(), code.getErrorCorrectionLevel()); + try (ByteArrayOutputStream os = new ByteArrayOutputStream()) { + BitMatrix bitMatrix = createBitMatrix(code); + MatrixToImageConfig config = new MatrixToImageConfig(code.getOnColor(), code.getOffColor()); + String format = resolveImageFormat(code); + + MatrixToImageWriter.writeToStream(bitMatrix, format, os, config); + + log.trace("QR code stream generated successfully [file={}, bytes={}]", + code.getFileName(), os.size()); + return Optional.of(new ByteArrayInputStream(os.toByteArray())); } catch (WriterException | IOException e) { - log.error("Error creating qr code.", e); + log.error("Failed to generate QR code to stream [file={}]", code.getFileName(), e); return Optional.empty(); } } @Override public Optional generateToFile(QrCode code) { + log.debug("Generating QR code to file [file={}, size={}x{}, errorCorrection={}]", + code.getFileName(), code.getWidth(), code.getHeight(), code.getErrorCorrectionLevel()); try { - return Optional.of(generateFile(code)); - } catch (Exception e) { - log.error("Error creating qr code.", e); + File result = generateFile(code); + log.trace("QR code file generated successfully [file={}, sizeBytes={}]", + result.getAbsolutePath(), result.length()); + return Optional.of(result); + } catch (WriterException | IOException e) { + log.error("Failed to generate QR code to file [file={}]", code.getFileName(), e); return Optional.empty(); } } @Override public Optional generateWithLogo(QrCode code, InputStream imageStream) { - try { - ByteArrayOutputStream os = new ByteArrayOutputStream(); + log.debug("Generating QR code with logo [file={}, size={}x{}, errorCorrection={}, logoRatio={}]", + code.getFileName(), code.getWidth(), code.getHeight(), + code.getErrorCorrectionLevel(), code.getLogoRatio()); + + validateLogoErrorCorrection(code); + BufferedImage overlay = readLogoImage(imageStream, code.getFileName()); + if (overlay == null) { + return Optional.empty(); + } + + try { BitMatrix bitMatrix = createBitMatrix(code); MatrixToImageConfig config = new MatrixToImageConfig(code.getOnColor(), code.getOffColor()); - BufferedImage qrImage = MatrixToImageWriter.toBufferedImage(bitMatrix, config); - BufferedImage overlay = ImageIO.read(imageStream); - - if (overlay == null) { - log.error("Error creating qr code. Overlay image could not be read."); - return Optional.empty(); - } - - int maxLogoSize = (int) (Math.min(qrImage.getWidth(), qrImage.getHeight()) * 0.22); - int logoWidth = overlay.getWidth(); - int logoHeight = overlay.getHeight(); - - if (logoWidth > maxLogoSize || logoHeight > maxLogoSize) { - double scale = Math.min((double) maxLogoSize / logoWidth, (double) maxLogoSize / logoHeight); - logoWidth = (int) (logoWidth * scale); - logoHeight = (int) (logoHeight * scale); - } - BufferedImage scaledOverlay = new BufferedImage(logoWidth, logoHeight, BufferedImage.TYPE_INT_ARGB); - Graphics2D gScaled = scaledOverlay.createGraphics(); - try { - gScaled.setRenderingHint(java.awt.RenderingHints.KEY_INTERPOLATION, - java.awt.RenderingHints.VALUE_INTERPOLATION_BILINEAR); - gScaled.drawImage(overlay, 0, 0, logoWidth, logoHeight, null); - } finally { - gScaled.dispose(); - } + log.trace("QR BitMatrix created [file={}, qrSize={}x{}]", + code.getFileName(), qrImage.getWidth(), qrImage.getHeight()); - int x = (qrImage.getWidth() - logoWidth) / 2; - int y = (qrImage.getHeight() - logoHeight) / 2; + BufferedImage scaledLogo = scaleLogo(overlay, qrImage.getWidth(), qrImage.getHeight(), code.getLogoRatio()); + BufferedImage combined = compositeLogoOnQr(qrImage, scaledLogo, code); + writeImageToFile(combined, code); - BufferedImage combined = new BufferedImage( - qrImage.getWidth(), - qrImage.getHeight(), - BufferedImage.TYPE_INT_ARGB - ); - - Graphics2D g = combined.createGraphics(); - try { - g.drawImage(qrImage, 0, 0, null); - g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 1f)); - g.drawImage(scaledOverlay, x, y, null); - } finally { - g.dispose(); - } - - ImageIO.write(combined, resolveImageFormat(code), os); - Files.copy( - new ByteArrayInputStream(os.toByteArray()), - Path.of(code.getFileName()), - StandardCopyOption.REPLACE_EXISTING - ); - - return Optional.of(new File(code.getFileName())); + File result = new File(code.getFileName()); + log.trace("QR code with logo written to disk [file={}, sizeBytes={}]", + result.getAbsolutePath(), result.length()); + return Optional.of(result); } catch (WriterException | IOException e) { - log.error("Error creating qr code.", e); + log.error("Failed to generate QR code with logo [file={}]", code.getFileName(), e); return Optional.empty(); } } @@ -123,16 +102,12 @@ public Optional generateWithLogo(QrCode code, InputStream imageStream) { private File generateFile(QrCode code) throws WriterException, IOException { BitMatrix bitMatrix = createBitMatrix(code); MatrixToImageConfig config = new MatrixToImageConfig(code.getOnColor(), code.getOffColor()); - Path outputPath = Path.of(code.getFileName()); try (OutputStream outputStream = Files.newOutputStream(outputPath)) { - MatrixToImageWriter.writeToStream( - bitMatrix, - resolveImageFormat(code), - outputStream, - config - ); + MatrixToImageWriter.writeToStream(bitMatrix, resolveImageFormat(code), outputStream, config); + } catch (IOException e) { + log.error("Failed to write QR code to file [file={}]", code.getFileName(), e); } return outputPath.toFile(); @@ -144,8 +119,10 @@ private BitMatrix createBitMatrix(QrCode code) throws WriterException { hints.put(EncodeHintType.ERROR_CORRECTION, code.getErrorCorrectionLevel()); hints.put(EncodeHintType.MARGIN, 1); - QRCodeWriter writer = new QRCodeWriter(); - return writer.encode( + log.trace("Encoding BitMatrix [file={}, charset={}, errorCorrection={}, margin=1]", + code.getFileName(), code.getCharset(), code.getErrorCorrectionLevel()); + + return new QRCodeWriter().encode( code.getContent(), BarcodeFormat.QR_CODE, code.getWidth(), @@ -154,11 +131,129 @@ private BitMatrix createBitMatrix(QrCode code) throws WriterException { ); } + private BufferedImage readLogoImage(InputStream imageStream, String fileName) { + try { + BufferedImage image = ImageIO.read(imageStream); + if (image == null) { + log.error("Logo image stream produced a null image — unsupported format or empty stream [file={}]", + fileName); + return null; + } + log.trace("Logo image read successfully [file={}, logoSize={}x{}]", + fileName, image.getWidth(), image.getHeight()); + return image; + } catch (IOException e) { + log.error("Failed to read logo image stream [file={}]", fileName, e); + return null; + } + } + + private BufferedImage scaleLogo(BufferedImage logo, int qrWidth, int qrHeight, double logoRatio) { + int maxSize = (int) (Math.min(qrWidth, qrHeight) * logoRatio); + int logoWidth = logo.getWidth(); + int logoHeight = logo.getHeight(); + + log.trace("Logo scale check [original={}x{}, maxAllowed={}px, ratio={}]", + logoWidth, logoHeight, maxSize, logoRatio); + + if (logoWidth <= maxSize && logoHeight <= maxSize) { + log.debug("Logo fits within ratio, no scaling needed [logo={}x{}, max={}px]", + logoWidth, logoHeight, maxSize); + return logo; + } + + double scale = Math.min((double) maxSize / logoWidth, (double) maxSize / logoHeight); + int scaledWidth = Math.max(1, (int) (logoWidth * scale)); + int scaledHeight = Math.max(1, (int) (logoHeight * scale)); + + log.debug("Scaling logo [from={}x{} to={}x{}, maxAllowed={}px, scale={}]", + logoWidth, logoHeight, scaledWidth, scaledHeight, maxSize, String.format("%.4f", scale)); + + BufferedImage scaled = new BufferedImage(scaledWidth, scaledHeight, BufferedImage.TYPE_INT_ARGB); + Graphics2D g = scaled.createGraphics(); + try { + g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC); + g.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY); + g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + g.drawImage(logo, 0, 0, scaledWidth, scaledHeight, null); + } finally { + g.dispose(); + } + + return scaled; + } + + private BufferedImage compositeLogoOnQr(BufferedImage qrImage, BufferedImage logo, QrCode code) { + int x = (qrImage.getWidth() - logo.getWidth()) / 2; + int y = (qrImage.getHeight() - logo.getHeight()) / 2; + int padding = code.getLogoBackgroundPadding(); + int arc = code.getLogoBackgroundArc(); + + log.trace("Compositing logo onto QR [qr={}x{}, logo={}x{}, offset=({},{}), padding={}, arc={}]", + qrImage.getWidth(), qrImage.getHeight(), + logo.getWidth(), logo.getHeight(), x, y, padding, arc); + + BufferedImage combined = new BufferedImage( + qrImage.getWidth(), + qrImage.getHeight(), + BufferedImage.TYPE_INT_ARGB + ); + + Graphics2D g = combined.createGraphics(); + try { + g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + g.drawImage(qrImage, 0, 0, null); + + g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 1f)); + g.setColor(Color.WHITE); + g.fillRoundRect( + x - padding, + y - padding, + logo.getWidth() + padding * 2, + logo.getHeight() + padding * 2, + arc, + arc + ); + + g.drawImage(logo, x, y, null); + } finally { + g.dispose(); + } + + return combined; + } + + private void writeImageToFile(BufferedImage image, QrCode code) throws IOException { + String format = resolveImageFormat(code); + Path outputPath = Path.of(code.getFileName()); + + log.trace("Writing combined image to disk [file={}, format={}]", code.getFileName(), format); + + try (OutputStream outputStream = Files.newOutputStream(outputPath)) { + boolean written = ImageIO.write(image, format, outputStream); + if (!written) { + throw new IOException("No ImageIO writer found for format: " + format); + } + } + } + + + private void validateLogoErrorCorrection(QrCode code) { + if (code.getErrorCorrectionLevel() != ErrorCorrectionLevel.H) { + log.warn( + "QR code '{}' uses ErrorCorrectionLevel.{} — ErrorCorrectionLevel.H is strongly " + + "recommended when embedding a logo to ensure reliable scanning.", + code.getFileName(), + code.getErrorCorrectionLevel() + ); + } + } + private String resolveImageFormat(QrCode code) { if (code.getImageType() == null) { + log.trace("No image type set, defaulting to PNG [file={}]", code.getFileName()); return QrImageType.PNG.getFormat(); } - return code.getImageType().getFormat(); } } \ No newline at end of file From bf6c09752906a2a57151ed9376aa89e33a532cde Mon Sep 17 00:00:00 2001 From: Machac Date: Wed, 8 Apr 2026 13:24:31 +0200 Subject: [PATCH 3/6] [NAE-2405] QRService dependency rework - Refactor logging to maintain consistent formatting across methods. - Adjust log messages for QR generation, scaling, and compositing methods. - Simplify multi-line logs into single-line statements for clarity. --- .../engine/business/qr/QrService.java | 45 ++++++------------- 1 file changed, 13 insertions(+), 32 deletions(-) diff --git a/application-engine/src/main/java/com/netgrif/application/engine/business/qr/QrService.java b/application-engine/src/main/java/com/netgrif/application/engine/business/qr/QrService.java index 2c00ad2a5a0..6d6e31dd348 100644 --- a/application-engine/src/main/java/com/netgrif/application/engine/business/qr/QrService.java +++ b/application-engine/src/main/java/com/netgrif/application/engine/business/qr/QrService.java @@ -31,8 +31,7 @@ public class QrService implements IQrService { @Override public Optional generateToStream(QrCode code) { - log.debug("Generating QR code to stream [file={}, size={}x{}, errorCorrection={}]", - code.getFileName(), code.getWidth(), code.getHeight(), code.getErrorCorrectionLevel()); + log.debug("Generating QR code to stream [file={}, size={}x{}, errorCorrection={}]", code.getFileName(), code.getWidth(), code.getHeight(), code.getErrorCorrectionLevel()); try (ByteArrayOutputStream os = new ByteArrayOutputStream()) { BitMatrix bitMatrix = createBitMatrix(code); MatrixToImageConfig config = new MatrixToImageConfig(code.getOnColor(), code.getOffColor()); @@ -40,8 +39,7 @@ public Optional generateToStream(QrCode code) { MatrixToImageWriter.writeToStream(bitMatrix, format, os, config); - log.trace("QR code stream generated successfully [file={}, bytes={}]", - code.getFileName(), os.size()); + log.trace("QR code stream generated successfully [file={}, bytes={}]", code.getFileName(), os.size()); return Optional.of(new ByteArrayInputStream(os.toByteArray())); } catch (WriterException | IOException e) { log.error("Failed to generate QR code to stream [file={}]", code.getFileName(), e); @@ -51,12 +49,10 @@ public Optional generateToStream(QrCode code) { @Override public Optional generateToFile(QrCode code) { - log.debug("Generating QR code to file [file={}, size={}x{}, errorCorrection={}]", - code.getFileName(), code.getWidth(), code.getHeight(), code.getErrorCorrectionLevel()); + log.debug("Generating QR code to file [file={}, size={}x{}, errorCorrection={}]", code.getFileName(), code.getWidth(), code.getHeight(), code.getErrorCorrectionLevel()); try { File result = generateFile(code); - log.trace("QR code file generated successfully [file={}, sizeBytes={}]", - result.getAbsolutePath(), result.length()); + log.trace("QR code file generated successfully [file={}, sizeBytes={}]", result.getAbsolutePath(), result.length()); return Optional.of(result); } catch (WriterException | IOException e) { log.error("Failed to generate QR code to file [file={}]", code.getFileName(), e); @@ -66,9 +62,7 @@ public Optional generateToFile(QrCode code) { @Override public Optional generateWithLogo(QrCode code, InputStream imageStream) { - log.debug("Generating QR code with logo [file={}, size={}x{}, errorCorrection={}, logoRatio={}]", - code.getFileName(), code.getWidth(), code.getHeight(), - code.getErrorCorrectionLevel(), code.getLogoRatio()); + log.debug("Generating QR code with logo [file={}, size={}x{}, errorCorrection={}, logoRatio={}]", code.getFileName(), code.getWidth(), code.getHeight(), code.getErrorCorrectionLevel(), code.getLogoRatio()); validateLogoErrorCorrection(code); @@ -82,16 +76,14 @@ public Optional generateWithLogo(QrCode code, InputStream imageStream) { MatrixToImageConfig config = new MatrixToImageConfig(code.getOnColor(), code.getOffColor()); BufferedImage qrImage = MatrixToImageWriter.toBufferedImage(bitMatrix, config); - log.trace("QR BitMatrix created [file={}, qrSize={}x{}]", - code.getFileName(), qrImage.getWidth(), qrImage.getHeight()); + log.trace("QR BitMatrix created [file={}, qrSize={}x{}]", code.getFileName(), qrImage.getWidth(), qrImage.getHeight()); BufferedImage scaledLogo = scaleLogo(overlay, qrImage.getWidth(), qrImage.getHeight(), code.getLogoRatio()); BufferedImage combined = compositeLogoOnQr(qrImage, scaledLogo, code); writeImageToFile(combined, code); File result = new File(code.getFileName()); - log.trace("QR code with logo written to disk [file={}, sizeBytes={}]", - result.getAbsolutePath(), result.length()); + log.trace("QR code with logo written to disk [file={}, sizeBytes={}]", result.getAbsolutePath(), result.length()); return Optional.of(result); } catch (WriterException | IOException e) { log.error("Failed to generate QR code with logo [file={}]", code.getFileName(), e); @@ -119,8 +111,7 @@ private BitMatrix createBitMatrix(QrCode code) throws WriterException { hints.put(EncodeHintType.ERROR_CORRECTION, code.getErrorCorrectionLevel()); hints.put(EncodeHintType.MARGIN, 1); - log.trace("Encoding BitMatrix [file={}, charset={}, errorCorrection={}, margin=1]", - code.getFileName(), code.getCharset(), code.getErrorCorrectionLevel()); + log.trace("Encoding BitMatrix [file={}, charset={}, errorCorrection={}, margin=1]", code.getFileName(), code.getCharset(), code.getErrorCorrectionLevel()); return new QRCodeWriter().encode( code.getContent(), @@ -135,8 +126,7 @@ private BufferedImage readLogoImage(InputStream imageStream, String fileName) { try { BufferedImage image = ImageIO.read(imageStream); if (image == null) { - log.error("Logo image stream produced a null image — unsupported format or empty stream [file={}]", - fileName); + log.error("Logo image stream produced a null image — unsupported format or empty stream [file={}]", fileName); return null; } log.trace("Logo image read successfully [file={}, logoSize={}x{}]", @@ -153,12 +143,10 @@ private BufferedImage scaleLogo(BufferedImage logo, int qrWidth, int qrHeight, d int logoWidth = logo.getWidth(); int logoHeight = logo.getHeight(); - log.trace("Logo scale check [original={}x{}, maxAllowed={}px, ratio={}]", - logoWidth, logoHeight, maxSize, logoRatio); + log.trace("Logo scale check [original={}x{}, maxAllowed={}px, ratio={}]", logoWidth, logoHeight, maxSize, logoRatio); if (logoWidth <= maxSize && logoHeight <= maxSize) { - log.debug("Logo fits within ratio, no scaling needed [logo={}x{}, max={}px]", - logoWidth, logoHeight, maxSize); + log.debug("Logo fits within ratio, no scaling needed [logo={}x{}, max={}px]", logoWidth, logoHeight, maxSize); return logo; } @@ -189,9 +177,7 @@ private BufferedImage compositeLogoOnQr(BufferedImage qrImage, BufferedImage log int padding = code.getLogoBackgroundPadding(); int arc = code.getLogoBackgroundArc(); - log.trace("Compositing logo onto QR [qr={}x{}, logo={}x{}, offset=({},{}), padding={}, arc={}]", - qrImage.getWidth(), qrImage.getHeight(), - logo.getWidth(), logo.getHeight(), x, y, padding, arc); + log.trace("Compositing logo onto QR [qr={}x{}, logo={}x{}, offset=({},{}), padding={}, arc={}]", qrImage.getWidth(), qrImage.getHeight(), logo.getWidth(), logo.getHeight(), x, y, padding, arc); BufferedImage combined = new BufferedImage( qrImage.getWidth(), @@ -240,12 +226,7 @@ private void writeImageToFile(BufferedImage image, QrCode code) throws IOExcepti private void validateLogoErrorCorrection(QrCode code) { if (code.getErrorCorrectionLevel() != ErrorCorrectionLevel.H) { - log.warn( - "QR code '{}' uses ErrorCorrectionLevel.{} — ErrorCorrectionLevel.H is strongly " + - "recommended when embedding a logo to ensure reliable scanning.", - code.getFileName(), - code.getErrorCorrectionLevel() - ); + log.warn("QR code '{}' uses ErrorCorrectionLevel.{} — ErrorCorrectionLevel.H is strongly recommended when embedding a logo to ensure reliable scanning.", code.getFileName(), code.getErrorCorrectionLevel()); } } From 6b21e2f209a14502a25bf357082b3856528237d6 Mon Sep 17 00:00:00 2001 From: Machac Date: Wed, 8 Apr 2026 13:40:43 +0200 Subject: [PATCH 4/6] [NAE-2405] QRService dependency rework - Remove unused IOException logging in `QrService`. - Update `QrCode` defaults: error correction to H, off-color to white. --- .../com/netgrif/application/engine/business/qr/QrCode.java | 4 ++-- .../com/netgrif/application/engine/business/qr/QrService.java | 2 -- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/application-engine/src/main/java/com/netgrif/application/engine/business/qr/QrCode.java b/application-engine/src/main/java/com/netgrif/application/engine/business/qr/QrCode.java index 5423dcc14b9..244baa510ce 100644 --- a/application-engine/src/main/java/com/netgrif/application/engine/business/qr/QrCode.java +++ b/application-engine/src/main/java/com/netgrif/application/engine/business/qr/QrCode.java @@ -22,11 +22,11 @@ public class QrCode { private int height = 250; - private ErrorCorrectionLevel errorCorrectionLevel = ErrorCorrectionLevel.L; + private ErrorCorrectionLevel errorCorrectionLevel = ErrorCorrectionLevel.H; private int onColor = 0xFF000000; - private int offColor = 0xFFFF99FF; + private int offColor = 0xFFFFFFFF; private String charset = "ISO-8859-1"; diff --git a/application-engine/src/main/java/com/netgrif/application/engine/business/qr/QrService.java b/application-engine/src/main/java/com/netgrif/application/engine/business/qr/QrService.java index 6d6e31dd348..7163e877da6 100644 --- a/application-engine/src/main/java/com/netgrif/application/engine/business/qr/QrService.java +++ b/application-engine/src/main/java/com/netgrif/application/engine/business/qr/QrService.java @@ -98,8 +98,6 @@ private File generateFile(QrCode code) throws WriterException, IOException { try (OutputStream outputStream = Files.newOutputStream(outputPath)) { MatrixToImageWriter.writeToStream(bitMatrix, resolveImageFormat(code), outputStream, config); - } catch (IOException e) { - log.error("Failed to write QR code to file [file={}]", code.getFileName(), e); } return outputPath.toFile(); From 6b0a5dd7218aeda2074e102c6ec9d0e74ee41f8f Mon Sep 17 00:00:00 2001 From: Machac Date: Wed, 8 Apr 2026 13:56:26 +0200 Subject: [PATCH 5/6] [NAE-2405] QRService dependency rework - Remove unused IOException logging in `QrService`. - Update `QrCode` defaults: error correction to H, off-color to white. --- .github/workflows/master-build.yml | 43 +++++++++++++++++++++--------- 1 file changed, 31 insertions(+), 12 deletions(-) diff --git a/.github/workflows/master-build.yml b/.github/workflows/master-build.yml index 05b9fc31706..97cb4475dbd 100644 --- a/.github/workflows/master-build.yml +++ b/.github/workflows/master-build.yml @@ -1,15 +1,18 @@ name: Build + on: push: branches: - master + paths-ignore: + - 'docs/**' jobs: build: name: Build runs-on: ubuntu-latest steps: - - uses: actions/checkout@4 + - uses: actions/checkout@v4 with: fetch-depth: 0 @@ -50,21 +53,30 @@ jobs: ports: - 9200:9200 - 9300:9300 - options: -e="discovery.type=single-node" -e="xpack.security.enabled=false" --health-cmd="curl http://localhost:9200/_cluster/health" --health-interval=10s --health-timeout=5s --health-retries=10 + options: >- + -e="discovery.type=single-node" + -e="xpack.security.enabled=false" + --health-cmd="curl http://localhost:9200/_cluster/health" + --health-interval=10s + --health-timeout=5s + --health-retries=10 minio: image: docker.io/bawix/minio:2022 ports: - 9000:9000 - 9001:9001 - options: -e="MINIO_ROOT_USER=root" -e="MINIO_ROOT_PASSWORD=password" -e="MINIO_DEFAULT_BUCKETS=default" + options: >- + -e="MINIO_ROOT_USER=root" + -e="MINIO_ROOT_PASSWORD=password" + -e="MINIO_DEFAULT_BUCKETS=default" steps: - - name: Test Database + - name: Test Elasticsearch health env: ELASTIC_SEARCH_URL: http://localhost:${{ job.services.elasticsearch.ports[9200] }} run: | - echo $ELASTIC_SEARCH_URL + echo "Elasticsearch URL: $ELASTIC_SEARCH_URL" curl -fsSL "$ELASTIC_SEARCH_URL/_cat/health?h=status" - uses: actions/checkout@v4 @@ -92,7 +104,12 @@ jobs: restore-keys: ${{ runner.os }}-m2 - name: Generate certificates - run: cd application-engine/src/main/resources/certificates && openssl genrsa -out keypair.pem 4096 && openssl rsa -in keypair.pem -pubout -out public.crt && openssl pkcs8 -topk8 -inform PEM -outform DER -nocrypt -in keypair.pem -out private.der && cd ../../../../.. + run: | + cd application-engine/src/main/resources/certificates + openssl genrsa -out keypair.pem 4096 + openssl rsa -in keypair.pem -pubout -out public.crt + openssl pkcs8 -topk8 -inform PEM -outform DER -nocrypt -in keypair.pem -out private.der + cd ../../../../.. - name: Build run: mvn clean package install -DskipTests=true @@ -103,7 +120,7 @@ jobs: # SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} # run: mvn -B verify org.sonarsource.scanner.maven:sonar-maven-plugin:sonar -Dsonar.projectKey=netgrif_application-engine - - name: Build, test + - name: Test env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: mvn -B verify @@ -139,8 +156,10 @@ jobs: mvn javadoc:javadoc cp -r ./application-engine/target/reports/apidocs/* ./docs/javadoc/ - - uses: EndBug/add-and-commit@v9 - with: - add: docs - pathspec_error_handling: exitImmediately - message: 'CI - Update documentation' + - name: Commit docs if changed + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git add docs + git diff --staged --quiet || git commit -m "CI - Update documentation" + git push \ No newline at end of file From 85e34255c59e928dbf05308b78066c1c9f014b43 Mon Sep 17 00:00:00 2001 From: Machac Date: Wed, 8 Apr 2026 14:31:15 +0200 Subject: [PATCH 6/6] [NAE-2405] QRService dependency rework - Add error logging for file write failures in `QrService`. - Enhance exception handling with detailed log context. --- .../com/netgrif/application/engine/business/qr/QrService.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/application-engine/src/main/java/com/netgrif/application/engine/business/qr/QrService.java b/application-engine/src/main/java/com/netgrif/application/engine/business/qr/QrService.java index 7163e877da6..c5057deca32 100644 --- a/application-engine/src/main/java/com/netgrif/application/engine/business/qr/QrService.java +++ b/application-engine/src/main/java/com/netgrif/application/engine/business/qr/QrService.java @@ -98,6 +98,9 @@ private File generateFile(QrCode code) throws WriterException, IOException { try (OutputStream outputStream = Files.newOutputStream(outputPath)) { MatrixToImageWriter.writeToStream(bitMatrix, resolveImageFormat(code), outputStream, config); + } catch (IOException e) { + log.error("Failed to write QR code to file [file={}, path={}]", code.getFileName(), outputPath.toAbsolutePath(), e); + throw e; } return outputPath.toFile(); @@ -216,6 +219,7 @@ private void writeImageToFile(BufferedImage image, QrCode code) throws IOExcepti try (OutputStream outputStream = Files.newOutputStream(outputPath)) { boolean written = ImageIO.write(image, format, outputStream); if (!written) { + log.error("Failed to write image to disk [file={}, format={}, path={}]", code.getFileName(), format, outputPath.toAbsolutePath()); throw new IOException("No ImageIO writer found for format: " + format); } }