Skip to content

Commit b8bbd3c

Browse files
Add PdfBatch class, and remove duplicate code from batch util classes.
1 parent b6e02fa commit b8bbd3c

6 files changed

Lines changed: 91 additions & 121 deletions

File tree

changelogs/3.0.0.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,9 @@ This means that source code using OpenPDF must be migrated to use import stateme
1515
📄 [Read the PDF 2.0 (ISO 32000-2:2020) specification](https://www.iso.org/standard/75839.html)
1616

1717

18-
* Added [Html2PdfBatchUtils](https://github.com/LibrePDF/OpenPDF/blob/master/openpdf-html/src/main/java/org/openpdf/html/Html2PdfBatchUtils.java) and [PdfBatchUtils](https://github.com/LibrePDF/OpenPDF/blob/master/openpdf-core/src/main/java/org/openpdf/text/pdf/PdfBatchUtils.java), new batch utilities built on Java 21 virtual threads.
18+
* Added [HtmlToPdfBatchUtils](https://github.com/LibrePDF/OpenPDF/blob/master/openpdf-html/src/main/java/org/openpdf
19+
/html/HtmlToPdfBatchUtils.java) and [PdfBatchUtils](https://github.
20+
com/LibrePDF/OpenPDF/blob/master/openpdf-core/src/main/java/org/openpdf/text/pdf/PdfBatchUtils.java), new batch utilities built on Java 21 virtual threads.
1921
These APIs improve throughput for HTML→PDF rendering and common PDF tasks such as merge, split, watermark, and encrypt.
2022

2123

openpdf-core/src/main/java/org/openpdf/text/pdf/PdfBatchUtils.java

Lines changed: 8 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -52,21 +52,19 @@
5252
import org.openpdf.text.Element;
5353
import org.openpdf.text.Font;
5454
import org.openpdf.text.Rectangle;
55+
import org.openpdf.text.utils.PdfBatch;
56+
import org.openpdf.text.utils.PdfBatch.BatchResult;
5557

5658
import java.io.Closeable;
5759
import java.io.FileOutputStream;
5860
import java.io.IOException;
5961
import java.nio.file.Files;
6062
import java.nio.file.Path;
6163
import java.util.ArrayList;
62-
import java.util.Collection;
6364
import java.util.List;
6465
import java.util.Objects;
6566
import java.util.concurrent.Callable;
66-
import java.util.concurrent.ExecutionException;
67-
import java.util.concurrent.ExecutorService;
68-
import java.util.concurrent.Executors;
69-
import java.util.concurrent.Future;
67+
7068
import java.util.function.Consumer;
7169

7270
/**
@@ -78,21 +76,6 @@ public final class PdfBatchUtils {
7876

7977
private PdfBatchUtils() {}
8078

81-
/** Generic result container for batch operations. */
82-
public static final class BatchResult<T> {
83-
public final List<T> successes = new ArrayList<>();
84-
public final List<Throwable> failures = new ArrayList<>();
85-
public boolean isAllSuccessful() { return failures.isEmpty(); }
86-
public int total() { return successes.size() + failures.size(); }
87-
@Override public String toString() {
88-
return "BatchResult{" +
89-
"successes=" + successes.size() +
90-
", failures=" + failures.size() +
91-
", total=" + total() +
92-
'}';
93-
}
94-
}
95-
9679
// ------------------------- Common job records -------------------------
9780

9881
/** Merge several PDFs into one. */
@@ -109,36 +92,6 @@ public record EncryptJob(Path input, Path output,
10992
/** Split one PDF into per-page PDFs in the given directory (files will be named baseName_pageX.pdf). */
11093
public record SplitJob(Path input, Path outputDir, String baseName) {}
11194

112-
// ------------------------- Public batch APIs -------------------------
113-
114-
/** Run arbitrary tasks on virtual threads with optional progress callback. */
115-
public static <T> BatchResult<T> runBatch(Collection<? extends Callable<T>> tasks,
116-
Consumer<T> onSuccess,
117-
Consumer<Throwable> onFailure) {
118-
Objects.requireNonNull(tasks, "tasks");
119-
var result = new BatchResult<T>();
120-
if (tasks.isEmpty()) return result;
121-
122-
try (ExecutorService exec = Executors.newVirtualThreadPerTaskExecutor()) {
123-
List<Future<T>> futures = tasks.stream().map(exec::submit).toList();
124-
for (Future<T> f : futures) {
125-
try {
126-
T v = f.get();
127-
result.successes.add(v);
128-
if (onSuccess != null) onSuccess.accept(v);
129-
} catch (ExecutionException ee) {
130-
Throwable cause = ee.getCause() != null ? ee.getCause() : ee;
131-
result.failures.add(cause);
132-
if (onFailure != null) onFailure.accept(cause);
133-
} catch (InterruptedException ie) {
134-
Thread.currentThread().interrupt();
135-
result.failures.add(ie);
136-
if (onFailure != null) onFailure.accept(ie);
137-
}
138-
}
139-
}
140-
return result;
141-
}
14295

14396
// ------------------------- Merge -------------------------
14497

@@ -167,7 +120,8 @@ public static Path merge(List<Path> inputs, Path output) throws IOException, Doc
167120

168121
/** Batch merge. */
169122
public static BatchResult<Path> batchMerge(List<MergeJob> jobs, Consumer<Path> onSuccess, Consumer<Throwable> onFailure) {
170-
return runBatch(jobs.stream().map(job -> (Callable<Path>) () -> merge(job.inputs, job.output)).toList(), onSuccess, onFailure);
123+
return PdfBatch.run(jobs.stream().map(job -> (Callable<Path>) () -> merge(job.inputs, job.output)).toList(),
124+
onSuccess, onFailure);
171125
}
172126

173127
// ------------------------- Watermark -------------------------
@@ -207,7 +161,7 @@ public static Path watermark(Path input, Path output, String text, float fontSiz
207161

208162
/** Batch watermark. */
209163
public static BatchResult<Path> batchWatermark(List<WatermarkJob> jobs, Consumer<Path> onSuccess, Consumer<Throwable> onFailure) {
210-
return runBatch(jobs.stream().map(j -> (Callable<Path>) () -> watermark(j.input, j.output, j.text, j.fontSize, j.opacity)).toList(), onSuccess, onFailure);
164+
return PdfBatch.run(jobs.stream().map(j -> (Callable<Path>) () -> watermark(j.input, j.output, j.text, j.fontSize, j.opacity)).toList(), onSuccess, onFailure);
211165
}
212166

213167
// ------------------------- Encrypt -------------------------
@@ -233,7 +187,7 @@ public static Path encrypt(Path input, Path output,
233187

234188
/** Batch encrypt. */
235189
public static BatchResult<Path> batchEncrypt(List<EncryptJob> jobs, Consumer<Path> onSuccess, Consumer<Throwable> onFailure) {
236-
return runBatch(jobs.stream().map(j -> (Callable<Path>) () -> encrypt(j.input, j.output, j.userPassword, j.ownerPassword, j.permissions, j.encryptionType)).toList(), onSuccess, onFailure);
190+
return PdfBatch.run(jobs.stream().map(j -> (Callable<Path>) () -> encrypt(j.input, j.output, j.userPassword, j.ownerPassword, j.permissions, j.encryptionType)).toList(), onSuccess, onFailure);
237191
}
238192

239193
// ------------------------- Split -------------------------
@@ -261,7 +215,7 @@ public static List<Path> split(Path input, Path outputDir, String baseName) thro
261215

262216
/** Batch split. */
263217
public static BatchResult<List<Path>> batchSplit(List<SplitJob> jobs, Consumer<List<Path>> onSuccess, Consumer<Throwable> onFailure) {
264-
return runBatch(jobs.stream().map(j -> (Callable<List<Path>>) () -> split(j.input, j.outputDir, j.baseName)).toList(), onSuccess, onFailure);
218+
return PdfBatch.run(jobs.stream().map(j -> (Callable<List<Path>>) () -> split(j.input, j.outputDir, j.baseName)).toList(), onSuccess, onFailure);
265219
}
266220

267221
// ------------------------- Convenience helpers -------------------------
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
package org.openpdf.text.utils;
2+
3+
import java.util.ArrayList;
4+
import java.util.Collection;
5+
import java.util.List;
6+
import java.util.Objects;
7+
import java.util.concurrent.Callable;
8+
import java.util.concurrent.ExecutionException;
9+
import java.util.concurrent.ExecutorService;
10+
import java.util.concurrent.Executors;
11+
import java.util.concurrent.Future;
12+
import java.util.function.Consumer;
13+
14+
/**
15+
* Utility class for executing collections of tasks concurrently using Java 21 virtual threads.
16+
*/
17+
public final class PdfBatch {
18+
private PdfBatch() {}
19+
public static final class BatchResult<T> {
20+
public final List<T> successes = new ArrayList<>();
21+
public final List<Throwable> failures = new ArrayList<>();
22+
public boolean isAllSuccessful() { return failures.isEmpty(); }
23+
public int total() { return successes.size() + failures.size(); }
24+
@Override public String toString() {
25+
return "BatchResult{" +
26+
"successes=" + successes.size() +
27+
", failures=" + failures.size() +
28+
", total=" + total() +
29+
'}';
30+
} }
31+
public static <T> BatchResult<T> run(Collection<? extends Callable<T>> tasks,
32+
Consumer<T> onSuccess,
33+
Consumer<Throwable> onFailure) {
34+
Objects.requireNonNull(tasks, "tasks");
35+
var result = new BatchResult<T>();
36+
if (tasks.isEmpty()) return result;
37+
38+
try (ExecutorService exec = Executors.newVirtualThreadPerTaskExecutor()) {
39+
List<Future<T>> futures = tasks.stream().map(exec::submit).toList();
40+
for (Future<T> f : futures) {
41+
try {
42+
T v = f.get();
43+
result.successes.add(v);
44+
if (onSuccess != null) onSuccess.accept(v);
45+
} catch (ExecutionException ee) {
46+
Throwable cause = ee.getCause() != null ? ee.getCause() : ee;
47+
result.failures.add(cause);
48+
if (onFailure != null) onFailure.accept(cause);
49+
} catch (InterruptedException ie) {
50+
Thread.currentThread().interrupt();
51+
result.failures.add(ie);
52+
if (onFailure != null) onFailure.accept(ie);
53+
}
54+
}
55+
}
56+
return result;
57+
58+
}
59+
}

openpdf-core/src/test/java/org/openpdf/text/pdf/PdfBatchUtilsTest.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import org.junit.jupiter.api.Test;
44
import org.openpdf.text.Document;
55
import org.openpdf.text.Paragraph;
6+
import org.openpdf.text.utils.PdfBatch;
67

78
import java.io.FileOutputStream;
89
import java.io.IOException;
@@ -49,7 +50,7 @@ void runBatch_usesVirtualThreads() {
4950
}
5051
);
5152

52-
var result = PdfBatchUtils.runBatch(tasks, v -> {}, t -> fail(t));
53+
var result = PdfBatch.run(tasks, v -> {}, t -> fail(t));
5354
assertTrue(result.isAllSuccessful(), "All tasks should succeed");
5455
assertEquals(3, result.successes.size());
5556
assertEquals(0, result.failures.size());

openpdf-html/src/main/java/org/openpdf/html/Html2PdfBatchUtils.java renamed to openpdf-html/src/main/java/org/openpdf/html/HtmlToPdfBatchUtils.java

Lines changed: 10 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -49,23 +49,20 @@
4949

5050
import org.openpdf.pdf.ITextRenderer;
5151
import org.openpdf.layout.SharedContext;
52+
import org.openpdf.text.utils.PdfBatch;
53+
import org.openpdf.text.utils.PdfBatch.BatchResult;
5254

5355
import java.io.FileOutputStream;
5456
import java.io.IOException;
5557
import java.nio.charset.StandardCharsets;
5658
import java.nio.file.Files;
5759
import java.nio.file.Path;
58-
import java.util.ArrayList;
59-
import java.util.Collection;
6060
import java.util.List;
6161

6262
import java.util.Objects;
6363
import java.util.Optional;
6464
import java.util.concurrent.Callable;
65-
import java.util.concurrent.ExecutionException;
66-
import java.util.concurrent.ExecutorService;
6765
import java.util.concurrent.Executors;
68-
import java.util.concurrent.Future;
6966
import java.util.function.Consumer;
7067
import java.util.function.UnaryOperator;
7168

@@ -133,24 +130,11 @@
133130
* @since 3.0.0
134131
*/
135132

136-
public final class Html2PdfBatchUtils {
137-
138-
private Html2PdfBatchUtils() {}
139-
140-
/** Generic result container for batch operations. */
141-
public static final class BatchResult<T> {
142-
public final List<T> successes = new ArrayList<>();
143-
public final List<Throwable> failures = new ArrayList<>();
144-
public boolean isAllSuccessful() { return failures.isEmpty(); }
145-
public int total() { return successes.size() + failures.size(); }
146-
@Override public String toString() {
147-
return "BatchResult{" +
148-
"successes=" + successes.size() +
149-
", failures=" + failures.size() +
150-
", total=" + total() +
151-
'}';
152-
}
153-
}
133+
public final class HtmlToPdfBatchUtils {
134+
135+
private HtmlToPdfBatchUtils() {}
136+
137+
154138

155139
// ------------------------- Job records -------------------------
156140

@@ -169,36 +153,6 @@ public record UrlJob(String url, Path output,
169153
Optional<String> injectCss,
170154
Optional<Consumer<ITextRenderer>> rendererCustomizer) {}
171155

172-
// ------------------------- Public batch API -------------------------
173-
174-
public static <T> BatchResult<T> runBatch(Collection<? extends Callable<T>> tasks,
175-
Consumer<T> onSuccess,
176-
Consumer<Throwable> onFailure) {
177-
Objects.requireNonNull(tasks, "tasks");
178-
var result = new BatchResult<T>();
179-
if (tasks.isEmpty()) return result;
180-
181-
try (ExecutorService exec = Executors.newVirtualThreadPerTaskExecutor()) {
182-
List<Future<T>> futures = tasks.stream().map(exec::submit).toList();
183-
for (Future<T> f : futures) {
184-
try {
185-
T v = f.get();
186-
result.successes.add(v);
187-
if (onSuccess != null) onSuccess.accept(v);
188-
} catch (ExecutionException ee) {
189-
Throwable cause = ee.getCause() != null ? ee.getCause() : ee;
190-
result.failures.add(cause);
191-
if (onFailure != null) onFailure.accept(cause);
192-
} catch (InterruptedException ie) {
193-
Thread.currentThread().interrupt();
194-
result.failures.add(ie);
195-
if (onFailure != null) onFailure.accept(ie);
196-
}
197-
}
198-
}
199-
return result;
200-
}
201-
202156
// ------------------------- Single operations -------------------------
203157

204158
/** Render an HTML string. */
@@ -282,7 +236,7 @@ public static Path renderUrl(String url, Path output,
282236
public static BatchResult<Path> batchHtmlStrings(List<HtmlStringJob> jobs,
283237
Consumer<Path> onSuccess,
284238
Consumer<Throwable> onFailure) {
285-
return runBatch(jobs.stream().map(j -> (Callable<Path>) () ->
239+
return PdfBatch.run(jobs.stream().map(j -> (Callable<Path>) () ->
286240
renderHtmlString(j.html, j.baseUri, j.output,
287241
j.injectCss.orElse(null),
288242
j.rendererCustomizer.orElse(null))
@@ -292,7 +246,7 @@ public static BatchResult<Path> batchHtmlStrings(List<HtmlStringJob> jobs,
292246
public static BatchResult<Path> batchHtmlFiles(List<HtmlFileJob> jobs,
293247
Consumer<Path> onSuccess,
294248
Consumer<Throwable> onFailure) {
295-
return runBatch(jobs.stream().map(j -> (Callable<Path>) () ->
249+
return PdfBatch.run(jobs.stream().map(j -> (Callable<Path>) () ->
296250
renderHtmlFile(j.htmlFile, j.baseDir, j.output,
297251
j.injectCss.orElse(null),
298252
j.rendererCustomizer.orElse(null))
@@ -302,7 +256,7 @@ public static BatchResult<Path> batchHtmlFiles(List<HtmlFileJob> jobs,
302256
public static BatchResult<Path> batchUrls(List<UrlJob> jobs,
303257
Consumer<Path> onSuccess,
304258
Consumer<Throwable> onFailure) {
305-
return runBatch(jobs.stream().map(j -> (Callable<Path>) () ->
259+
return PdfBatch.run(jobs.stream().map(j -> (Callable<Path>) () ->
306260
renderUrl(j.url, j.output, j.injectCss.orElse(null), j.rendererCustomizer.orElse(null))
307261
).toList(), onSuccess, onFailure);
308262
}

openpdf-html/src/test/java/org/openpdf/html/Html2PdfBatchUtilsTest.java renamed to openpdf-html/src/test/java/org/openpdf/html/HtmlToPdfBatchUtilsTest.java

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
/**
1313
* Tests for Html2PdfBatchUtils to ensure it runs batch jobs on virtual threads.
1414
*/
15-
class Html2PdfBatchUtilsTest {
15+
class HtmlToPdfBatchUtilsTest {
1616

1717
@Test
1818
void testBatchHtmlStringsRunsOnVirtualThreads() throws Exception {
@@ -23,29 +23,29 @@ void testBatchHtmlStringsRunsOnVirtualThreads() throws Exception {
2323
Path out3 = Files.createTempFile("vt-batch-3-", ".pdf");
2424

2525
// Assert virtual-thread execution from inside each batch task.
26-
var vtAssertCustomizer = Html2PdfBatchUtils.setDpi(96).andThen(renderer ->
26+
var vtAssertCustomizer = HtmlToPdfBatchUtils.setDpi(96).andThen(renderer ->
2727
assertTrue(Thread.currentThread().isVirtual(), "Batch task should run on a virtual thread")
2828
);
2929

3030
var jobs = List.of(
31-
new Html2PdfBatchUtils.HtmlStringJob(
31+
new HtmlToPdfBatchUtils.HtmlStringJob(
3232
html, null, out1,
33-
Optional.of(Html2PdfBatchUtils.CSS_A4_20MM),
33+
Optional.of(HtmlToPdfBatchUtils.CSS_A4_20MM),
3434
Optional.of(vtAssertCustomizer)
3535
),
36-
new Html2PdfBatchUtils.HtmlStringJob(
36+
new HtmlToPdfBatchUtils.HtmlStringJob(
3737
html, null, out2,
38-
Optional.of(Html2PdfBatchUtils.CSS_A4_20MM),
38+
Optional.of(HtmlToPdfBatchUtils.CSS_A4_20MM),
3939
Optional.of(vtAssertCustomizer)
4040
),
41-
new Html2PdfBatchUtils.HtmlStringJob(
41+
new HtmlToPdfBatchUtils.HtmlStringJob(
4242
html, null, out3,
43-
Optional.of(Html2PdfBatchUtils.CSS_A4_20MM),
43+
Optional.of(HtmlToPdfBatchUtils.CSS_A4_20MM),
4444
Optional.of(vtAssertCustomizer)
4545
)
4646
);
4747

48-
var result = Html2PdfBatchUtils.batchHtmlStrings(jobs, p -> {}, t -> fail(t));
48+
var result = HtmlToPdfBatchUtils.batchHtmlStrings(jobs, p -> {}, t -> fail(t));
4949

5050
assertTrue(result.isAllSuccessful(), "All batch jobs should succeed");
5151

0 commit comments

Comments
 (0)