Skip to content

Commit

Permalink
#392 refactor code for html2image conversion
Browse files Browse the repository at this point in the history
the test is taken from PR #393 comments.
  • Loading branch information
asolntsev committed Sep 29, 2024
1 parent fc6dede commit 91b3113
Show file tree
Hide file tree
Showing 7 changed files with 116 additions and 14 deletions.
1 change: 1 addition & 0 deletions .idea/inspectionProfiles/Project_Default.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Changelog

## 9.9.5 (under construction) - see https://github.com/flyingsaucerproject/flyingsaucer/milestone/21
*
* #392 Fix transparent background of resized base64 encoded images - thanks to @Openhelios (#393)

## 9.9.4 (20.09.2024) - see https://github.com/flyingsaucerproject/flyingsaucer/milestone/20?closed=1
* Set "Page only" as default initial view in PDF viewer (#390)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,18 @@
import org.xhtmlrenderer.simple.extend.XhtmlNamespaceHandler;
import org.xhtmlrenderer.util.Configuration;
import org.xhtmlrenderer.util.ImageUtil;
import org.xml.sax.SAXException;

import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;

import static java.nio.charset.StandardCharsets.UTF_8;

/**
* <p>Renders an XML files, formatted with CSS, as an image. Input is a document in the form of file or URL,
Expand Down Expand Up @@ -352,4 +359,13 @@ public boolean isFocus(Element e) {
return false;
}
}

public static BufferedImage htmlAsImage(String html, int widthInPixels) throws SAXException {
try (InputStream in = new ByteArrayInputStream(html.getBytes(UTF_8))) {
Document document = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(in);
return new Java2DRenderer(document, widthInPixels).getImage();
} catch (IOException | ParserConfigurationException e) {
throw new RuntimeException(e);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,13 @@
import java.util.Map;
import java.util.logging.Level;

import static java.awt.Transparency.OPAQUE;
import static java.awt.Transparency.TRANSLUCENT;
import static java.awt.image.BufferedImage.TYPE_4BYTE_ABGR;
import static java.awt.image.BufferedImage.TYPE_4BYTE_ABGR_PRE;
import static java.awt.image.BufferedImage.TYPE_INT_ARGB;
import static java.awt.image.BufferedImage.TYPE_INT_ARGB_PRE;

/**
* Static utility methods for working with images. Meant to suggest "best practices" for the most straightforward
* cases of working with images.
Expand Down Expand Up @@ -98,23 +105,24 @@ public static BufferedImage makeCompatible(BufferedImage image) {
@Nonnull
@CheckReturnValue
public static BufferedImage createCompatibleBufferedImage(int width, int height, int biType) {
final BufferedImage image;

GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment();
if (ge.isHeadlessInstance()) {
image = new BufferedImage(width, height, biType);
return new BufferedImage(width, height, biType);
} else {
GraphicsConfiguration gc = getGraphicsConfiguration();

// TODO: check type using image type - can be sniffed; see Filthy Rich Clients
int type = (biType == BufferedImage.TYPE_INT_ARGB || biType == BufferedImage.TYPE_INT_ARGB_PRE
|| biType == BufferedImage.TYPE_4BYTE_ABGR || biType == BufferedImage.TYPE_4BYTE_ABGR_PRE ?
Transparency.TRANSLUCENT : Transparency.OPAQUE);

image = gc.createCompatibleImage(width, height, type);
return gc.createCompatibleImage(width, height, detectTransparency(biType));
}
}

return image;
static int detectTransparency(int biType) {
// TODO: check type using image type - can be sniffed; see Filthy Rich Clients
return switch (biType) {
case TYPE_INT_ARGB,
TYPE_INT_ARGB_PRE,
TYPE_4BYTE_ABGR,
TYPE_4BYTE_ABGR_PRE -> TRANSLUCENT;
default -> OPAQUE;
};
}

@Nonnull
Expand Down Expand Up @@ -204,7 +212,7 @@ public static BufferedImage getScaledInstance(BufferedImage orgImage, int target
Object hint = Configuration.valueFromClassConstant("xr.image.render-quality",
RenderingHints.VALUE_INTERPOLATION_BICUBIC);

ScalingOptions opt = new ScalingOptions(targetWidth, targetHeight, BufferedImage.TYPE_INT_ARGB, quality, hint);
ScalingOptions opt = new ScalingOptions(targetWidth, targetHeight, TYPE_INT_ARGB, quality, hint);

return getScaledInstance(opt, orgImage);
}
Expand Down Expand Up @@ -236,7 +244,7 @@ public static BufferedImage convertToBufferedImage(Image awtImg, int type) {
@Nonnull
@CheckReturnValue
public static BufferedImage createTransparentImage(int width, int height) {
BufferedImage bi = createCompatibleBufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
BufferedImage bi = createCompatibleBufferedImage(width, height, TYPE_INT_ARGB);
Graphics2D g2d = bi.createGraphics();

// Make all filled pixels transparent
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package org.xhtmlrenderer.swing;

import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.util.Base64;

import static java.awt.Color.RED;
import static java.awt.Color.WHITE;
import static java.awt.image.BufferedImage.TYPE_4BYTE_ABGR;
import static java.awt.image.BufferedImage.TYPE_4BYTE_ABGR_PRE;
import static java.awt.image.BufferedImage.TYPE_INT_ARGB;
import static java.awt.image.BufferedImage.TYPE_INT_ARGB_PRE;
import static org.junit.jupiter.api.Assertions.assertEquals;

class Java2DRendererTest {
private static final Logger log = LoggerFactory.getLogger(Java2DRendererTest.class);

@ParameterizedTest
@ValueSource(ints = {TYPE_INT_ARGB, TYPE_INT_ARGB_PRE, TYPE_4BYTE_ABGR, TYPE_4BYTE_ABGR_PRE})
void convertHtmlToImage_withResizedEmbeddedABGRImage(int imageType) throws Exception {
BufferedImage smallImg = create2PixelImage(imageType);
String html = """
<html><body>
<img width="100" src="data:image/png;base64,%s"/>
<h1>Hello</h1></body></html>""".formatted(imageBase64(smallImg));
BufferedImage htmlAsImage = Java2DRenderer.htmlAsImage(html, 100);

File result = new File("target/%s.%s.png".formatted(getClass().getSimpleName(), imageType));
ImageIO.write(htmlAsImage, "png", result);
log.info("Generated image from html: {}", result.getAbsolutePath());

assertEquals(RED.getRGB(), htmlAsImage.getRGB(75, 25));
assertEquals(WHITE.getRGB(), htmlAsImage.getRGB(25, 25));
}

private static BufferedImage create2PixelImage(int imageType) {
BufferedImage img = new BufferedImage(2, 1, imageType);
img.setRGB(0, 0, 0);
img.setRGB(1, 0, 0xFFFF0000);
return img;
}

private static String imageBase64(BufferedImage img) throws IOException {
try (ByteArrayOutputStream bytes = new ByteArrayOutputStream(96)) {
ImageIO.write(img, "png", bytes);
return Base64.getEncoder().encodeToString(bytes.toByteArray());
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@

import java.awt.image.BufferedImage;

import static java.awt.Transparency.OPAQUE;
import static java.awt.Transparency.TRANSLUCENT;
import static org.assertj.core.api.Assertions.assertThat;
import static org.xhtmlrenderer.util.ImageUtil.detectTransparency;
import static org.xhtmlrenderer.util.ImageUtil.isEmbeddedBase64Image;
import static org.xhtmlrenderer.util.ImageUtil.loadEmbeddedBase64Image;

Expand All @@ -30,4 +33,16 @@ void embeddedBase64Image() {
assertThat(image.getWidth()).isEqualTo(352);
assertThat(image.getHeight()).isEqualTo(186);
}

@Test
void detectsImageType() {
assertThat(detectTransparency(BufferedImage.TYPE_INT_ARGB)).isEqualTo(TRANSLUCENT);
assertThat(detectTransparency(BufferedImage.TYPE_INT_ARGB_PRE)).isEqualTo(TRANSLUCENT);
assertThat(detectTransparency(BufferedImage.TYPE_4BYTE_ABGR)).isEqualTo(TRANSLUCENT);
assertThat(detectTransparency(BufferedImage.TYPE_4BYTE_ABGR_PRE)).isEqualTo(TRANSLUCENT);

assertThat(detectTransparency(BufferedImage.TYPE_3BYTE_BGR)).isEqualTo(OPAQUE);
assertThat(detectTransparency(BufferedImage.TYPE_CUSTOM)).isEqualTo(OPAQUE);
assertThat(detectTransparency(BufferedImage.TYPE_INT_RGB)).isEqualTo(OPAQUE);
}
}
5 changes: 5 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,11 @@
<artifactId>junit-jupiter-api</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-params</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
Expand Down

0 comments on commit 91b3113

Please sign in to comment.