diff --git a/openpdf-html/src/main/java/org/openpdf/html/HtmlToPdfBatchUtils.java b/openpdf-html/src/main/java/org/openpdf/html/HtmlToPdfBatchUtils.java index a03d04ae6..8769fefdf 100644 --- a/openpdf-html/src/main/java/org/openpdf/html/HtmlToPdfBatchUtils.java +++ b/openpdf-html/src/main/java/org/openpdf/html/HtmlToPdfBatchUtils.java @@ -54,6 +54,7 @@ import java.io.FileOutputStream; import java.io.IOException; +import java.io.OutputStream; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; @@ -92,12 +93,12 @@ * *

Convert a single HTML string to PDF

*
{@code
- * Path pdf = Html2PdfBatchUtils.renderHtmlString(
+ * Path pdf = HtmlToPdfBatchUtils.renderHtmlString(
  *     "Hello World",
  *     "https://example.com/",
  *     Path.of("out.pdf"),
- *     Html2PdfBatchUtils.CSS_A4_20MM,
- *     Html2PdfBatchUtils.setDpi(150)
+ *     HtmlToPdfBatchUtils.CSS_A4_20MM,
+ *     HtmlToPdfBatchUtils.setDpi(150)
  * );
  * }
* @@ -105,14 +106,14 @@ *
{@code
  * List jobs = List.of(
  *     new HtmlFileJob(Path.of("file1.html"), Path.of("."), Path.of("file1.pdf"),
- *                     Optional.of(Html2PdfBatchUtils.CSS_LETTER_HALF_IN),
+ *                     Optional.of(HtmlToPdfBatchUtils.CSS_LETTER_HALF_IN),
  *                     Optional.empty()),
  *     new HtmlFileJob(Path.of("file2.html"), Path.of("."), Path.of("file2.pdf"),
  *                     Optional.empty(),
- *                     Optional.of(Html2PdfBatchUtils.registerFontDir(Path.of("fonts"))))
+ *                     Optional.of(HtmlToPdfBatchUtils.registerFontDir(Path.of("fonts"))))
  * );
  *
- * BatchResult result = Html2PdfBatchUtils.batchHtmlFiles(jobs,
+ * BatchResult result = HtmlToPdfBatchUtils.batchHtmlFiles(jobs,
  *     path -> System.out.println("Created PDF: " + path),
  *     error -> error.printStackTrace()
  * );
@@ -124,6 +125,9 @@
  *   
  • Always provide a {@code baseUri} (or {@code baseDir}) if your HTML references relative resources.
  • *
  • Use {@code rendererCustomizer} to adjust advanced rendering settings like DPI or font loading.
  • *
  • Batch methods allow optional success and failure callbacks for real-time feedback during processing.
  • + *
  • When using OutputStream-based batch jobs, every job must use a distinct + * {@code OutputStream} instance. Sharing a stream between concurrent jobs will corrupt + * the output. The caller is responsible for closing streams after the batch completes.
  • * * * @implNote Internally uses {@link Executors#newVirtualThreadPerTaskExecutor()} for efficient parallelism. @@ -153,6 +157,42 @@ public record UrlJob(String url, Path output, Optional injectCss, Optional> rendererCustomizer) {} + /** + * Render raw HTML string to a pre-opened {@link OutputStream}. + * + *

    Concurrency note: Each job in a batch runs on its own virtual thread. + * Every job in the batch must use a distinct {@code OutputStream} instance — + * sharing a stream between jobs will corrupt the output. The caller is responsible + * for closing the stream after the batch completes.

    + */ + public record HtmlStringStreamJob(String html, String baseUri, OutputStream outputStream, + Optional injectCss, + Optional> rendererCustomizer) {} + + /** + * Render an HTML file (and its relative assets) to a pre-opened {@link OutputStream}. + * + *

    Concurrency note: Each job in a batch runs on its own virtual thread. + * Every job in the batch must use a distinct {@code OutputStream} instance — + * sharing a stream between jobs will corrupt the output. The caller is responsible + * for closing the stream after the batch completes.

    + */ + public record HtmlFileStreamJob(Path htmlFile, Path baseDir, OutputStream outputStream, + Optional injectCss, + Optional> rendererCustomizer) {} + + /** + * Render a remote URL to a pre-opened {@link OutputStream}. + * + *

    Concurrency note: Each job in a batch runs on its own virtual thread. + * Every job in the batch must use a distinct {@code OutputStream} instance — + * sharing a stream between jobs will corrupt the output. The caller is responsible + * for closing the stream after the batch completes.

    + */ + public record UrlStreamJob(String url, OutputStream outputStream, + Optional injectCss, + Optional> rendererCustomizer) {} + // ------------------------- Single operations ------------------------- /** Render an HTML string. */ @@ -163,29 +203,39 @@ public static Path renderHtmlString(String html, String baseUri, Path output, Objects.requireNonNull(output, "output"); Files.createDirectories(output.getParent()); + try (var out = new FileOutputStream(output.toFile())) { + renderHtmlString(html, baseUri, out, injectCss, rendererCustomizer); + } + return output; + } + + /** Render an HTML string. */ + public static void renderHtmlString(String html, String baseUri, OutputStream outputStream, + String injectCss, + Consumer rendererCustomizer) { + Objects.requireNonNull(html, "html"); + Objects.requireNonNull(outputStream, "output"); + String finalHtml = injectCss != null && !injectCss.isEmpty() ? injectCssBlock(injectCss).apply(html) : html; - try (var out = new FileOutputStream(output.toFile())) { - ITextRenderer renderer = new ITextRenderer(); - SharedContext sc = renderer.getSharedContext(); - // Slightly safer resource loading if you need custom schemes: - sc.setUserAgentCallback(renderer.getOutputDevice().getSharedContext().getUserAgentCallback()); + ITextRenderer renderer = new ITextRenderer(); + SharedContext sc = renderer.getSharedContext(); + // Slightly safer resource loading if you need custom schemes: + sc.setUserAgentCallback(renderer.getOutputDevice().getSharedContext().getUserAgentCallback()); - if (rendererCustomizer != null) { - rendererCustomizer.accept(renderer); - } + if (rendererCustomizer != null) { + rendererCustomizer.accept(renderer); + } - if (baseUri != null && !baseUri.isBlank()) { - renderer.setDocumentFromString(finalHtml, baseUri); - } else { - renderer.setDocumentFromString(finalHtml); - } - renderer.layout(); - renderer.createPDF(out, true); + if (baseUri != null && !baseUri.isBlank()) { + renderer.setDocumentFromString(finalHtml, baseUri); + } else { + renderer.setDocumentFromString(finalHtml); } - return output; + renderer.layout(); + renderer.createPDF(outputStream, true); } /** Render an HTML file (and relatives). */ @@ -201,38 +251,57 @@ public static Path renderHtmlFile(Path htmlFile, Path baseDir, Path output, return renderHtmlString(html, base, output, injectCss, rendererCustomizer); } + /** Render an HTML file (and relatives). */ + public static void renderHtmlFile(Path htmlFile, Path baseDir, OutputStream outputStream, + String injectCss, + Consumer rendererCustomizer) throws IOException { + Objects.requireNonNull(htmlFile, "htmlFile"); + Objects.requireNonNull(outputStream, "output"); + + String html = Files.readString(htmlFile, StandardCharsets.UTF_8); + String base = (baseDir != null ? baseDir.toUri().toString() : htmlFile.getParent().toUri().toString()); + renderHtmlString(html, base, outputStream, injectCss, rendererCustomizer); + } + /** Render a URL to PDF. */ public static Path renderUrl(String url, Path output, String injectCss, Consumer rendererCustomizer) throws IOException { - Objects.requireNonNull(url, "url"); Objects.requireNonNull(output, "output"); Files.createDirectories(output.getParent()); try (var out = new FileOutputStream(output.toFile())) { - ITextRenderer renderer = new ITextRenderer(); - if (rendererCustomizer != null) { - rendererCustomizer.accept(renderer); - } + renderUrl(url, out, injectCss, rendererCustomizer); + } + return output; + } - if (injectCss == null || injectCss.isEmpty()) { - // No CSS injection: load DOM directly via user agent and set base URL - org.w3c.dom.Document doc = renderer.getSharedContext().getUac().getXMLResource(url).getDocument(); - renderer.setDocument(doc, url); - } else { - // Inject CSS: fetch HTML as text, prepend