|
| 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