From 80a757b3ca06f73010b25171b3e6295156859328 Mon Sep 17 00:00:00 2001 From: Andrei Solntsev Date: Wed, 28 Feb 2024 09:23:04 +0200 Subject: [PATCH] simplify API for creating PDF: now user needs to write less code to generate PDF from HTML This commits adds 2 new apis: 1. class Html2Pdf (a single method for generating PDF from HTML) 2. renderer.createPDF(doc, os) (instead of old sequence `setDocument`, `layout`, `createPDF`) --- .../xhtmlrenderer/resource/XMLResource.java | 10 ++++ .../src/main/java/FontGlyphTableRender.java | 6 +-- .../src/main/java/PDFRender.java | 4 +- .../demo/browser/BrowserPanel.java | 50 +++++++++---------- .../pdf/ConcurrentPdfGenerationTest.java | 36 ++----------- .../org/xhtmlrenderer/pdf/PDFRenderTest.java | 10 ++-- .../org/xhtmlrenderer/pdf/SimpleHtmlTest.java | 19 +++---- .../java/org/xhtmlrenderer/pdf/Html2Pdf.java | 40 +++++++++++++++ .../org/xhtmlrenderer/pdf/ITextRenderer.java | 22 +++++++- .../BorderRadiusNonRegressionTest.java | 30 +++-------- .../pdf/bug/EndlessLoopTest.java | 19 +++---- 11 files changed, 122 insertions(+), 124 deletions(-) create mode 100644 flying-saucer-pdf/src/main/java/org/xhtmlrenderer/pdf/Html2Pdf.java diff --git a/flying-saucer-core/src/main/java/org/xhtmlrenderer/resource/XMLResource.java b/flying-saucer-core/src/main/java/org/xhtmlrenderer/resource/XMLResource.java index f81cd0208..f428013d7 100644 --- a/flying-saucer-core/src/main/java/org/xhtmlrenderer/resource/XMLResource.java +++ b/flying-saucer-core/src/main/java/org/xhtmlrenderer/resource/XMLResource.java @@ -48,8 +48,10 @@ import java.io.IOException; import java.io.InputStream; import java.io.Reader; +import java.io.StringReader; import java.lang.ref.Reference; import java.lang.ref.SoftReference; +import java.net.URL; import java.util.Queue; import java.util.concurrent.ArrayBlockingQueue; import java.util.logging.Level; @@ -77,6 +79,10 @@ private XMLResource(InputSource source) { super(source); } + public static XMLResource load(URL source) { + return load(new InputSource(source.toString())); + } + public static XMLResource load(InputStream stream) { return XML_RESOURCE_BUILDER.createXMLResource(new XMLResource(stream)); } @@ -89,6 +95,10 @@ public static XMLResource load(Reader reader) { return XML_RESOURCE_BUILDER.createXMLResource(new XMLResource(new InputSource(reader))); } + public static XMLResource load(String xml) { + return load(new StringReader(xml)); + } + public static XMLResource load(Source source) { return XML_RESOURCE_BUILDER.createXMLResource(source); } diff --git a/flying-saucer-examples/src/main/java/FontGlyphTableRender.java b/flying-saucer-examples/src/main/java/FontGlyphTableRender.java index e1feb33ca..391bb0d42 100644 --- a/flying-saucer-examples/src/main/java/FontGlyphTableRender.java +++ b/flying-saucer-examples/src/main/java/FontGlyphTableRender.java @@ -339,7 +339,7 @@ private void renderPDF(Document doc) { String msgToUser; try (FileOutputStream fos = new FileOutputStream(f)) { BufferedOutputStream bos = new BufferedOutputStream(fos); - renderer.setDocument(doc, null, new XhtmlNamespaceHandler()); + ITextFontResolver resolver = renderer.getFontResolver(); // TODO: encoding is hard-coded as IDENTITY_H; maybe give user option to override resolver.addFont( @@ -347,8 +347,8 @@ private void renderPDF(Document doc) { BaseFont.IDENTITY_H, BaseFont.EMBEDDED ); - renderer.layout(); - renderer.createPDF(bos); + renderer.getSharedContext().setNamespaceHandler(new XhtmlNamespaceHandler()); + renderer.createPDF(doc, bos); msgToUser = "Rendered PDF: " + f.getCanonicalPath(); } catch (DocumentException | IOException e) { diff --git a/flying-saucer-examples/src/main/java/PDFRender.java b/flying-saucer-examples/src/main/java/PDFRender.java index f2375ab0d..31a434d56 100644 --- a/flying-saucer-examples/src/main/java/PDFRender.java +++ b/flying-saucer-examples/src/main/java/PDFRender.java @@ -59,9 +59,7 @@ public static void createPDF(String url, String pdf) throws IOException, Documen Document doc = XMLResource.load(new InputSource(url)).getDocument(); - renderer.setDocument(doc, url); - renderer.layout(); - renderer.createPDF(os); + renderer.createPDF(doc, os); } } diff --git a/flying-saucer-examples/src/main/java/org/xhtmlrenderer/demo/browser/BrowserPanel.java b/flying-saucer-examples/src/main/java/org/xhtmlrenderer/demo/browser/BrowserPanel.java index 370c62546..53730ebde 100755 --- a/flying-saucer-examples/src/main/java/org/xhtmlrenderer/demo/browser/BrowserPanel.java +++ b/flying-saucer-examples/src/main/java/org/xhtmlrenderer/demo/browser/BrowserPanel.java @@ -289,35 +289,31 @@ public void loadPage(final String url_text) { } } - public void exportToPdf( String path ) - { - if (manager.getBaseURL() != null) { - setStatus( "Exporting to " + path + "..." ); - try (OutputStream os = Files.newOutputStream(Paths.get(path))) { - try { - ITextRenderer renderer = new ITextRenderer(); - - DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); - DocumentBuilder db = dbf.newDocumentBuilder(); - Document doc = db.parse(manager.getBaseURL()); - - PDFCreationListener pdfCreationListener = new XHtmlMetaToPdfInfoAdapter( doc ); - renderer.setListener( pdfCreationListener ); - - renderer.setDocument(manager.getBaseURL()); - renderer.layout(); - - renderer.createPDF(os); - setStatus( "Done export." ); + public void exportToPdf(String path) { + if (manager.getBaseURL() != null) { + setStatus("Exporting to " + path + "..."); + try (OutputStream os = Files.newOutputStream(Paths.get(path))) { + try { + ITextRenderer renderer = new ITextRenderer(); + + DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); + DocumentBuilder db = dbf.newDocumentBuilder(); + Document doc = db.parse(manager.getBaseURL()); + + PDFCreationListener pdfCreationListener = new XHtmlMetaToPdfInfoAdapter(doc); + renderer.setListener(pdfCreationListener); + + renderer.createPDF(doc, os); + setStatus("Done export."); + } catch (Exception e) { + XRLog.general(Level.SEVERE, "Could not export PDF.", e); + e.printStackTrace(); + setStatus("Error exporting to PDF."); + } } catch (Exception e) { - XRLog.general(Level.SEVERE, "Could not export PDF.", e); e.printStackTrace(); - setStatus( "Error exporting to PDF." ); - } - } catch (Exception e) { - e.printStackTrace(); - } - } + } + } } private void handlePageLoadFailed(String url_text, XRRuntimeException ex) { diff --git a/flying-saucer-examples/src/test/java/org/xhtmlrenderer/pdf/ConcurrentPdfGenerationTest.java b/flying-saucer-examples/src/test/java/org/xhtmlrenderer/pdf/ConcurrentPdfGenerationTest.java index ef9317ab9..22554210c 100644 --- a/flying-saucer-examples/src/test/java/org/xhtmlrenderer/pdf/ConcurrentPdfGenerationTest.java +++ b/flying-saucer-examples/src/test/java/org/xhtmlrenderer/pdf/ConcurrentPdfGenerationTest.java @@ -1,19 +1,10 @@ package org.xhtmlrenderer.pdf; import com.codeborne.pdftest.PDF; -import com.lowagie.text.DocumentException; import org.junit.jupiter.api.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.w3c.dom.Document; -import org.xhtmlrenderer.resource.FSEntityResolver; -import org.xml.sax.SAXException; -import javax.xml.parsers.DocumentBuilder; -import javax.xml.parsers.DocumentBuilderFactory; -import javax.xml.parsers.ParserConfigurationException; -import java.io.ByteArrayOutputStream; -import java.io.IOException; import java.net.URL; import java.util.ArrayList; import java.util.List; @@ -21,6 +12,7 @@ import java.util.concurrent.ScheduledExecutorService; import static com.codeborne.pdftest.assertj.Assertions.assertThat; +import static java.lang.Thread.currentThread; import static java.util.Objects.requireNonNull; import static java.util.concurrent.Executors.newFixedThreadPool; import static java.util.concurrent.Executors.newScheduledThreadPool; @@ -73,29 +65,9 @@ private void verifyPdf(byte[] pdfBytes) { } private byte[] generatePdf(String htmlPath) { - ITextRenderer renderer = new ITextRenderer(); - renderer.getSharedContext().setMedia("pdf"); - renderer.getSharedContext().setInteractive(false); - renderer.getSharedContext().getTextRenderer().setSmoothingThreshold(0); - - URL htmlUrl = requireNonNull(Thread.currentThread().getContextClassLoader().getResource(htmlPath), () -> "Test resource not found: " + htmlPath); - - try { - DocumentBuilder builder = DocumentBuilderFactory.newInstance().newDocumentBuilder(); - builder.setEntityResolver(FSEntityResolver.instance()); - - Document doc = builder.parse(htmlUrl.openStream()); - - renderer.setDocument(doc, htmlUrl.toString()); - renderer.layout(); - - ByteArrayOutputStream bos = new ByteArrayOutputStream(); - renderer.createPDF(bos); - return bos.toByteArray(); - } - catch (DocumentException | IOException | SAXException | ParserConfigurationException e) { - throw new IllegalArgumentException(e); - } + URL htmlUrl = requireNonNull(currentThread().getContextClassLoader().getResource(htmlPath), + () -> "Test resource not found: " + htmlPath); + return Html2Pdf.fromUrl(htmlUrl); } } diff --git a/flying-saucer-examples/src/test/java/org/xhtmlrenderer/pdf/PDFRenderTest.java b/flying-saucer-examples/src/test/java/org/xhtmlrenderer/pdf/PDFRenderTest.java index 9877f5ec7..e91ac05a7 100644 --- a/flying-saucer-examples/src/test/java/org/xhtmlrenderer/pdf/PDFRenderTest.java +++ b/flying-saucer-examples/src/test/java/org/xhtmlrenderer/pdf/PDFRenderTest.java @@ -7,7 +7,6 @@ import org.slf4j.LoggerFactory; import org.w3c.dom.Document; import org.xhtmlrenderer.resource.XMLResource; -import org.xml.sax.InputSource; import javax.annotation.ParametersAreNonnullByDefault; import java.io.File; @@ -51,16 +50,13 @@ public void convertComplexHtmlToPdf() throws IOException, DocumentException { } private static PDF generatePDF(URL source, File output) throws IOException, DocumentException { + Document doc = XMLResource.load(source).getDocument(); + try (OutputStream os = newOutputStream(output.toPath())) { ITextRenderer renderer = new ITextRenderer(); ResourceLoaderUserAgent callback = new ResourceLoaderUserAgent(renderer.getOutputDevice(), renderer.getSharedContext().getDotsPerPixel()); renderer.getSharedContext().setUserAgentCallback(callback); - - Document doc = XMLResource.load(new InputSource(source.toString())).getDocument(); - - renderer.setDocument(doc, source.toString()); - renderer.layout(); - renderer.createPDF(os); + renderer.createPDF(doc, os); } log.info("Rendered {}{} to PDF: {}", source, lineSeparator(), output.toURI()); return new PDF(output); diff --git a/flying-saucer-examples/src/test/java/org/xhtmlrenderer/pdf/SimpleHtmlTest.java b/flying-saucer-examples/src/test/java/org/xhtmlrenderer/pdf/SimpleHtmlTest.java index ec93f4981..d33e00ce2 100644 --- a/flying-saucer-examples/src/test/java/org/xhtmlrenderer/pdf/SimpleHtmlTest.java +++ b/flying-saucer-examples/src/test/java/org/xhtmlrenderer/pdf/SimpleHtmlTest.java @@ -2,16 +2,16 @@ import com.codeborne.pdftest.PDF; import com.lowagie.text.DocumentException; -import org.apache.pdfbox.io.IOUtils; import org.junit.jupiter.api.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.w3c.dom.Document; +import org.xhtmlrenderer.resource.XMLResource; -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; +import java.io.StringReader; import static com.codeborne.pdftest.assertj.Assertions.assertThat; @@ -20,20 +20,13 @@ public class SimpleHtmlTest { @Test public void simplePdf() throws DocumentException, IOException { - ITextRenderer renderer = new ITextRenderer(); - String htmlContent = "

My First Heading

My first paragraph.

"; - renderer.setDocumentFromString(htmlContent); - - ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); - renderer.layout(); - renderer.createPDF(outputStream); - renderer.finishPDF(); - File file = new File("target/simple.pdf"); try (FileOutputStream o = new FileOutputStream(file)) { - IOUtils.copy(new ByteArrayInputStream(outputStream.toByteArray()), o); + ITextRenderer renderer = new ITextRenderer(); + Document source = XMLResource.load(new StringReader(htmlContent)).getDocument(); + renderer.createPDF(source, o); } log.info("Generated PDF: {}", file.getAbsolutePath()); diff --git a/flying-saucer-pdf/src/main/java/org/xhtmlrenderer/pdf/Html2Pdf.java b/flying-saucer-pdf/src/main/java/org/xhtmlrenderer/pdf/Html2Pdf.java new file mode 100644 index 000000000..9b49c9515 --- /dev/null +++ b/flying-saucer-pdf/src/main/java/org/xhtmlrenderer/pdf/Html2Pdf.java @@ -0,0 +1,40 @@ +package org.xhtmlrenderer.pdf; + +import com.lowagie.text.DocumentException; +import org.w3c.dom.Document; +import org.xhtmlrenderer.resource.FSEntityResolver; +import org.xml.sax.SAXException; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import java.io.IOException; +import java.net.URL; + +import static java.util.Objects.requireNonNull; + +public class Html2Pdf { + public static byte[] fromClasspathResource(String fileName) { + URL htmlUrl = requireNonNull(Thread.currentThread().getContextClassLoader().getResource(fileName), + () -> "Resource not found in classpath: " + fileName); + return fromUrl(htmlUrl); + } + + public static byte[] fromUrl(URL html) { + ITextRenderer renderer = new ITextRenderer(); + renderer.getSharedContext().setMedia("pdf"); + renderer.getSharedContext().setInteractive(false); + renderer.getSharedContext().getTextRenderer().setSmoothingThreshold(0); + + try { + DocumentBuilder builder = DocumentBuilderFactory.newInstance().newDocumentBuilder(); + builder.setEntityResolver(FSEntityResolver.instance()); + + Document doc = builder.parse(html.toString()); + return renderer.createPDF(doc); + } + catch (DocumentException | IOException | SAXException | ParserConfigurationException e) { + throw new IllegalArgumentException(e); + } + } +} diff --git a/flying-saucer-pdf/src/main/java/org/xhtmlrenderer/pdf/ITextRenderer.java b/flying-saucer-pdf/src/main/java/org/xhtmlrenderer/pdf/ITextRenderer.java index e00008610..1c397bdb4 100644 --- a/flying-saucer-pdf/src/main/java/org/xhtmlrenderer/pdf/ITextRenderer.java +++ b/flying-saucer-pdf/src/main/java/org/xhtmlrenderer/pdf/ITextRenderer.java @@ -53,6 +53,7 @@ import javax.xml.transform.stream.StreamResult; import java.awt.*; import java.io.BufferedReader; +import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; import java.io.OutputStream; @@ -180,8 +181,8 @@ public void setDocument(Document doc, String url) { setDocument(doc, url, new XhtmlNamespaceHandler()); } + @Deprecated public void setDocument(File file) throws IOException { - File parent = file.getAbsoluteFile().getParentFile(); setDocument(loadDocument(file.toURI().toURL().toExternalForm()), (parent == null ? "" : parent.toURI().toURL().toExternalForm())); } @@ -197,6 +198,7 @@ public void setDocumentFromString(String content, String baseUrl) { setDocument(dom, baseUrl); } + @Deprecated public void setDocument(Document doc, String url, NamespaceHandler nsh) { _doc = doc; @@ -245,7 +247,6 @@ public int getPDFXConformance(){ return _pdfXConformance == null ? '0' : _pdfXConformance; } - public void layout() { LayoutContext c = newLayoutContext(); BlockBox root = BoxBuilder.createRootBox(c, _doc); @@ -285,6 +286,23 @@ private LayoutContext newLayoutContext() { return result; } + public byte[] createPDF(Document source) throws DocumentException { + setDocument(source, source.getDocumentURI()); + layout(); + + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + createPDF(bos); + finishPDF(); + return bos.toByteArray(); + } + + public void createPDF(Document source, OutputStream os) throws DocumentException { + setDocument(source, source.getDocumentURI()); + layout(); + createPDF(os); + finishPDF(); + } + public void createPDF(OutputStream os) throws DocumentException { createPDF(os, true, 0); } diff --git a/flying-saucer-pdf/src/test/java/org/xhtmlrenderer/pdf/borderradius/BorderRadiusNonRegressionTest.java b/flying-saucer-pdf/src/test/java/org/xhtmlrenderer/pdf/borderradius/BorderRadiusNonRegressionTest.java index 8422127f4..539359dfa 100644 --- a/flying-saucer-pdf/src/test/java/org/xhtmlrenderer/pdf/borderradius/BorderRadiusNonRegressionTest.java +++ b/flying-saucer-pdf/src/test/java/org/xhtmlrenderer/pdf/borderradius/BorderRadiusNonRegressionTest.java @@ -2,16 +2,12 @@ import com.codeborne.pdftest.PDF; import org.junit.jupiter.api.Test; -import org.w3c.dom.Document; -import org.xhtmlrenderer.pdf.ITextRenderer; -import org.xhtmlrenderer.resource.FSEntityResolver; +import org.xhtmlrenderer.pdf.Html2Pdf; -import javax.xml.parsers.DocumentBuilder; -import javax.xml.parsers.DocumentBuilderFactory; -import java.io.ByteArrayOutputStream; import java.net.URL; import static com.codeborne.pdftest.assertj.Assertions.assertThat; +import static java.util.Objects.requireNonNull; public class BorderRadiusNonRegressionTest { @@ -23,24 +19,10 @@ public void borderRadiusWithBorderWidthZero() throws Exception { testNoException("borderRadiusWithBorderWidthZero.html"); } - private void testNoException(String htmlPath) throws Exception { - URL htmlUrl = getClass().getResource(htmlPath); - - DocumentBuilder builder = DocumentBuilderFactory.newInstance().newDocumentBuilder(); - builder.setEntityResolver(FSEntityResolver.instance()); - - Document doc = builder.parse(htmlUrl.openStream()); - - ITextRenderer renderer = new ITextRenderer(); - renderer.getSharedContext().setMedia("pdf"); - - renderer.setDocument(doc, htmlUrl.toString()); - renderer.layout(); - - ByteArrayOutputStream bos = new ByteArrayOutputStream(); - renderer.createPDF(bos); - - assertThat(new PDF(bos.toByteArray())).containsText("Some content"); + private void testNoException(String htmlPath) { + URL htmlUrl = requireNonNull(getClass().getResource(htmlPath), () -> "test resource not found: " + htmlPath); + byte[] pdf = Html2Pdf.fromUrl(htmlUrl); + assertThat(new PDF(pdf)).containsText("Some content"); } } diff --git a/flying-saucer-pdf/src/test/java/org/xhtmlrenderer/pdf/bug/EndlessLoopTest.java b/flying-saucer-pdf/src/test/java/org/xhtmlrenderer/pdf/bug/EndlessLoopTest.java index 02eaf59c0..d3b2433fd 100644 --- a/flying-saucer-pdf/src/test/java/org/xhtmlrenderer/pdf/bug/EndlessLoopTest.java +++ b/flying-saucer-pdf/src/test/java/org/xhtmlrenderer/pdf/bug/EndlessLoopTest.java @@ -3,29 +3,22 @@ import com.codeborne.pdftest.PDF; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Timeout; -import org.xhtmlrenderer.pdf.ITextRenderer; +import org.xhtmlrenderer.pdf.Html2Pdf; -import java.io.ByteArrayOutputStream; -import java.io.File; import java.net.URL; import static com.codeborne.pdftest.assertj.Assertions.assertThat; +import static java.util.Objects.requireNonNull; import static java.util.concurrent.TimeUnit.SECONDS; public class EndlessLoopTest { @Test @Timeout(value = 3, unit = SECONDS) - public void wordwrap() throws Exception { - URL htmlUrl = getClass().getResource("EndlessLoopTest_wordwrap.html"); - File htmlFile = new File(htmlUrl.toURI()); - ITextRenderer renderer = new ITextRenderer(); - renderer.setDocument(htmlFile); - renderer.layout(); - - ByteArrayOutputStream bos = new ByteArrayOutputStream(); - renderer.createPDF(bos); - assertThat(new PDF(bos.toByteArray())).containsText( + public void wordwrap() { + URL htmlUrl = requireNonNull(getClass().getResource("EndlessLoopTest_wordwrap.html")); + byte[] pdf = Html2Pdf.fromUrl(htmlUrl); + assertThat(new PDF(pdf)).containsText( "floated", "word wrapped" );