diff --git a/pdfbox-layout-awt/src/test/java/org/apache/pdfbox/glyphlayout/awt/GlyphLayoutBidiTest.java b/pdfbox-layout-awt/src/test/java/org/apache/pdfbox/glyphlayout/awt/GlyphLayoutBidiTest.java index ba0a50e9378..a69dd7bdd35 100644 --- a/pdfbox-layout-awt/src/test/java/org/apache/pdfbox/glyphlayout/awt/GlyphLayoutBidiTest.java +++ b/pdfbox-layout-awt/src/test/java/org/apache/pdfbox/glyphlayout/awt/GlyphLayoutBidiTest.java @@ -16,16 +16,27 @@ */ package org.apache.pdfbox.glyphlayout.awt; +import org.apache.pdfbox.Loader; +import org.apache.pdfbox.pdmodel.AbstractGlyphLayoutProcessor; +import org.apache.pdfbox.pdmodel.PDDocument; +import org.apache.pdfbox.pdmodel.PDPage; +import org.apache.pdfbox.pdmodel.PDPageContentStream; +import org.apache.pdfbox.pdmodel.font.PDType0Font; +import org.apache.pdfbox.text.PDFTextStripper; import org.junit.jupiter.api.Test; import java.awt.FontFormatException; +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileOutputStream; import java.io.IOException; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.Writer; import java.net.URISyntaxException; +import java.nio.charset.StandardCharsets; -import org.apache.pdfbox.pdmodel.PDDocument; -import org.apache.pdfbox.pdmodel.PDPage; -import org.apache.pdfbox.pdmodel.PDPageContentStream; -import org.apache.pdfbox.pdmodel.font.PDType0Font; +import static org.junit.jupiter.api.Assertions.assertEquals; /** * Examples for bidirectional text with GlyphLayoutProcessorAwt @@ -73,13 +84,52 @@ private float showLine(PDPageContentStream cs, PDType0Font[] fonts, float fontSi return y; } + /** + * Test, no ActualText + * + * @throws IOException + * @throws FontFormatException + * @throws URISyntaxException + */ + @Test + void testGlyphLayoutBidiNoActualText() throws IOException, FontFormatException, URISyntaxException + { + testGlyphLayoutBidi(false, ""); + } + + /** + * Test with ActualText + * + * @throws IOException + * @throws FontFormatException + * @throws URISyntaxException + */ @Test - void testGlyphLayoutBidi() throws IOException, FontFormatException, URISyntaxException + void testGlyphLayoutBidiActualText() throws IOException, FontFormatException, URISyntaxException { - GlyphLayoutProcessorAwt glyphLayoutProcessorAwt = new GlyphLayoutProcessorAwt(); + testGlyphLayoutBidi(true, "_ActualText"); + } + + /** + * Test + * @param useActualText + * @throws IOException + * @throws FontFormatException + * @throws URISyntaxException + */ + void testGlyphLayoutBidi(boolean useActualText, String sActualText) throws IOException, FontFormatException, URISyntaxException + { + AbstractGlyphLayoutProcessor.GlyphLayoutProcessorOptions options = new AbstractGlyphLayoutProcessor.GlyphLayoutProcessorOptions(); + if (useActualText) + { + options.useActualText(); + } + GlyphLayoutProcessorAwt glyphLayoutProcessor = new GlyphLayoutProcessorAwt(options); + + String outputBaseName = String.format("GlyphLayoutBidi%s", sActualText); + String outputPDFFilename = "target/" + outputBaseName + ".pdf"; + String outputTextFilename = "target/" + outputBaseName + ".txt"; - String outputName = "GlyphLayoutBidi.pdf"; - String outputFilename = "target/" + outputName; String arabicPath = "/ttf/NotoSansArabic-Regular.ttf"; String lgcPath = "/ttf/DejaVuSans.ttf"; @@ -87,14 +137,14 @@ void testGlyphLayoutBidi() throws IOException, FontFormatException, URISyntaxExc try (PDDocument doc = new PDDocument()) { - PDType0Font arabicFont = createPdType0Font(glyphLayoutProcessorAwt, doc, arabicPath); - PDType0Font lgcFont = createPdType0Font(glyphLayoutProcessorAwt, doc, lgcPath); + PDType0Font arabicFont = createPdType0Font(glyphLayoutProcessor, doc, arabicPath); + PDType0Font lgcFont = createPdType0Font(glyphLayoutProcessor, doc, lgcPath); PDPage page = new PDPage(); doc.addPage(page); try (PDPageContentStream cs = new PDPageContentStream(doc, page)) { - cs.setGlyphLayoutProcessor(glyphLayoutProcessorAwt); + cs.setGlyphLayoutProcessor(glyphLayoutProcessor); float x = page.getBBox().getLowerLeftX() + fontSize; float y = page.getBBox().getUpperRightY() - fontSize; @@ -102,8 +152,43 @@ void testGlyphLayoutBidi() throws IOException, FontFormatException, URISyntaxExc y = showLine(cs, arabicFont, fontSize, x, y, TEXT1); showLine(cs, new PDType0Font[]{ lgcFont, arabicFont, lgcFont }, fontSize, x, y, new String[]{ TEXT2, TEXT3, TEXT4 }); } - doc.save(outputFilename); + doc.save(outputPDFFilename); + } + + checkRenderIdent(outputBaseName + ".pdf"); + + // Extract text + try (PDDocument doc = Loader.loadPDF(new File(outputPDFFilename))) { + assertEquals(1, doc.getNumberOfPages()); + + PDFTextStripper stripper = new PDFTextStripper(); + String s = stripper.getText(doc); + String sStripped = s.replace("\r", "").replaceAll(" +", " ") + .replace(" \n", "\n") + .strip(); + + String text = + TEXT1 + "\n" + TEXT2 + TEXT3 + TEXT4; + + if (useActualText) { + // Not correct as of 2026-09-27 + // PDFTextStripper should take the text from ActualText as is and not reorder the text + assertEquals(text, sStripped, "Extracted text should equal the written text for " + outputPDFFilename); + } else { + // Extracted text is wrong + assertEquals(text, sStripped, "Extracted text should equal the written text for " + outputPDFFilename); + } + + try (OutputStream os = new FileOutputStream(outputTextFilename)) { + os.write(0xEF); + os.write(0xBB); + os.write(0xBF); + + try (Writer writer = new BufferedWriter(new OutputStreamWriter(os, StandardCharsets.UTF_8))) { + // The output is not yet correct as of 27.9.2026 + writer.write(s); + } + } } - checkRenderIdent(outputName); } } diff --git a/pdfbox-layout-awt/src/test/java/org/apache/pdfbox/glyphlayout/awt/GlyphLayoutDin91379FormTest.java b/pdfbox-layout-awt/src/test/java/org/apache/pdfbox/glyphlayout/awt/GlyphLayoutDin91379FormTest.java index f9fd7b41789..02a50eabdd7 100644 --- a/pdfbox-layout-awt/src/test/java/org/apache/pdfbox/glyphlayout/awt/GlyphLayoutDin91379FormTest.java +++ b/pdfbox-layout-awt/src/test/java/org/apache/pdfbox/glyphlayout/awt/GlyphLayoutDin91379FormTest.java @@ -17,11 +17,19 @@ package org.apache.pdfbox.glyphlayout.awt; import java.awt.FontFormatException; +import java.io.BufferedWriter; import java.io.File; +import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.Writer; import java.net.URISyntaxException; +import java.nio.charset.StandardCharsets; + import org.apache.pdfbox.Loader; +import org.apache.pdfbox.pdmodel.AbstractGlyphLayoutProcessor; import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.PDDocumentCatalog; import org.apache.pdfbox.pdmodel.PDResources; @@ -29,8 +37,11 @@ import org.apache.pdfbox.pdmodel.interactive.form.PDAcroForm; import org.apache.pdfbox.pdmodel.interactive.form.PDField; import org.apache.pdfbox.pdmodel.interactive.form.PDTextField; +import org.apache.pdfbox.text.PDFTextStripper; import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertEquals; + /* * Example of formatting for letters defined in: * DIN 91379: Characters and defined character sequences in Unicode for the electronic processing of names @@ -84,15 +95,51 @@ class GlyphLayoutDin91379FormTest extends TestBase + "ƒ ʰ ʳ ˆ ˜ ˢ ᵈ ᵗ ‘ ‚ “ ” „ † … ‰ ′ ″ ‹ › ⁰ ⁴ ⁵ ⁶ ⁷ ⁸ " + "⁹ ⁿ ₀ ₁ ₂ ₃ ₄ ₅ ₆ ₇ ₈ ₉ ™ ∞ ≤ ≥ \n" + "Additional non-letters (not included in DIN 91379): – — •�"; + /** + * Test, no ActualText + * + * @throws IOException + * @throws FontFormatException + * @throws URISyntaxException + */ + @Test + void testGlyphLayoutDin91379FormNoActualText() throws IOException, FontFormatException, URISyntaxException + { + testGlyphLayoutDin91379Form(false, ""); + } + /** + * Test with ActualText + * + * @throws IOException + * @throws FontFormatException + * @throws URISyntaxException + */ @Test - void testGlyphLayoutDin91379Form() throws IOException, FontFormatException, URISyntaxException + void testGlyphLayoutDin91379FormActualText() throws IOException, FontFormatException, URISyntaxException { - GlyphLayoutProcessorAwt glyphLayoutProcessor = new GlyphLayoutProcessorAwt(); + testGlyphLayoutDin91379Form(true, "_ActualText"); + } - String outputName = "GlyphLayoutDIN91379Form.pdf"; - String outputFilename = "target/" + outputName; + /** + * Test + * @param useActualText + * @throws IOException + * @throws FontFormatException + * @throws URISyntaxException + */ + void testGlyphLayoutDin91379Form(boolean useActualText, String sActualText) throws IOException, FontFormatException, URISyntaxException + { + AbstractGlyphLayoutProcessor.GlyphLayoutProcessorOptions options = new AbstractGlyphLayoutProcessor.GlyphLayoutProcessorOptions(); + if (useActualText) + { + options.useActualText(); + } + GlyphLayoutProcessorAwt glyphLayoutProcessor = new GlyphLayoutProcessorAwt(options); + String outputBaseName = String.format("GlyphLayoutDIN91379Form%s", sActualText); + String outputPDFFilename = "target/" + outputBaseName + ".pdf"; + String outputTextFilename = "target/" + outputBaseName + ".txt"; String fontSizeString = "12"; InputStream fontStream = GlyphLayoutDin91379FormTest.class.getResourceAsStream("/ttf/Arimo-Regular.ttf"); @@ -125,8 +172,47 @@ void testGlyphLayoutDin91379Form() throws IOException, FontFormatException, URIS } acroForm.refreshAppearances(); acroForm.flatten(); - doc.save(outputFilename); + doc.save(outputPDFFilename); + } + + checkRenderIdent(outputBaseName + ".pdf"); + + // Extract text + try (PDDocument doc = Loader.loadPDF(new File(outputPDFFilename))) { + assertEquals(1, doc.getNumberOfPages()); + + PDFTextStripper stripper = new PDFTextStripper(); + String s = stripper.getText(doc); + String sStripped = s.replace('\n', ' ') + .replaceAll(" +", " ") + .replace(" ", "\n") + .strip(); + + String expectedText = ("Test form for PDFBox glyph layout\n" + LATIN_CHARS_DIN_91379) + .replace('\n', ' ') + .replaceAll(" +", " ") + .replace(" ", "\n") + .strip(); + + try (OutputStream os = new FileOutputStream(outputTextFilename)) { + os.write(0xEF); + os.write(0xBB); + os.write(0xBF); + + try (Writer writer = new BufferedWriter(new OutputStreamWriter(os, StandardCharsets.UTF_8))) { + // The output without ActualText is not yet correct + writer.write(s); + } + } + + if (useActualText) { + // ok with ActualText + assertEquals(expectedText, sStripped, "Extracted text should equal the written text for " + outputPDFFilename); + } else { + // not ok without ActualText + assertEquals(expectedText, sStripped, "Extracted text should equal the written text for " + outputPDFFilename); + } + } - checkRenderIdent(outputName); } } diff --git a/pdfbox-layout-awt/src/test/java/org/apache/pdfbox/glyphlayout/awt/GlyphLayoutSMPTest.java b/pdfbox-layout-awt/src/test/java/org/apache/pdfbox/glyphlayout/awt/GlyphLayoutSMPTest.java index bbf350d5267..ad4240bd2d7 100644 --- a/pdfbox-layout-awt/src/test/java/org/apache/pdfbox/glyphlayout/awt/GlyphLayoutSMPTest.java +++ b/pdfbox-layout-awt/src/test/java/org/apache/pdfbox/glyphlayout/awt/GlyphLayoutSMPTest.java @@ -18,15 +18,28 @@ package org.apache.pdfbox.glyphlayout.awt; import java.awt.FontFormatException; +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileOutputStream; import java.io.IOException; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.Writer; import java.net.URISyntaxException; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import org.apache.pdfbox.Loader; +import org.apache.pdfbox.pdmodel.AbstractGlyphLayoutProcessor; import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.PDPage; import org.apache.pdfbox.pdmodel.PDPageContentStream; import org.apache.pdfbox.pdmodel.font.PDType0Font; +import org.apache.pdfbox.text.PDFTextStripper; import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertEquals; + /** * Examples for Supplementary Multilingual Plane with GlyphLayoutProcessorAwt that require a * representation with two characters in Java. This is not a new functionality but a regression test @@ -97,13 +110,52 @@ private float showLine(PDPageContentStream cs, PDType0Font font, float fontSize, return y; } + /** + * Test, no ActualText + * + * @throws IOException + * @throws FontFormatException + * @throws URISyntaxException + */ + @Test + void testGlyphLayoutSMPNoActualText() throws IOException, FontFormatException, URISyntaxException + { + testGlyphLayoutSMP(false, ""); + } + + /** + * Test with ActualText + * + * @throws IOException + * @throws FontFormatException + * @throws URISyntaxException + */ @Test - void testGlyphLayoutSMP() throws IOException, FontFormatException, URISyntaxException + void testGlyphLayoutSMPActualText() throws IOException, FontFormatException, URISyntaxException + { + testGlyphLayoutSMP(true, "_ActualText"); + } + + /** + * Test + * @param useActualText + * @throws IOException + * @throws FontFormatException + * @throws URISyntaxException + */ + void testGlyphLayoutSMP(boolean useActualText, String sActualText) throws IOException, FontFormatException, URISyntaxException { - GlyphLayoutProcessorAwt glyphLayoutProcessor = new GlyphLayoutProcessorAwt(); + AbstractGlyphLayoutProcessor.GlyphLayoutProcessorOptions options = new AbstractGlyphLayoutProcessor.GlyphLayoutProcessorOptions(); + if (useActualText) + { + options.useActualText(); + } + GlyphLayoutProcessorAwt glyphLayoutProcessor = new GlyphLayoutProcessorAwt(options); + + String outputBaseName = String.format("GlyphLayoutSMP%s", sActualText); + String outputPDFFilename = "target/" + outputBaseName + ".pdf"; + String outputTextFilename = "target/" + outputBaseName + ".txt"; - String outputName = "GlyphLayoutSMP.pdf"; - String outputFilename = "target/" + outputName; String sansFontPath = "/ttf/Arimo-Regular.ttf"; String mathFontPath = "/ttf/NotoSansMath-Regular.ttf"; @@ -128,8 +180,40 @@ void testGlyphLayoutSMP() throws IOException, FontFormatException, URISyntaxExce y = showLine(cs, sansFont, fontSize, x, y, "Font used: " + mathFont.getName()); showLines(cs, mathFont, fontSize, x, y, MATHEMATICAL); } - doc.save(outputFilename); + doc.save(outputPDFFilename); + } + + checkRenderIdent(outputBaseName + ".pdf"); + + // Extract text + try (PDDocument doc = Loader.loadPDF(new File(outputPDFFilename))) { + assertEquals(1, doc.getNumberOfPages()); + + PDFTextStripper stripper = new PDFTextStripper(); + String s = stripper.getText(doc); + String sStripped = s.strip(); + + String[] lines = + Arrays.stream((TEXT_INTRO + "\n" + "Font used: NotoSansMath-Regular" + "\n" + MATHEMATICAL) + .split("\n")).filter(line->line.length()>0).toArray(i ->new String[i]); + String text = String.join("\n", lines); + + if (useActualText) { + assertEquals(text, sStripped, "Extracted text should equal the written text for " + outputPDFFilename); + } else { + assertEquals(text, sStripped, "Extracted text should equal the written text for " + outputPDFFilename); + } + + try (OutputStream os = new FileOutputStream(outputTextFilename)) { + os.write(0xEF); + os.write(0xBB); + os.write(0xBF); + + try (Writer writer = new BufferedWriter(new OutputStreamWriter(os, StandardCharsets.UTF_8))) { + // The output is correct + writer.write(s); + } + } } - checkRenderIdent(outputName); } } diff --git a/pdfbox-layout-awt/src/test/resources/pdf/GlyphLayoutBidi_ActualText.pdf b/pdfbox-layout-awt/src/test/resources/pdf/GlyphLayoutBidi_ActualText.pdf new file mode 100644 index 00000000000..9f6cf33fb2d Binary files /dev/null and b/pdfbox-layout-awt/src/test/resources/pdf/GlyphLayoutBidi_ActualText.pdf differ diff --git a/pdfbox-layout-awt/src/test/resources/pdf/GlyphLayoutDIN91379Form_ActualText.pdf b/pdfbox-layout-awt/src/test/resources/pdf/GlyphLayoutDIN91379Form_ActualText.pdf new file mode 100644 index 00000000000..d177431fd4f Binary files /dev/null and b/pdfbox-layout-awt/src/test/resources/pdf/GlyphLayoutDIN91379Form_ActualText.pdf differ diff --git a/pdfbox-layout-awt/src/test/resources/pdf/GlyphLayoutSMP_ActualText.pdf b/pdfbox-layout-awt/src/test/resources/pdf/GlyphLayoutSMP_ActualText.pdf new file mode 100644 index 00000000000..1990ef67938 Binary files /dev/null and b/pdfbox-layout-awt/src/test/resources/pdf/GlyphLayoutSMP_ActualText.pdf differ