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 @@ * *
{@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