Skip to content

Commit 42ab77f

Browse files
committed
Add batch utility classes which uses Java 21 virtual threads.
1 parent 19b3cfa commit 42ab77f

File tree

2 files changed

+599
-0
lines changed

2 files changed

+599
-0
lines changed
Lines changed: 296 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,296 @@
1+
/*
2+
* Copyright 2025 OpenPDF
3+
*
4+
* The contents of this file are subject to the Mozilla Public License Version 1.1
5+
* (the "License"); you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at http://www.mozilla.org/MPL/
7+
*
8+
* Software distributed under the License is distributed on an "AS IS" basis,
9+
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
10+
* for the specific language governing rights and limitations under the License.
11+
*
12+
* The Original Code is 'iText, a free JAVA-PDF library'.
13+
*
14+
* The Initial Developer of the Original Code is Bruno Lowagie. Portions created by
15+
* the Initial Developer are Copyright (C) 1999, 2000, 2001, 2002 by Bruno Lowagie.
16+
* All Rights Reserved.
17+
* Co-Developer of the code is Paulo Soares. Portions created by the Co-Developer
18+
* are Copyright (C) 2000, 2001, 2002 by Paulo Soares. All Rights Reserved.
19+
*
20+
* Contributor(s): all the names of the contributors are added in the source code
21+
* where applicable.
22+
*
23+
* Alternatively, the contents of this file may be used under the terms of the
24+
* LGPL license (the "GNU LIBRARY GENERAL PUBLIC LICENSE"), in which case the
25+
* provisions of LGPL are applicable instead of those above. If you wish to
26+
* allow use of your version of this file only under the terms of the LGPL
27+
* License and not to allow others to use your version of this file under
28+
* the MPL, indicate your decision by deleting the provisions above and
29+
* replace them with the notice and other provisions required by the LGPL.
30+
* If you do not delete the provisions above, a recipient may use your version
31+
* of this file under either the MPL or the GNU LIBRARY GENERAL PUBLIC LICENSE.
32+
*
33+
* This library is free software; you can redistribute it and/or modify it
34+
* under the terms of the MPL as stated above or under the terms of the GNU
35+
* Library General Public License as published by the Free Software Foundation;
36+
* either version 2 of the License, or any later version.
37+
*
38+
* This library is distributed in the hope that it will be useful, but WITHOUT
39+
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
40+
* FOR A PARTICULAR PURPOSE. See the GNU Library general Public License for more
41+
* details.
42+
*
43+
* If you didn't download this code from the following link, you should check if
44+
* you aren't using an obsolete version:
45+
* https://github.com/LibrePDF/OpenPDF
46+
*/
47+
48+
package org.openpdf.text.pdf;
49+
50+
import org.openpdf.text.Document;
51+
import org.openpdf.text.DocumentException;
52+
import org.openpdf.text.Element;
53+
import org.openpdf.text.Font;
54+
import org.openpdf.text.Rectangle;
55+
56+
import java.io.Closeable;
57+
import java.io.FileOutputStream;
58+
import java.io.IOException;
59+
import java.nio.file.Files;
60+
import java.nio.file.Path;
61+
import java.time.Instant;
62+
import java.util.ArrayList;
63+
import java.util.Collection;
64+
import java.util.List;
65+
import java.util.Objects;
66+
import java.util.concurrent.Callable;
67+
import java.util.concurrent.ExecutionException;
68+
import java.util.concurrent.ExecutorService;
69+
import java.util.concurrent.Executors;
70+
import java.util.concurrent.Future;
71+
import java.util.function.Consumer;
72+
73+
/**
74+
* PDF batch utilities using Java 21 virtual threads.
75+
*
76+
*/
77+
public final class PdfBatchUtils {
78+
79+
private PdfBatchUtils() {}
80+
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+
96+
// ------------------------- Common job records -------------------------
97+
98+
/** Merge several PDFs into one. */
99+
public record MergeJob(List<Path> inputs, Path output) {}
100+
101+
/** Add a semi-transparent text watermark to all pages. */
102+
public record WatermarkJob(Path input, Path output, String text, float fontSize, float opacity) {}
103+
104+
/** Encrypt a PDF with given passwords and permissions. */
105+
public record EncryptJob(Path input, Path output,
106+
String userPassword, String ownerPassword,
107+
int permissions, int encryptionType) {}
108+
109+
/** Split one PDF into per-page PDFs in the given directory (files will be named baseName_pageX.pdf). */
110+
public record SplitJob(Path input, Path outputDir, String baseName) {}
111+
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+
}
142+
143+
// ------------------------- Merge -------------------------
144+
145+
/** Merge one set of inputs into a single output file. */
146+
public static Path merge(List<Path> inputs, Path output) throws IOException, DocumentException {
147+
Objects.requireNonNull(inputs, "inputs");
148+
Objects.requireNonNull(output, "output");
149+
Files.createDirectories(output.getParent());
150+
151+
try (var fos = new FileOutputStream(output.toFile())) {
152+
Document doc = new Document();
153+
PdfCopy copy = new PdfCopy(doc, fos);
154+
doc.open();
155+
for (Path in : inputs) {
156+
try (PdfReader reader = new PdfReader(Files.readAllBytes(in))) {
157+
int n = reader.getNumberOfPages();
158+
for (int i = 1; i <= n; i++) {
159+
copy.addPage(copy.getImportedPage(reader, i));
160+
}
161+
}
162+
}
163+
doc.close();
164+
}
165+
return output;
166+
}
167+
168+
/** Batch merge. */
169+
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);
171+
}
172+
173+
// ------------------------- Watermark -------------------------
174+
175+
/** Watermark one PDF with text on every page (centered, diagonal). */
176+
public static Path watermark(Path input, Path output, String text, float fontSize, float opacity)
177+
throws IOException, DocumentException {
178+
Files.createDirectories(output.getParent());
179+
try (PdfReader reader = new PdfReader(Files.readAllBytes(input));
180+
var out = new FileOutputStream(output.toFile())) {
181+
PdfStamper stamper = new PdfStamper(reader, out);
182+
BaseFont bf = BaseFont.createFont(BaseFont.HELVETICA, BaseFont.WINANSI, BaseFont.EMBEDDED);
183+
Font font = new Font(bf, fontSize);
184+
185+
PdfGState gs = new PdfGState();
186+
gs.setFillOpacity(opacity);
187+
188+
int n = reader.getNumberOfPages();
189+
for (int i = 1; i <= n; i++) {
190+
Rectangle pageSize = reader.getPageSizeWithRotation(i);
191+
float x = (pageSize.getLeft() + pageSize.getRight()) / 2f;
192+
float y = (pageSize.getTop() + pageSize.getBottom()) / 2f;
193+
194+
PdfContentByte over = stamper.getOverContent(i);
195+
over.saveState();
196+
over.setGState(gs);
197+
over.beginText();
198+
over.setFontAndSize(bf, font.getSize());
199+
over.showTextAligned(Element.ALIGN_CENTER, text, x, y, 45f);
200+
over.endText();
201+
over.restoreState();
202+
}
203+
stamper.close();
204+
}
205+
return output;
206+
}
207+
208+
/** Batch watermark. */
209+
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);
211+
}
212+
213+
// ------------------------- Encrypt -------------------------
214+
215+
/** Encrypt one PDF. */
216+
public static Path encrypt(Path input, Path output,
217+
String userPassword, String ownerPassword,
218+
int permissions, int encryptionType)
219+
throws IOException, DocumentException {
220+
Files.createDirectories(output.getParent());
221+
try (PdfReader reader = new PdfReader(Files.readAllBytes(input));
222+
var out = new FileOutputStream(output.toFile())) {
223+
PdfStamper stamper = new PdfStamper(reader, out);
224+
stamper.setEncryption(
225+
userPassword != null ? userPassword.getBytes() : null,
226+
ownerPassword != null ? ownerPassword.getBytes() : null,
227+
permissions,
228+
encryptionType);
229+
stamper.close();
230+
}
231+
return output;
232+
}
233+
234+
/** Batch encrypt. */
235+
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);
237+
}
238+
239+
// ------------------------- Split -------------------------
240+
241+
/** Split one PDF to per-page PDFs. */
242+
public static List<Path> split(Path input, Path outputDir, String baseName) throws IOException, DocumentException {
243+
Files.createDirectories(outputDir);
244+
List<Path> outputs = new ArrayList<>();
245+
try (PdfReader reader = new PdfReader(Files.readAllBytes(input))) {
246+
int n = reader.getNumberOfPages();
247+
for (int i = 1; i <= n; i++) {
248+
Path out = outputDir.resolve(baseName + "_page" + i + ".pdf");
249+
try (var fos = new FileOutputStream(out.toFile())) {
250+
Document doc = new Document(reader.getPageSizeWithRotation(i));
251+
PdfCopy copy = new PdfCopy(doc, fos);
252+
doc.open();
253+
copy.addPage(copy.getImportedPage(reader, i));
254+
doc.close();
255+
}
256+
outputs.add(out);
257+
}
258+
}
259+
return outputs;
260+
}
261+
262+
/** Batch split. */
263+
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);
265+
}
266+
267+
// ------------------------- Convenience helpers -------------------------
268+
269+
/** Quick permissions helper. */
270+
public static int perms(boolean print, boolean modify, boolean copy, boolean annotate) {
271+
int p = 0;
272+
if (print) p |= PdfWriter.ALLOW_PRINTING;
273+
if (modify) p |= PdfWriter.ALLOW_MODIFY_CONTENTS;
274+
if (copy) p |= PdfWriter.ALLOW_COPY;
275+
if (annotate) p |= PdfWriter.ALLOW_MODIFY_ANNOTATIONS;
276+
return p;
277+
}
278+
279+
/** AES 128 vs 256 convenience. */
280+
public static int aes128() { return PdfWriter.ENCRYPTION_AES_128; }
281+
public static int aes256() { return PdfWriter.ENCRYPTION_AES_256_V3; }
282+
283+
/** Small utility for closing Closeables, ignoring exceptions. */
284+
private static void closeQuietly(Closeable c) { try { if (c != null) c.close(); } catch (Exception ignored) {} }
285+
286+
/** Simple example that shows how to use batch APIs. Remove in production. */
287+
public static void main(String[] args) throws Exception {
288+
// Example: watermark a bunch of PDFs using virtual threads
289+
List<WatermarkJob> jobs = List.of(
290+
new WatermarkJob(Path.of("in1.pdf"), Path.of("out1.pdf"), "CONFIDENTIAL", 72f, 0.12f),
291+
new WatermarkJob(Path.of("in2.pdf"), Path.of("out2.pdf"), "DRAFT "+ Instant.now(), 64f, 0.10f)
292+
);
293+
var res = batchWatermark(jobs, p -> System.out.println("OK: " + p), e -> e.printStackTrace());
294+
System.out.println(res);
295+
}
296+
}

0 commit comments

Comments
 (0)