Skip to content

Commit

Permalink
ZXing and QRCodeGen tests
Browse files Browse the repository at this point in the history
  • Loading branch information
melloware committed Oct 31, 2024
1 parent 47c688b commit 6ebf3a7
Show file tree
Hide file tree
Showing 14 changed files with 599 additions and 1 deletion.
103 changes: 103 additions & 0 deletions ext-qrcodegen/integration-tests/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<parent>
<groupId>io.quarkiverse.barcode</groupId>
<artifactId>quarkus-qrcodegen-parent</artifactId>
<version>999-SNAPSHOT</version>
</parent>
<artifactId>quarkus-qrcodegen-integration-tests</artifactId>
<name>Quarkus QR Code Generator - Integration Tests</name>

<properties>
<skipITs>true</skipITs>
</properties>

<dependencies>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-rest</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-smallrye-openapi</artifactId>
</dependency>
<dependency>
<groupId>io.quarkiverse.barcode</groupId>
<artifactId>quarkus-qrcodegen</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-junit5</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.rest-assured</groupId>
<artifactId>rest-assured</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-maven-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>build</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-failsafe-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>integration-test</goal>
<goal>verify</goal>
</goals>
</execution>
</executions>
<configuration>
<systemPropertyVariables>
<native.image.path>${project.build.directory}/${project.build.finalName}-runner</native.image.path>
<java.util.logging.manager>org.jboss.logmanager.LogManager</java.util.logging.manager>
<maven.home>${maven.home}</maven.home>
</systemPropertyVariables>
</configuration>
</plugin>
</plugins>
</build>

<profiles>
<profile>
<id>native-image</id>
<activation>
<property>
<name>native</name>
</property>
</activation>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<skipTests>${native.surefire.skip}</skipTests>
</configuration>
</plugin>
</plugins>
</build>
<properties>
<skipITs>false</skipITs>
<quarkus.native.enabled>true</quarkus.native.enabled>
</properties>
</profile>
</profiles>
</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package io.quarkiverse.barcode.it;

import jakarta.ws.rs.core.HttpHeaders;
import jakarta.ws.rs.core.Response;

public class BaseImageResource {
public static final String CONTENT_DISPOSITION = "attachment;filename=%s";
public static final String PNG_MIME_TYPE = "image/png";
public static final String SVG_MIME_TYPE = "image/svg+xml";

/**
* Builds a Response containing image data with appropriate headers.
*
* @param imageData The raw bytes of the image
* @param mimeType The MIME type of the image (e.g. "image/png")
* @param fileName The filename to use in the Content-Disposition header
* @return A Response object configured with the image data and headers
*/
protected Response buildImageResponse(byte[] imageData, String mimeType, String fileName) {
final Response.ResponseBuilder response = Response.ok(imageData);
response.header(HttpHeaders.CONTENT_DISPOSITION, CONTENT_DISPOSITION.formatted(fileName));
response.header(HttpHeaders.CONTENT_TYPE, mimeType);
return response.build();
}

protected String stripWhitespace(String svgContent) {
// Remove unnecessary whitespace characters (spaces, tabs, newlines) from SVG content
return svgContent
.replaceAll(">\\s+<", "><") // Remove whitespace between tags
.replaceAll("\\s{2,}", " ") // Replace multiple spaces with a single space
.replaceAll("\\s+/>", "/>"); // Trim spaces before self-closing tags
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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 io.quarkiverse.barcode.it;

import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Objects;

import javax.imageio.ImageIO;

import jakarta.enterprise.context.ApplicationScoped;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;

import org.eclipse.microprofile.openapi.annotations.enums.SchemaType;
import org.eclipse.microprofile.openapi.annotations.media.Content;
import org.eclipse.microprofile.openapi.annotations.media.Schema;
import org.eclipse.microprofile.openapi.annotations.responses.APIResponse;

import io.nayuki.qrcodegen.QrCode;

@Path("/qrcodegen")
@ApplicationScoped
public class QrCodeGenResource extends BaseImageResource {

@GET
@Path("qr/png")
@APIResponse(responseCode = "200", description = "Document downloaded", content = @Content(mediaType = MediaType.APPLICATION_OCTET_STREAM, schema = @Schema(type = SchemaType.STRING, format = "binary")))
public Response qrCodePng() throws IOException {
try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
QrCode qr = QrCode.encodeText("Quarkus Rocks!", QrCode.Ecc.MEDIUM);
BufferedImage img = toImage(qr, 4, 10, 0xFFE0E0, 0x602020);
ImageIO.write(img, "png", outputStream);

// return the image
return buildImageResponse(outputStream.toByteArray(), PNG_MIME_TYPE, "qrcodegen.png");
}
}

@GET
@Path("qr/svg")
@APIResponse(responseCode = "200", description = "Document downloaded", content = @Content(mediaType = MediaType.APPLICATION_OCTET_STREAM, schema = @Schema(type = SchemaType.STRING, format = "binary")))
public Response qrCodeSvg() {
QrCode qr = QrCode.encodeText("Quarkus Rocks!", QrCode.Ecc.MEDIUM);
String svg = stripWhitespace(toSvgString(qr, 4, "#FFFFFF", "#000000")); // Convert to SVG XML code

// return the image
return buildImageResponse(svg.getBytes(StandardCharsets.UTF_8), SVG_MIME_TYPE, "qrcodegen.svg");
}

/**
* Returns a raster image depicting the specified QR Code, with
* the specified module scale, border modules, and module colors.
* <p>
* For example, scale=10 and border=4 means to pad the QR Code with 4 light border
* modules on all four sides, and use 10&#xD7;10 pixels to represent each module.
*
* @param qr the QR Code to render (not {@code null})
* @param scale the side length (measured in pixels, must be positive) of each module
* @param border the number of border modules to add, which must be non-negative
* @param lightColor the color to use for light modules, in 0xRRGGBB format
* @param darkColor the color to use for dark modules, in 0xRRGGBB format
* @return a new image representing the QR Code, with padding and scaling
* @throws NullPointerException if the QR Code is {@code null}
* @throws IllegalArgumentException if the scale or border is out of range, or if
* {scale, border, size} cause the image dimensions to exceed Integer.MAX_VALUE
*/
private static BufferedImage toImage(QrCode qr, int scale, int border, int lightColor, int darkColor) {
Objects.requireNonNull(qr);
if (scale <= 0 || border < 0)
throw new IllegalArgumentException("Value out of range");
if (border > Integer.MAX_VALUE / 2 || qr.size + border * 2L > Integer.MAX_VALUE / scale)
throw new IllegalArgumentException("Scale or border too large");

BufferedImage result = new BufferedImage((qr.size + border * 2) * scale, (qr.size + border * 2) * scale,
BufferedImage.TYPE_INT_RGB);
for (int y = 0; y < result.getHeight(); y++) {
for (int x = 0; x < result.getWidth(); x++) {
boolean color = qr.getModule(x / scale - border, y / scale - border);
result.setRGB(x, y, color ? darkColor : lightColor);
}
}
return result;
}

/**
* Returns a string of SVG code for an image depicting the specified QR Code, with the specified
* number of border modules. The string always uses Unix newlines (\n), regardless of the platform.
*
* @param qr the QR Code to render (not {@code null})
* @param border the number of border modules to add, which must be non-negative
* @param lightColor the color to use for light modules, in any format supported by CSS, not {@code null}
* @param darkColor the color to use for dark modules, in any format supported by CSS, not {@code null}
* @return a string representing the QR Code as an SVG XML document
* @throws NullPointerException if any object is {@code null}
* @throws IllegalArgumentException if the border is negative
*/
private static String toSvgString(QrCode qr, int border, String lightColor, String darkColor) {
Objects.requireNonNull(qr);
Objects.requireNonNull(lightColor);
Objects.requireNonNull(darkColor);
if (border < 0)
throw new IllegalArgumentException("Border must be non-negative");
StringBuilder sb = new StringBuilder()
.append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n")
.append("<!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 1.1//EN\" \"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd\">\n")
.append(String.format(
"<svg xmlns=\"http://www.w3.org/2000/svg\" version=\"1.1\" viewBox=\"0 0 %1$d %1$d\" stroke=\"none\">\n",
qr.size + (long) border * 2))
.append("\t<rect width=\"100%\" height=\"100%\" fill=\"").append(lightColor).append("\"/>\n")
.append("\t<path d=\"");
for (int y = 0; y < qr.size; y++) {
for (int x = 0; x < qr.size; x++) {
if (qr.getModule(x, y)) {
if (x != 0 || y != 0)
sb.append(" ");
sb.append(String.format("M%d,%dh1v1h-1z", x + (long) border, y + (long) border));
}
}
}
return sb.append("\" fill=\"").append(darkColor).append("\"/>\n")
.append("</svg>\n")
.toString();
}
}
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package io.quarkiverse.barcode.it;

import io.quarkus.test.junit.QuarkusIntegrationTest;

@QuarkusIntegrationTest
public class QrCodeGenResourceIT extends QrCodeGenResourceTest {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package io.quarkiverse.barcode.it;

import static io.restassured.RestAssured.given;
import static jakarta.ws.rs.core.HttpHeaders.CONTENT_LENGTH;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.greaterThan;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.notNullValue;

import org.junit.jupiter.api.Test;

import io.quarkus.test.junit.QuarkusTest;

@QuarkusTest
public class QrCodeGenResourceTest {

@Test
public void testQrCodeGenPng() {
given()
.when().get("/qrcodegen/qr/png")
.then()
.statusCode(200)
.contentType(equalTo(BaseImageResource.PNG_MIME_TYPE))
.header(CONTENT_LENGTH, Integer::parseInt, greaterThan(0))
.body(notNullValue());
}

@Test
public void testQrCodeGenSvg() {
given()
.when().get("/qrcodegen/qr/svg")
.then()
.statusCode(200)
.contentType(equalTo(BaseImageResource.SVG_MIME_TYPE))
.header(CONTENT_LENGTH, Integer::parseInt, greaterThan(0))
.body(is(
"""
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 29 29" stroke="none"><rect width="100%" height="100%" fill="#FFFFFF"/><path d="M4,4h1v1h-1z M5,4h1v1h-1z M6,4h1v1h-1z M7,4h1v1h-1z M8,4h1v1h-1z M9,4h1v1h-1z M10,4h1v1h-1z M12,4h1v1h-1z M15,4h1v1h-1z M18,4h1v1h-1z M19,4h1v1h-1z M20,4h1v1h-1z M21,4h1v1h-1z M22,4h1v1h-1z M23,4h1v1h-1z M24,4h1v1h-1z M4,5h1v1h-1z M10,5h1v1h-1z M12,5h1v1h-1z M13,5h1v1h-1z M16,5h1v1h-1z M18,5h1v1h-1z M24,5h1v1h-1z M4,6h1v1h-1z M6,6h1v1h-1z M7,6h1v1h-1z M8,6h1v1h-1z M10,6h1v1h-1z M13,6h1v1h-1z M15,6h1v1h-1z M18,6h1v1h-1z M20,6h1v1h-1z M21,6h1v1h-1z M22,6h1v1h-1z M24,6h1v1h-1z M4,7h1v1h-1z M6,7h1v1h-1z M7,7h1v1h-1z M8,7h1v1h-1z M10,7h1v1h-1z M12,7h1v1h-1z M13,7h1v1h-1z M14,7h1v1h-1z M18,7h1v1h-1z M20,7h1v1h-1z M21,7h1v1h-1z M22,7h1v1h-1z M24,7h1v1h-1z M4,8h1v1h-1z M6,8h1v1h-1z M7,8h1v1h-1z M8,8h1v1h-1z M10,8h1v1h-1z M13,8h1v1h-1z M14,8h1v1h-1z M15,8h1v1h-1z M18,8h1v1h-1z M20,8h1v1h-1z M21,8h1v1h-1z M22,8h1v1h-1z M24,8h1v1h-1z M4,9h1v1h-1z M10,9h1v1h-1z M13,9h1v1h-1z M18,9h1v1h-1z M24,9h1v1h-1z M4,10h1v1h-1z M5,10h1v1h-1z M6,10h1v1h-1z M7,10h1v1h-1z M8,10h1v1h-1z M9,10h1v1h-1z M10,10h1v1h-1z M12,10h1v1h-1z M14,10h1v1h-1z M16,10h1v1h-1z M18,10h1v1h-1z M19,10h1v1h-1z M20,10h1v1h-1z M21,10h1v1h-1z M22,10h1v1h-1z M23,10h1v1h-1z M24,10h1v1h-1z M12,11h1v1h-1z M4,12h1v1h-1z M6,12h1v1h-1z M7,12h1v1h-1z M9,12h1v1h-1z M10,12h1v1h-1z M11,12h1v1h-1z M14,12h1v1h-1z M18,12h1v1h-1z M21,12h1v1h-1z M23,12h1v1h-1z M24,12h1v1h-1z M5,13h1v1h-1z M8,13h1v1h-1z M9,13h1v1h-1z M12,13h1v1h-1z M13,13h1v1h-1z M19,13h1v1h-1z M20,13h1v1h-1z M21,13h1v1h-1z M22,13h1v1h-1z M23,13h1v1h-1z M24,13h1v1h-1z M5,14h1v1h-1z M7,14h1v1h-1z M8,14h1v1h-1z M10,14h1v1h-1z M12,14h1v1h-1z M15,14h1v1h-1z M18,14h1v1h-1z M23,14h1v1h-1z M24,14h1v1h-1z M5,15h1v1h-1z M6,15h1v1h-1z M11,15h1v1h-1z M13,15h1v1h-1z M14,15h1v1h-1z M15,15h1v1h-1z M16,15h1v1h-1z M19,15h1v1h-1z M21,15h1v1h-1z M6,16h1v1h-1z M8,16h1v1h-1z M10,16h1v1h-1z M11,16h1v1h-1z M13,16h1v1h-1z M16,16h1v1h-1z M17,16h1v1h-1z M19,16h1v1h-1z M12,17h1v1h-1z M14,17h1v1h-1z M16,17h1v1h-1z M18,17h1v1h-1z M19,17h1v1h-1z M20,17h1v1h-1z M21,17h1v1h-1z M23,17h1v1h-1z M24,17h1v1h-1z M4,18h1v1h-1z M5,18h1v1h-1z M6,18h1v1h-1z M7,18h1v1h-1z M8,18h1v1h-1z M9,18h1v1h-1z M10,18h1v1h-1z M12,18h1v1h-1z M13,18h1v1h-1z M15,18h1v1h-1z M17,18h1v1h-1z M18,18h1v1h-1z M19,18h1v1h-1z M20,18h1v1h-1z M21,18h1v1h-1z M4,19h1v1h-1z M10,19h1v1h-1z M12,19h1v1h-1z M14,19h1v1h-1z M15,19h1v1h-1z M17,19h1v1h-1z M21,19h1v1h-1z M22,19h1v1h-1z M24,19h1v1h-1z M4,20h1v1h-1z M6,20h1v1h-1z M7,20h1v1h-1z M8,20h1v1h-1z M10,20h1v1h-1z M13,20h1v1h-1z M14,20h1v1h-1z M15,20h1v1h-1z M16,20h1v1h-1z M19,20h1v1h-1z M22,20h1v1h-1z M23,20h1v1h-1z M4,21h1v1h-1z M6,21h1v1h-1z M7,21h1v1h-1z M8,21h1v1h-1z M10,21h1v1h-1z M12,21h1v1h-1z M13,21h1v1h-1z M14,21h1v1h-1z M16,21h1v1h-1z M18,21h1v1h-1z M19,21h1v1h-1z M23,21h1v1h-1z M4,22h1v1h-1z M6,22h1v1h-1z M7,22h1v1h-1z M8,22h1v1h-1z M10,22h1v1h-1z M12,22h1v1h-1z M14,22h1v1h-1z M15,22h1v1h-1z M16,22h1v1h-1z M17,22h1v1h-1z M18,22h1v1h-1z M4,23h1v1h-1z M10,23h1v1h-1z M16,23h1v1h-1z M17,23h1v1h-1z M18,23h1v1h-1z M19,23h1v1h-1z M20,23h1v1h-1z M24,23h1v1h-1z M4,24h1v1h-1z M5,24h1v1h-1z M6,24h1v1h-1z M7,24h1v1h-1z M8,24h1v1h-1z M9,24h1v1h-1z M10,24h1v1h-1z M12,24h1v1h-1z M13,24h1v1h-1z M17,24h1v1h-1z M18,24h1v1h-1z M22,24h1v1h-1z" fill="#000000"/></svg>
"""));
}
}
16 changes: 15 additions & 1 deletion ext-qrcodegen/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,18 @@
<module>deployment</module>
<module>runtime</module>
</modules>
</project>
<profiles>
<profile>
<id>it</id>
<activation>
<property>
<name>performRelease</name>
<value>!true</value>
</property>
</activation>
<modules>
<module>integration-tests</module>
</modules>
</profile>
</profiles>
</project>
Loading

0 comments on commit 6ebf3a7

Please sign in to comment.