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

Fix relative background image #441

Merged
merged 2 commits into from
Nov 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
@@ -0,0 +1,95 @@
/*
* {{{ header & license
* Copyright (c) 2007 Patrick Wright
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public License
* as published by the Free Software Foundation; either version 2.1
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
* }}}
*/
package org.xhtmlrenderer.simple;

import org.xhtmlrenderer.swing.Java2DRenderer;
import org.xhtmlrenderer.util.FSImageWriter;

import java.awt.image.BufferedImage;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.net.URL;
import java.nio.file.Paths;

import static java.nio.file.Files.newOutputStream;

/**
* <p>
* ImageRenderer supports rendering of XHTML documents to image formats, writing out the generated image to an output stream
* or a file in a given image format. There are two static utility methods, one for rendering
* a {@link java.net.URL}, {@link #renderToImage(String, String, int)} and one
* for rendering a {@link java.io.File}, {@link #renderToImage(File, String, int)}</p>
*
* <p>You can use this utility from the command line by passing in
* the URL or file location as first parameter, and output file path as second
* parameter:
* <pre>{@code
* java -cp %classpath% org.xhtmlrenderer.simple.ImageRenderer <url> <img>
* }</pre>
* <p>If the second parameters is not provided, a PNG-format image will be created
* in the same directory as the source (if source is a file) or as a temp file
* in the standard temp directory; the output file name will be printed out
* in either case.</p>
*
* <p>Image width must always be supplied; height is determined automatically.</p>
*
* @author Pete Brant
* @author Patrick Wright
*/
public class ImageRenderer {
/**
* Renders the XML file at the given URL as an image file at the target location. Width must be provided,
* height is determined automatically based on content and CSS.
*
* @param url url for the XML file to render
* @param path path to the PDF file to create
* @param width Width in pixels to which the document should be constrained.
*
* @throws java.io.IOException if the input URL, or output path location is invalid
*/
public static BufferedImage renderToImage(String url, String path, int width) throws IOException {
try (OutputStream os = new BufferedOutputStream(newOutputStream(Paths.get(path)))) {
Java2DRenderer renderer = new Java2DRenderer(url, url, width);
BufferedImage image = renderer.getImage();
new FSImageWriter().write(image, os);
return image;
}
}

/**
* Renders the XML file as an image file at the target location. Width must be provided, height is determined
* automatically based on content and CSS.
*
* @param xhtmlFile XML file to render
* @param path path to the image file to create
* @param width Width in pixels to which the document should be constrained.
*
* @throws java.io.IOException if the input URL, or output path location is invalid
*/
public static BufferedImage renderToImage(File xhtmlFile, String path, int width) throws IOException {
return renderToImage(xhtmlFile.toURI().toURL().toExternalForm(), path, width);
}

public static BufferedImage renderToImage(URL xhtmlUrl, String path, int width) throws IOException {
return renderToImage(xhtmlUrl.toExternalForm(), path, width);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -353,10 +353,14 @@ public boolean isFocus(Element e) {

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();
return htmlAsImage(in, widthInPixels);
} catch (IOException | ParserConfigurationException e) {
throw new RuntimeException(e);
}
}

public static BufferedImage htmlAsImage(InputStream source, int widthInPixels) throws SAXException, IOException, ParserConfigurationException {
Document document = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(source);
return new Java2DRenderer(document, widthInPixels).getImage();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
/*
* {{{ header & license
* Copyright (c) 2007 Patrick Wright
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public License
* as published by the Free Software Foundation; either version 2.1
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
* }}}
*/
package org.xhtmlrenderer.util;

import org.jspecify.annotations.Nullable;

import javax.imageio.IIOImage;
import javax.imageio.ImageIO;
import javax.imageio.ImageWriteParam;
import javax.imageio.ImageWriter;
import javax.imageio.stream.ImageOutputStream;
import java.awt.image.BufferedImage;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Iterator;

import static java.nio.file.Files.newOutputStream;

/**
* <p>Writes out BufferedImages to some output stream, like a file. Allows image writer parameters to be specified and
* thus controlled. Uses the java ImageIO libraries--see {@link javax.imageio.ImageIO} and related classes,
* especially {@link javax.imageio.ImageWriter}.</p>
* <p>
* By default, FSImageWriter writes BufferedImages out in PNG format. The simplest possible usage is
* <pre>
* FSImageWriter writer = new FSImageWriter();
* writer.write(img, new File("image.png"));
* </pre>
* <p>
* <p>You can set the image format in the constructor ({@link org.xhtmlrenderer.util.FSImageWriter#FSImageWriter(String)},
* and can set compression settings using various setters; this lets you create writer to reuse across a number
* of images, all output at the same compression level. Note that not all image formats support compression. For
* those that do, you may need to set more than one compression setting, in combination, for it to work. For JPG,
* it might look like this</p>
* <pre>
* writer = new FSImageWriter("jpg");
* writer.setWriteCompressionMode(ImageWriteParam.MODE_EXPLICIT);
* writer.setWriteCompressionType("JPEG");
* writer.setWriteCompressionQuality(.75f);
* </pre>
*/
public class FSImageWriter {
private final String imageFormat;
private final float writeCompressionQuality;
private final int writeCompressionMode;
@Nullable
private final String writeCompressionType;

/**
* New image writer for the PNG image format
*/
public FSImageWriter() {
this("png");
}

/**
* New writer for a given image format, using the informal format name.
*
* @param imageFormat Informal image format name, e.g. "jpg", "png", "bmp"; usually the part that appears
* as the file extension.
*/
public FSImageWriter(String imageFormat) {
this.imageFormat = imageFormat;
this.writeCompressionMode = ImageWriteParam.MODE_COPY_FROM_METADATA;
this.writeCompressionType = null;
this.writeCompressionQuality = 1.0f;
}

/**
* Writes the image out to the target file, creating the file if necessary, or overwriting if it already
* exists.
*
* @param image Image to write.
* @param filePath Path for file to write. The extension for the file name is not changed; it is up to the
* caller to make sure this corresponds to the image format.
* @throws IOException If the file could not be written.
*/
public void write(BufferedImage image, String filePath) throws IOException {
File file = new File(filePath);
if (file.exists()) {
if (!file.delete()) {
throw new IOException("File " + filePath + " exists already, and call to .delete() failed " +
"unexpectedly");
}
} else {
if (!file.createNewFile()) {
throw new IOException("Unable to create file at path " + filePath + ", call to .createNewFile() " +
"failed unexpectedly.");
}
}

try (OutputStream fos = new BufferedOutputStream(newOutputStream(file.toPath()))) {
write(image, fos);
}
}

/**
* Writes the image out to the target file, creating the file if necessary, or overwriting if it already
* exists.
*
* @param image Image to write.
* @param os output stream to write to
* @throws IOException If the file could not be written.
*/
public void write(BufferedImage image, OutputStream os) throws IOException {
ImageWriter writer = lookupImageWriterForFormat(imageFormat);

try (ImageOutputStream ios = ImageIO.createImageOutputStream(os)) {
writer.setOutput(ios);
ImageWriteParam parameters = getImageWriteParameters(writer);

writer.write(null, new IIOImage(image, null, null), parameters);
ios.flush();
} finally {
writer.dispose();
}
}

/**
* Returns the image output parameters to control the output image quality, compression, etc. By default,
* this uses the compression values set in this class. Override this method to get full control over the
* ImageWriteParam used in image output.
*
* @param writer The ImageWriter we are going to use for image output.
* @return ImageWriteParam configured for image output.
*/
protected ImageWriteParam getImageWriteParameters(ImageWriter writer) {
ImageWriteParam param = writer.getDefaultWriteParam();
if (param.canWriteCompressed()) {
if (writeCompressionMode != ImageWriteParam.MODE_COPY_FROM_METADATA) {
param.setCompressionMode(writeCompressionMode);

// see docs for IWP--only allowed to set type and quality if mode is EXPLICIT
if (writeCompressionMode == ImageWriteParam.MODE_EXPLICIT) {
param.setCompressionType(writeCompressionType);
param.setCompressionQuality(writeCompressionQuality);
}

}
}

return param;
}

/**
* Utility method to find an image writer.
*
* @param imageFormat String informal format name, "jpg"
* @return ImageWriter corresponding to that format
*/
private ImageWriter lookupImageWriterForFormat(String imageFormat) {
Iterator<ImageWriter> iter = ImageIO.getImageWritersByFormatName(imageFormat);
if (iter.hasNext()) {
return iter.next();
}
throw new IllegalArgumentException("Image writer not found for format " + imageFormat);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package org.xhtmlrenderer.simple;

import org.junit.jupiter.api.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.awt.image.BufferedImage;
import java.io.File;

import static java.util.Objects.requireNonNull;
import static org.assertj.core.api.Assertions.assertThat;

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

@Test
public void backgroundImageWithRelativePath() throws Exception {
File result = new File("target/%s.png".formatted(getClass().getSimpleName()));
BufferedImage image = ImageRenderer.renderToImage(requireNonNull(getClass().getResource("/text-with-background-image.xhtml")), result.getAbsolutePath(), 600);
assertThat(image.getWidth()).isEqualTo(600);
assertThat(image.getHeight()).isBetween(100, 200);
log.info("Generated image from html: {}", result.getAbsolutePath());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,38 +8,15 @@
import java.awt.image.BufferedImage;
import java.io.File;

import static java.util.Objects.requireNonNull;
import static org.xhtmlrenderer.swing.Java2DRenderer.htmlAsImage;

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

//currently we cannot display different quotes based on depth
private static final String DOCUMENT =
"""
<html>
<head>
<style type='text/css'><![CDATA[
* { quotes: '"' '"' "'" "'" }
q:before { content: open-quote }
q:after { content: close-quote }
blockquote p:before { content: open-quote }
blockquote p:after { content: no-close-quote }
blockquote p.last:after { content: close-quote }
]]></style>
</head>
<body>
<blockquote>
<p>This is just a test of the emergency <q>quoting</q> system.</p>
<p>This is only a test.</p>
<p class='last'>Thank you for your cooperation during this <q>test.</q></p>
</blockquote>
</body>
</html>
""";

@Test
public void exampleWithQuotes() throws Exception {
BufferedImage image = htmlAsImage(DOCUMENT, 600);
BufferedImage image = htmlAsImage(requireNonNull(getClass().getResourceAsStream("/quotes.xhtml")), 600);
File result = new File("target/%s.png".formatted(getClass().getSimpleName()));
ImageIO.write(image, "png", result);
log.info("Generated image from html: {}", result.getAbsolutePath());
Expand Down
23 changes: 23 additions & 0 deletions flying-saucer-core/src/test/resources/quotes.xhtml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<!--
currently we cannot display different quotes based on depth
-->
<html lang="en">
<head>
<style type='text/css'><![CDATA[
* { quotes: '"' '"' "'" "'" }
q:before { content: open-quote }
q:after { content: close-quote }
blockquote p:before { content: open-quote }
blockquote p:after { content: no-close-quote }
blockquote p.last:after { content: close-quote }
]]></style>
<title>Page with quotes</title>
</head>
<body>
<blockquote>
<p>This is just a test of the emergency <q>quoting</q> system.</p>
<p>This is only a test.</p>
<p class='last'>Thank you for your cooperation during this <q>test.</q></p>
</blockquote>
</body>
</html>
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<html lang="en">
<head>
<title>Page with background image</title>
</head>
<body>

<p>AAA AAA AAA AAA AAA AAA </p>
<p style="background-image: url(transgrey.png); background-repeat: no-repeat;">BBB BBB BBB BBB BBB BBB BBB </p>
<p class='last'>CCC CCC CCC CCC CCC CCC CCC </p>

</body>
</html>
Binary file added flying-saucer-core/src/test/resources/transgrey.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.