diff --git a/openpdf-core/src/main/java/org/openpdf/text/pdf/PdfBatchUtils.java b/openpdf-core/src/main/java/org/openpdf/text/pdf/PdfBatchUtils.java index ff7dabfac..ce62c3614 100644 --- a/openpdf-core/src/main/java/org/openpdf/text/pdf/PdfBatchUtils.java +++ b/openpdf-core/src/main/java/org/openpdf/text/pdf/PdfBatchUtils.java @@ -223,18 +223,38 @@ public static BatchResult> batchSplit(List jobs, Consumer 1) ? Integer.parseInt(p2[1]) : 0; return Integer.compare(minor1, minor2); } + /** * @see org.openpdf.text.pdf.interfaces.PdfVersion#setPdfVersion(org.openpdf.text.pdf.PdfName) */ diff --git a/openpdf-core/src/main/java/org/openpdf/text/utils/PdfBatch.java b/openpdf-core/src/main/java/org/openpdf/text/utils/PdfBatch.java index 4ff6e2dfd..3d4821b55 100644 --- a/openpdf-core/src/main/java/org/openpdf/text/utils/PdfBatch.java +++ b/openpdf-core/src/main/java/org/openpdf/text/utils/PdfBatch.java @@ -15,25 +15,39 @@ * Utility class for executing collections of tasks concurrently using Java 21 virtual threads. */ public final class PdfBatch { + private PdfBatch() {} + public static final class BatchResult { public final List successes = new ArrayList<>(); public final List failures = new ArrayList<>(); - public boolean isAllSuccessful() { return failures.isEmpty(); } - public int total() { return successes.size() + failures.size(); } - @Override public String toString() { + + public boolean isAllSuccessful() { + return failures.isEmpty(); + } + + public int total() { + return successes.size() + failures.size(); + } + + @Override + public String toString() { return "BatchResult{" + "successes=" + successes.size() + ", failures=" + failures.size() + ", total=" + total() + '}'; - } } + } + } + public static BatchResult run(Collection> tasks, Consumer onSuccess, Consumer onFailure) { Objects.requireNonNull(tasks, "tasks"); var result = new BatchResult(); - if (tasks.isEmpty()) return result; + if (tasks.isEmpty()) { + return result; + } try (ExecutorService exec = Executors.newVirtualThreadPerTaskExecutor()) { List> futures = tasks.stream().map(exec::submit).toList(); @@ -41,19 +55,24 @@ public static BatchResult run(Collection> tasks, try { T v = f.get(); result.successes.add(v); - if (onSuccess != null) onSuccess.accept(v); + if (onSuccess != null) { + onSuccess.accept(v); + } } catch (ExecutionException ee) { Throwable cause = ee.getCause() != null ? ee.getCause() : ee; result.failures.add(cause); - if (onFailure != null) onFailure.accept(cause); + if (onFailure != null) { + onFailure.accept(cause); + } } catch (InterruptedException ie) { Thread.currentThread().interrupt(); result.failures.add(ie); - if (onFailure != null) onFailure.accept(ie); + if (onFailure != null) { + onFailure.accept(ie); + } } } } return result; - } } diff --git a/openpdf-core/src/test/java/org/openpdf/text/OpenPdfVersionTest.java b/openpdf-core/src/test/java/org/openpdf/text/OpenPdfVersionTest.java index 32cdd6cf5..c5eaaa980 100644 --- a/openpdf-core/src/test/java/org/openpdf/text/OpenPdfVersionTest.java +++ b/openpdf-core/src/test/java/org/openpdf/text/OpenPdfVersionTest.java @@ -26,7 +26,9 @@ class OpenPdfVersionTest { private static Set buildSupportedVersions() { Set s = new LinkedHashSet<>(); - for (int i = 0; i <= 7; i++) s.add("1." + i); + for (int i = 0; i <= 7; i++) { + s.add("1." + i); + } s.add("2.0"); return s; } @@ -75,7 +77,7 @@ void createDefaultPdfIs20(@TempDir Path tmp) throws Exception { } @ParameterizedTest(name = "Create + round-trip version {0} (memory and file)") - @ValueSource(strings = {"1.2","1.3","1.4","1.5","1.6","1.7","2.0"}) + @ValueSource(strings = {"1.2", "1.3", "1.4", "1.5", "1.6", "1.7", "2.0"}) void createAndRoundTripSpecificVersions(String version, @TempDir Path tmp) throws Exception { // In-memory byte[] bytes = createPdfToBytes(version); @@ -257,7 +259,9 @@ private static String getVersionFromReaderNormalized(PdfReader reader) { // Common in OpenPDF: getPdfVersion() returns char or String Method m = reader.getClass().getMethod("getPdfVersion"); Object v = m.invoke(reader); - if (v == null) return null; + if (v == null) { + return null; + } if (v instanceof Character) { char c = (Character) v; if (Character.isDigit(c)) { @@ -274,7 +278,9 @@ private static String getVersionFromReaderNormalized(PdfReader reader) { try { Method m2 = reader.getClass().getMethod("getHeaderVersion"); Object v2 = m2.invoke(reader); - if (v2 instanceof String) return (String) v2; + if (v2 instanceof String) { + return (String) v2; + } } catch (Exception ignored2) { // no suitable method } @@ -312,12 +318,16 @@ private static String extractHeaderVersion(byte[] pdfBytes) { int len = Math.min(pdfBytes.length, 4096); String header = new String(pdfBytes, 0, len, StandardCharsets.ISO_8859_1); int idx = header.indexOf("%PDF-"); - if (idx < 0) throw new IllegalStateException("PDF header not found"); + if (idx < 0) { + throw new IllegalStateException("PDF header not found"); + } int start = idx + 5; int end = start; while (end < header.length()) { char c = header.charAt(end); - if (!Character.isDigit(c) && c != '.') break; + if (!Character.isDigit(c) && c != '.') { + break; + } end++; } return header.substring(start, end); diff --git a/openpdf-core/src/test/java/org/openpdf/text/Pdf20ComplianceTest.java b/openpdf-core/src/test/java/org/openpdf/text/Pdf20ComplianceTest.java index c1d44b381..3bbe8ec42 100644 --- a/openpdf-core/src/test/java/org/openpdf/text/Pdf20ComplianceTest.java +++ b/openpdf-core/src/test/java/org/openpdf/text/Pdf20ComplianceTest.java @@ -112,11 +112,15 @@ private static String extractHeaderVersion(byte[] pdfBytes) { int len = Math.min(pdfBytes.length, 4096); String header = new String(pdfBytes, 0, len, StandardCharsets.ISO_8859_1); int idx = header.indexOf("%PDF-"); - if (idx < 0) throw new IllegalStateException("PDF header not found"); + if (idx < 0) { + throw new IllegalStateException("PDF header not found"); + } int start = idx + 5, end = start; while (end < header.length()) { char c = header.charAt(end); - if (!Character.isDigit(c) && c != '.') break; + if (!Character.isDigit(c) && c != '.') { + break; + } end++; } return header.substring(start, end); @@ -127,13 +131,19 @@ private static String getVersionFromReaderNormalized(PdfReader reader) { try { Method m = reader.getClass().getMethod("getPdfVersion"); Object v = m.invoke(reader); - if (v == null) return null; + if (v == null) { + return null; + } if (v instanceof Character) { char c = (Character) v; - if (Character.isDigit(c)) return "1." + c; + if (Character.isDigit(c)) { + return "1." + c; + } return String.valueOf(c); } - if (v instanceof String) return (String) v; + if (v instanceof String) { + return (String) v; + } } catch (NoSuchMethodException ignored) { // Older/newer forks may differ; ignore gracefully. } catch (Exception e) { diff --git a/openpdf-core/src/test/java/org/openpdf/text/Pdf20ExamplesConformanceTest.java b/openpdf-core/src/test/java/org/openpdf/text/Pdf20ExamplesConformanceTest.java index a8ecc96fa..d8fed26bb 100644 --- a/openpdf-core/src/test/java/org/openpdf/text/Pdf20ExamplesConformanceTest.java +++ b/openpdf-core/src/test/java/org/openpdf/text/Pdf20ExamplesConformanceTest.java @@ -17,7 +17,10 @@ class Pdf20ExamplesConformanceTest { // Override with -Dopenpdf.pdf20.dir=/absolute/path if needed private static final Path DIR = Path.of(System.getProperty("openpdf.pdf20.dir", "src/test/resources/pdf-2-0")); - private static Path f(String name) { return DIR.resolve(name); } + + private static Path f(String name) { + return DIR.resolve(name); + } // ---------- Simple PDF 2.0 file ---------- @@ -67,7 +70,9 @@ void testUtf8Annotation() throws Exception { for (int i = 0; i < annots.size(); i++) { PdfDictionary a = (PdfDictionary) PdfReader.getPdfObject(annots.getAsIndirectObject(i)); - if (a == null) continue; + if (a == null) { + continue; + } PdfString contents = a.getAsString(PdfName.CONTENTS); if (contents != null) { String s = contents.toUnicodeString(); @@ -124,14 +129,21 @@ void testPageLevelOutputIntent() throws Exception { // ================= helpers ================= private static class Header { - final int offset; final String version; - Header(int o, String v) { offset = o; version = v; } + final int offset; + final String version; + + Header(int o, String v) { + offset = o; + version = v; + } } /** Find the first %PDF- header anywhere and parse its version. */ private static Header firstHeader(byte[] bytes) { int idx = indexOfAscii(bytes, "%PDF-"); - if (idx < 0) return null; + if (idx < 0) { + return null; + } String ver = parseHeaderVersion(bytes, idx); return new Header(idx, ver); } @@ -139,7 +151,9 @@ private static Header firstHeader(byte[] bytes) { /** True if the file contains a %PDF-x.y header with the given version anywhere. */ private static boolean containsHeaderVersion(byte[] bytes, String wanted) { for (Header h : allHeaders(bytes)) { - if (wanted.equals(h.version)) return true; + if (wanted.equals(h.version)) { + return true; + } } return false; } @@ -151,7 +165,9 @@ private static List
allHeaders(byte[] bytes) { byte[] pat = "%PDF-".getBytes(StandardCharsets.ISO_8859_1); while (true) { int idx = indexOf(bytes, pat, from); - if (idx < 0) break; + if (idx < 0) { + break; + } String ver = parseHeaderVersion(bytes, idx); list.add(new Header(idx, ver)); from = idx + pat.length; @@ -165,7 +181,9 @@ private static String parseHeaderVersion(byte[] bytes, int headerIdx) { int end = start; while (end < bytes.length) { char c = (char) (bytes[end] & 0xFF); - if (!Character.isDigit(c) && c != '.') break; + if (!Character.isDigit(c) && c != '.') { + break; + } end++; } return new String(bytes, start, end - start, StandardCharsets.ISO_8859_1); @@ -188,7 +206,9 @@ private static int indexOfAscii(byte[] bytes, String token) { private static int indexOf(byte[] hay, byte[] needle, int from) { outer: for (int i = from; i <= hay.length - needle.length; i++) { for (int j = 0; j < needle.length; j++) { - if (hay[i + j] != needle[j]) continue outer; + if (hay[i + j] != needle[j]) { + continue outer; + } } return i; } diff --git a/openpdf-core/src/test/java/org/openpdf/text/Pdf20FeatureTests.java b/openpdf-core/src/test/java/org/openpdf/text/Pdf20FeatureWriteAndVerifyTest.java similarity index 86% rename from openpdf-core/src/test/java/org/openpdf/text/Pdf20FeatureTests.java rename to openpdf-core/src/test/java/org/openpdf/text/Pdf20FeatureWriteAndVerifyTest.java index 6074ed325..f5cf873fc 100644 --- a/openpdf-core/src/test/java/org/openpdf/text/Pdf20FeatureTests.java +++ b/openpdf-core/src/test/java/org/openpdf/text/Pdf20FeatureWriteAndVerifyTest.java @@ -1,216 +1,259 @@ -package org.openpdf.text; - -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.openpdf.text.pdf.*; - -import java.io.*; -import java.lang.reflect.Method; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; -import java.time.Instant; -import java.util.Map; - -import static org.junit.jupiter.api.Assertions.*; - -class Pdf20FeatureWriteAndVerifyTest { - - // Persistent output dir; not cleaned up by the test runner. - private static final Path OUT_DIR = Path.of(System.getProperty("openpdf.outdir", "target/pdf20-tests")); - - @Test - @DisplayName("Create PDF 2.0 to disk (no cleanup): header/version + Lang + AF + EmbeddedFiles + XMP + Unicode /UF") - void createAndVerifyPdf20Features_persistent() throws Exception { - Files.createDirectories(OUT_DIR); - - // -------- CREATE MAIN PDF -------- - Path outMain = OUT_DIR.resolve("pdf20-features.pdf"); - byte[] pdfBytes = createPdf20WithFeatures(outMain); - System.out.println("Wrote: " + outMain.toAbsolutePath()); - - // Header must be %PDF-2.0 - String header = new String(pdfBytes, 0, Math.min(4096, pdfBytes.length), StandardCharsets.ISO_8859_1); - assertTrue(header.startsWith("%PDF-2.0"), "Header must be %PDF-2.0"); - - // Verify via PdfReader - try (PdfReader reader = new PdfReader(new ByteArrayInputStream(pdfBytes))) { - assertTrue(reader.getNumberOfPages() >= 1, "Expected at least one page"); - - String readerVer = getVersionFromReaderNormalized(reader); - if (readerVer != null) assertEquals("2.0", readerVer, "Reader version should be 2.0"); - - PdfDictionary catalog = reader.getCatalog(); - assertNotNull(catalog, "Catalog missing"); - assertEquals("en-US", stripParens(catalog.getAsString(PdfName.LANG)), "Catalog /Lang mismatch"); - - // /AF present with two attachments - PdfArray afArr = catalog.getAsArray(new PdfName("AF")); - assertNotNull(afArr, "/AF missing on Catalog"); - assertTrue(afArr.size() >= 2, "/AF should reference both attachments"); - - // AF[0] AFRelationship=Data - PdfDictionary fs1Dict = derefToDict(afArr.getAsIndirectObject(0)); - assertEquals(new PdfName("Data"), fs1Dict.get(new PdfName("AFRelationship"))); - - // AF[1] has Unicode filename via /UF and AFRelationship=Data - PdfDictionary fs2Dict = derefToDict(afArr.getAsIndirectObject(1)); - assertEquals(new PdfName("Data"), fs2Dict.get(new PdfName("AFRelationship"))); - PdfObject uf = fs2Dict.get(PdfName.UF); - assertNotNull(uf, "Filespec #2 should contain /UF for Unicode filename"); - assertTrue(uf.toString().contains("les_meg_"), "Expected Unicode filename in /UF"); - - // Names tree: EmbeddedFiles present - PdfDictionary names = catalog.getAsDict(PdfName.NAMES); - assertNotNull(names, "/Names missing"); - PdfDictionary embeddedFiles = names.getAsDict(PdfName.EMBEDDEDFILES); - assertNotNull(embeddedFiles, "/Names/EmbeddedFiles missing"); - PdfArray nameArray = embeddedFiles.getAsArray(PdfName.NAMES); - assertNotNull(nameArray, "/Names/EmbeddedFiles/Names array missing"); - assertTrue(nameArray.size() >= 2, "EmbeddedFiles name tree should contain pairs"); - - // XMP present - byte[] xmp = reader.getMetadata(); - assertNotNull(xmp, "XMP metadata should be present"); - String xmpStr = new String(xmp, StandardCharsets.UTF_8); - assertTrue(xmpStr.contains(" info = reader.getInfo(); - assertTrue(info.containsKey("Title")); - assertTrue(info.get("Title").contains("æ") && info.get("Title").contains("π"), - "Unicode Title should round-trip"); - } - - // -------- WRITE A STAMPED VARIANT WITH PAGE-LEVEL /Lang OVERRIDE (ALSO NOT DELETED) -------- - Path outStamped = OUT_DIR.resolve("pdf20-features-lang-override.pdf"); - try (PdfReader reader = new PdfReader(outMain.toString()); - OutputStream os = Files.newOutputStream(outStamped)) { - PdfStamper stamper = new PdfStamper(reader, os); - PdfDictionary page1 = reader.getPageN(1); - page1.put(PdfName.LANG, new PdfString("nb-NO")); - stamper.close(); - } - System.out.println("Wrote: " + outStamped.toAbsolutePath()); - - try (PdfReader r2 = new PdfReader(outStamped.toString())) { - assertEquals("en-US", stripParens(r2.getCatalog().getAsString(PdfName.LANG)), - "Catalog /Lang should remain en-US"); - assertEquals("nb-NO", stripParens(r2.getPageN(1).getAsString(PdfName.LANG)), - "Page 1 /Lang override should be nb-NO"); - } - - System.out.println("Output directory: " + OUT_DIR.toAbsolutePath()); - System.out.println("Done. Files are NOT deleted."); - } - - // -------- helpers -------- - - private static byte[] createPdf20WithFeatures(Path out) throws Exception { - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - - Document doc = new Document(); - PdfWriter writer; - try (OutputStream fos = Files.newOutputStream(out)) { - writer = PdfWriter.getInstance(doc, new TeeOutputStream(baos, fos)); - writer.setXmpMetadata(minimalXmp("en-US", "OpenPDF 2.0 feature test").getBytes(StandardCharsets.UTF_8)); - - doc.open(); - - // Catalog /Lang - writer.getExtraCatalog().put(PdfName.LANG, new PdfString("en-US")); - - // Content - doc.add(new Paragraph("Hello PDF 2.0 – " + Instant.now())); - - // Attachment #1 (AFRelationship=Data) - byte[] bytes1 = "Hello attachment #1".getBytes(StandardCharsets.UTF_8); - PdfFileSpecification fs1 = PdfFileSpecification.fileEmbedded(writer, null, "readme.txt", bytes1); - fs1.put(new PdfName("AFRelationship"), new PdfName("Data")); - writer.addFileAttachment(fs1); - - // Attachment #2 with Unicode filename via /UF - byte[] bytes2 = "Hello attachment #2 (UTF-16 filename)".getBytes(StandardCharsets.UTF_8); - String unicodeName = "les_meg_æøå.txt"; - PdfFileSpecification fs2 = PdfFileSpecification.fileEmbedded(writer, null, unicodeName, bytes2); - fs2.put(PdfName.UF, new PdfString(unicodeName, PdfObject.TEXT_UNICODE)); - fs2.put(new PdfName("AFRelationship"), new PdfName("Data")); - writer.addFileAttachment(fs2); - - // /AF array referencing both - PdfArray af = new PdfArray(); - af.add(fs1.getReference()); - af.add(fs2.getReference()); - writer.getExtraCatalog().put(new PdfName("AF"), af); - - // Unicode Title (Info) - doc.addTitle("Tittel æøå – π"); - - doc.close(); // closes writer & flushes both streams - } - - return baos.toByteArray(); - } - - /** Minimal XMP packet (UTF-8). */ - private static String minimalXmp(String lang, String dcTitle) { - return "" - + "" - + "" - + " " - + " " - + " " - + " " + escapeXml(dcTitle) + "" - + " " - + " " - + " " - + "" - + ""; - } - - private static String escapeXml(String s) { - return s.replace("&", "&").replace("<", "<").replace(">", ">"); - } - - /** Normalize PdfReader#getPdfVersion(): '4' -> "1.4", "2.0" -> "2.0". May return null. */ - private static String getVersionFromReaderNormalized(PdfReader reader) { - try { - Method m = reader.getClass().getMethod("getPdfVersion"); - Object v = m.invoke(reader); - if (v == null) return null; - if (v instanceof Character) { - char c = (Character) v; - if (Character.isDigit(c)) return "1." + c; - return String.valueOf(c); - } - if (v instanceof String) return (String) v; - } catch (NoSuchMethodException ignored) { - // OK: header check already covers us - } catch (Exception e) { - System.out.println("getPdfVersion() reflection error: " + e); - } - return null; - } - - private static String stripParens(PdfString s) { - return s == null ? null : s.toString().replaceAll("[()]", ""); - } - - private static PdfDictionary derefToDict(PdfIndirectReference ref) { - if (ref == null) return null; - PdfObject obj = PdfReader.getPdfObject(ref); - return (obj instanceof PdfDictionary) ? (PdfDictionary) obj : null; - } - - /** Writes to two streams at once (memory + file). */ - private static final class TeeOutputStream extends OutputStream { - private final OutputStream a, b; - TeeOutputStream(OutputStream a, OutputStream b) { this.a = a; this.b = b; } - @Override public void write(int i) throws IOException { a.write(i); b.write(i); } - @Override public void write(byte[] buf) throws IOException { a.write(buf); b.write(buf); } - @Override public void write(byte[] buf, int off, int len) throws IOException { a.write(buf, off, len); b.write(buf, off, len); } - @Override public void flush() throws IOException { a.flush(); b.flush(); } - @Override public void close() throws IOException { try { a.close(); } finally { b.close(); } } - } -} +package org.openpdf.text; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.openpdf.text.pdf.*; + +import java.io.*; +import java.lang.reflect.Method; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.Instant; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.*; + +class Pdf20FeatureWriteAndVerifyTest { + + // Persistent output dir; not cleaned up by the test runner. + private static final Path OUT_DIR = Path.of(System.getProperty("openpdf.outdir", "target/pdf20-tests")); + + @Test + @DisplayName("Create PDF 2.0 to disk (no cleanup): header/version + Lang + AF + EmbeddedFiles + XMP + Unicode /UF") + void createAndVerifyPdf20Features_persistent() throws Exception { + Files.createDirectories(OUT_DIR); + + // -------- CREATE MAIN PDF -------- + Path outMain = OUT_DIR.resolve("pdf20-features.pdf"); + byte[] pdfBytes = createPdf20WithFeatures(outMain); + System.out.println("Wrote: " + outMain.toAbsolutePath()); + + // Header must be %PDF-2.0 + String header = new String(pdfBytes, 0, Math.min(4096, pdfBytes.length), StandardCharsets.ISO_8859_1); + assertTrue(header.startsWith("%PDF-2.0"), "Header must be %PDF-2.0"); + + // Verify via PdfReader + try (PdfReader reader = new PdfReader(new ByteArrayInputStream(pdfBytes))) { + assertTrue(reader.getNumberOfPages() >= 1, "Expected at least one page"); + + String readerVer = getVersionFromReaderNormalized(reader); + if (readerVer != null) { + assertEquals("2.0", readerVer, "Reader version should be 2.0"); + } + + PdfDictionary catalog = reader.getCatalog(); + assertNotNull(catalog, "Catalog missing"); + assertEquals("en-US", stripParens(catalog.getAsString(PdfName.LANG)), "Catalog /Lang mismatch"); + + // /AF present with two attachments + PdfArray afArr = catalog.getAsArray(new PdfName("AF")); + assertNotNull(afArr, "/AF missing on Catalog"); + assertTrue(afArr.size() >= 2, "/AF should reference both attachments"); + + // AF[0] AFRelationship=Data + PdfDictionary fs1Dict = derefToDict(afArr.getAsIndirectObject(0)); + assertEquals(new PdfName("Data"), fs1Dict.get(new PdfName("AFRelationship"))); + + // AF[1] has Unicode filename via /UF and AFRelationship=Data + PdfDictionary fs2Dict = derefToDict(afArr.getAsIndirectObject(1)); + assertEquals(new PdfName("Data"), fs2Dict.get(new PdfName("AFRelationship"))); + PdfObject uf = fs2Dict.get(PdfName.UF); + assertNotNull(uf, "Filespec #2 should contain /UF for Unicode filename"); + assertTrue(uf.toString().contains("les_meg_"), "Expected Unicode filename in /UF"); + + // Names tree: EmbeddedFiles present + PdfDictionary names = catalog.getAsDict(PdfName.NAMES); + assertNotNull(names, "/Names missing"); + PdfDictionary embeddedFiles = names.getAsDict(PdfName.EMBEDDEDFILES); + assertNotNull(embeddedFiles, "/Names/EmbeddedFiles missing"); + PdfArray nameArray = embeddedFiles.getAsArray(PdfName.NAMES); + assertNotNull(nameArray, "/Names/EmbeddedFiles/Names array missing"); + assertTrue(nameArray.size() >= 2, "EmbeddedFiles name tree should contain pairs"); + + // XMP present + byte[] xmp = reader.getMetadata(); + assertNotNull(xmp, "XMP metadata should be present"); + String xmpStr = new String(xmp, StandardCharsets.UTF_8); + assertTrue(xmpStr.contains(" info = reader.getInfo(); + assertTrue(info.containsKey("Title")); + assertTrue(info.get("Title").contains("æ") && info.get("Title").contains("π"), + "Unicode Title should round-trip"); + } + + // -------- WRITE A STAMPED VARIANT WITH PAGE-LEVEL /Lang OVERRIDE (ALSO NOT DELETED) -------- + Path outStamped = OUT_DIR.resolve("pdf20-features-lang-override.pdf"); + try (PdfReader reader = new PdfReader(outMain.toString()); + OutputStream os = Files.newOutputStream(outStamped)) { + PdfStamper stamper = new PdfStamper(reader, os); + PdfDictionary page1 = reader.getPageN(1); + page1.put(PdfName.LANG, new PdfString("nb-NO")); + stamper.close(); + } + System.out.println("Wrote: " + outStamped.toAbsolutePath()); + + try (PdfReader r2 = new PdfReader(outStamped.toString())) { + assertEquals("en-US", stripParens(r2.getCatalog().getAsString(PdfName.LANG)), + "Catalog /Lang should remain en-US"); + assertEquals("nb-NO", stripParens(r2.getPageN(1).getAsString(PdfName.LANG)), + "Page 1 /Lang override should be nb-NO"); + } + + System.out.println("Output directory: " + OUT_DIR.toAbsolutePath()); + System.out.println("Done. Files are NOT deleted."); + } + + // -------- helpers -------- + + private static byte[] createPdf20WithFeatures(Path out) throws Exception { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + + Document doc = new Document(); + PdfWriter writer; + try (OutputStream fos = Files.newOutputStream(out)) { + writer = PdfWriter.getInstance(doc, new TeeOutputStream(baos, fos)); + writer.setXmpMetadata(minimalXmp("en-US", "OpenPDF 2.0 feature test").getBytes(StandardCharsets.UTF_8)); + + doc.open(); + + // Catalog /Lang + writer.getExtraCatalog().put(PdfName.LANG, new PdfString("en-US")); + + // Content + doc.add(new Paragraph("Hello PDF 2.0 – " + Instant.now())); + + // Attachment #1 (AFRelationship=Data) + byte[] bytes1 = "Hello attachment #1".getBytes(StandardCharsets.UTF_8); + PdfFileSpecification fs1 = PdfFileSpecification.fileEmbedded(writer, null, "readme.txt", bytes1); + fs1.put(new PdfName("AFRelationship"), new PdfName("Data")); + writer.addFileAttachment(fs1); + + // Attachment #2 with Unicode filename via /UF + byte[] bytes2 = "Hello attachment #2 (UTF-16 filename)".getBytes(StandardCharsets.UTF_8); + String unicodeName = "les_meg_æøå.txt"; + PdfFileSpecification fs2 = PdfFileSpecification.fileEmbedded(writer, null, unicodeName, bytes2); + fs2.put(PdfName.UF, new PdfString(unicodeName, PdfObject.TEXT_UNICODE)); + fs2.put(new PdfName("AFRelationship"), new PdfName("Data")); + writer.addFileAttachment(fs2); + + // /AF array referencing both + PdfArray af = new PdfArray(); + af.add(fs1.getReference()); + af.add(fs2.getReference()); + writer.getExtraCatalog().put(new PdfName("AF"), af); + + // Unicode Title (Info) + doc.addTitle("Tittel æøå – π"); + + doc.close(); // closes writer & flushes both streams + } + + return baos.toByteArray(); + } + + /** Minimal XMP packet (UTF-8). */ + private static String minimalXmp(String lang, String dcTitle) { + return "" + + "" + + "" + + " " + + " " + + " " + + " " + escapeXml(dcTitle) + "" + + " " + + " " + + " " + + "" + + ""; + } + + private static String escapeXml(String s) { + return s.replace("&", "&").replace("<", "<").replace(">", ">"); + } + + /** Normalize PdfReader#getPdfVersion(): '4' -> "1.4", "2.0" -> "2.0". May return null. */ + private static String getVersionFromReaderNormalized(PdfReader reader) { + try { + Method m = reader.getClass().getMethod("getPdfVersion"); + Object v = m.invoke(reader); + if (v == null) { + return null; + } + if (v instanceof Character) { + char c = (Character) v; + if (Character.isDigit(c)) { + return "1." + c; + } + return String.valueOf(c); + } + if (v instanceof String) { + return (String) v; + } + } catch (NoSuchMethodException ignored) { + // OK: header check already covers us + } catch (Exception e) { + System.out.println("getPdfVersion() reflection error: " + e); + } + return null; + } + + private static String stripParens(PdfString s) { + return s == null ? null : s.toString().replaceAll("[()]", ""); + } + + private static PdfDictionary derefToDict(PdfIndirectReference ref) { + if (ref == null) { + return null; + } + PdfObject obj = PdfReader.getPdfObject(ref); + return (obj instanceof PdfDictionary) ? (PdfDictionary) obj : null; + } + + /** Writes to two streams at once (memory + file). */ + private static final class TeeOutputStream extends OutputStream { + private final OutputStream a; + private final OutputStream b; + + TeeOutputStream(OutputStream a, OutputStream b) { + this.a = a; + this.b = b; + } + + @Override + public void write(int i) throws IOException { + a.write(i); + b.write(i); + } + + @Override + public void write(byte[] buf) throws IOException { + a.write(buf); + b.write(buf); + } + + @Override + public void write(byte[] buf, int off, int len) throws IOException { + a.write(buf, off, len); + b.write(buf, off, len); + } + + @Override + public void flush() throws IOException { + a.flush(); + b.flush(); + } + + @Override + public void close() throws IOException { + try { + a.close(); + } finally { + b.close(); + } + } + } +} diff --git a/openpdf-core/src/test/java/org/openpdf/text/StandardFontsTest.java b/openpdf-core/src/test/java/org/openpdf/text/StandardFontsTest.java index ba832a19f..2d546f9dc 100644 --- a/openpdf-core/src/test/java/org/openpdf/text/StandardFontsTest.java +++ b/openpdf-core/src/test/java/org/openpdf/text/StandardFontsTest.java @@ -21,10 +21,7 @@ import org.openpdf.text.pdf.PdfWriter; import java.io.FileOutputStream; import java.io.IOException; -import java.util.Arrays; import java.util.List; -import java.util.stream.Collectors; -import org.assertj.core.api.SoftAssertions; import org.junit.jupiter.api.Test; class StandardFontsTest { diff --git a/openpdf-core/src/test/java/org/openpdf/text/pdf/ColumnTextFirstLineBaselineTest.java b/openpdf-core/src/test/java/org/openpdf/text/pdf/ColumnTextFirstLineBaselineTest.java index 6592b3724..ba07ec453 100644 --- a/openpdf-core/src/test/java/org/openpdf/text/pdf/ColumnTextFirstLineBaselineTest.java +++ b/openpdf-core/src/test/java/org/openpdf/text/pdf/ColumnTextFirstLineBaselineTest.java @@ -6,7 +6,6 @@ import org.openpdf.text.Font; import org.openpdf.text.PageSize; import org.openpdf.text.Paragraph; -import org.openpdf.text.pdf.BaseFont; import java.io.ByteArrayOutputStream; import java.io.File; diff --git a/openpdf-core/src/test/java/org/openpdf/text/pdf/FirstLineBaselineExample.java b/openpdf-core/src/test/java/org/openpdf/text/pdf/FirstLineBaselineExample.java index 0291bb0b7..1c66b2f11 100644 --- a/openpdf-core/src/test/java/org/openpdf/text/pdf/FirstLineBaselineExample.java +++ b/openpdf-core/src/test/java/org/openpdf/text/pdf/FirstLineBaselineExample.java @@ -1,7 +1,6 @@ package org.openpdf.text.pdf; import org.openpdf.text.*; -import org.openpdf.text.pdf.BaseFont; import java.io.FileOutputStream; diff --git a/openpdf-core/src/test/java/org/openpdf/text/pdf/PdfAConformanceTest.java b/openpdf-core/src/test/java/org/openpdf/text/pdf/PdfAConformanceTest.java index 1c7f9543b..187fbf18a 100644 --- a/openpdf-core/src/test/java/org/openpdf/text/pdf/PdfAConformanceTest.java +++ b/openpdf-core/src/test/java/org/openpdf/text/pdf/PdfAConformanceTest.java @@ -7,7 +7,6 @@ import org.openpdf.text.Document; import org.openpdf.text.DocumentException; import org.openpdf.text.PageSize; -import org.openpdf.text.Phrase; import org.openpdf.text.pdf.internal.PdfXConformanceImp; import org.openpdf.text.xml.xmp.PdfA1Schema; import org.openpdf.text.xml.xmp.XmpWriter;