diff --git a/pdfbox-layout-awt/src/main/java/org/apache/pdfbox/glyphlayout/awt/GlyphLayoutProcessorAwt.java b/pdfbox-layout-awt/src/main/java/org/apache/pdfbox/glyphlayout/awt/GlyphLayoutProcessorAwt.java index 1d09799972d..3937cba8cf1 100644 --- a/pdfbox-layout-awt/src/main/java/org/apache/pdfbox/glyphlayout/awt/GlyphLayoutProcessorAwt.java +++ b/pdfbox-layout-awt/src/main/java/org/apache/pdfbox/glyphlayout/awt/GlyphLayoutProcessorAwt.java @@ -29,6 +29,7 @@ import java.io.InputStream; import java.util.Objects; +import org.apache.pdfbox.cos.COSName; import org.apache.pdfbox.pdmodel.AbstractGlyphLayoutProcessor; import org.apache.pdfbox.pdmodel.ContentStreamForGlyphLayoutInterface; import org.apache.pdfbox.pdmodel.GlyphLayoutProcessorInterface; @@ -48,7 +49,17 @@ public class GlyphLayoutProcessorAwt extends AbstractGlyphLayoutProcessor implements GlyphLayoutProcessorInterface { - private final GlyphLayoutFontLoaderAwt glyphLayoutFontLoaderAwt; + private final GlyphLayoutFontLoaderAwt glyphLayoutFontLoaderAwt = new GlyphLayoutFontLoaderAwt(); + + + /** + * Constructs a GlyphLayoutProcessorFop with options + * + */ + public GlyphLayoutProcessorAwt(AbstractGlyphLayoutProcessor.GlyphLayoutProcessorOptions options) + { + super(options); + } /** * Constructs a GlyphLayoutProcessorAwt @@ -56,10 +67,12 @@ public class GlyphLayoutProcessorAwt extends AbstractGlyphLayoutProcessor implem */ public GlyphLayoutProcessorAwt() { - this.glyphLayoutFontLoaderAwt = new GlyphLayoutFontLoaderAwt(); + super(); } - /** + + + /** * Checks if glyphs needed for text are missing in awtFont * * @param text text to be checked diff --git a/pdfbox-layout-awt/src/test/java/org/apache/pdfbox/glyphlayout/awt/GlyphLayoutDin91379Test.java b/pdfbox-layout-awt/src/test/java/org/apache/pdfbox/glyphlayout/awt/GlyphLayoutDin91379Test.java index 03db0162ebb..94253f42a4f 100644 --- a/pdfbox-layout-awt/src/test/java/org/apache/pdfbox/glyphlayout/awt/GlyphLayoutDin91379Test.java +++ b/pdfbox-layout-awt/src/test/java/org/apache/pdfbox/glyphlayout/awt/GlyphLayoutDin91379Test.java @@ -26,8 +26,11 @@ 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.junit.jupiter.api.Test; import org.apache.pdfbox.pdmodel.PDDocument; @@ -90,14 +93,47 @@ class GlyphLayoutDin91379Test extends TestBase + "⁹ ⁿ ₀ ₁ ₂ ₃ ₄ ₅ ₆ ₇ ₈ ₉ ™ ∞ ≤ ≥\n" + "Additional non-letters (not included in DIN 91379): – — •�"; + /** + * Test, no ActualText + * @throws IOException + * @throws FontFormatException + * @throws URISyntaxException + */ + @Test + void testGlyphLayoutDin91379NoActualText() throws IOException, FontFormatException, URISyntaxException { + testGlyphLayoutDin91379(false, ""); + } + + /** + * Test with ActualText + * @throws IOException + * @throws FontFormatException + * @throws URISyntaxException + */ @Test - void testGlyphLayoutDin91379() throws IOException, FontFormatException, URISyntaxException + void testGlyphLayoutDin91379UseActualText() throws IOException, FontFormatException, URISyntaxException { + testGlyphLayoutDin91379(true, "_ActualText"); + } + + /** + * Test GlyphLayoutProcessorAwt with letters and sequences from DIN 91379 + * @param useActualText + * @throws IOException + * @throws FontFormatException + * @throws URISyntaxException + */ + void testGlyphLayoutDin91379(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 outputName = "GlyphLayoutDIN91379.pdf"; + String outputName = String.format("GlyphLayoutDIN91379%s.pdf", sActualText); String outputPDFFilename = "target/" + outputName; - String outputTextFilename = "target/GlyphLayoutDIN91379.txt"; + String outputTextFilename = String.format("target/GlyphLayoutDIN91379%s.txt", sActualText); + float fontSize = 12.0f; try (PDDocument doc = new PDDocument()) @@ -133,7 +169,7 @@ void testGlyphLayoutDin91379() throws IOException, FontFormatException, URISynta os.write (0xBB); os.write (0xBF); - try (Writer writer = new BufferedWriter(new OutputStreamWriter(os, "utf-8"))) + try (Writer writer = new BufferedWriter(new OutputStreamWriter(os, StandardCharsets.UTF_8))) { //TODO compare this output with the input, like in TextStripper test // Not yet correct as of 4.7.2026 @@ -158,7 +194,7 @@ private void showComposites(PDPageContentStream cs, PDType0Font font, float font if (!line.isEmpty()) { showCompositesLine(cs, font, fontSize, x, y, line); - y -= fontSize * 1.5; + y -= fontSize * 1.5f; } } } diff --git a/pdfbox-layout-awt/src/test/resources/pdf/GlyphLayoutDIN91379_ActualText.pdf b/pdfbox-layout-awt/src/test/resources/pdf/GlyphLayoutDIN91379_ActualText.pdf new file mode 100644 index 00000000000..e30eda7314a Binary files /dev/null and b/pdfbox-layout-awt/src/test/resources/pdf/GlyphLayoutDIN91379_ActualText.pdf differ diff --git a/pdfbox-layout-fop/src/main/java/org/apache/pdfbox/glyphlayout/fop/GlyphLayoutProcessorFop.java b/pdfbox-layout-fop/src/main/java/org/apache/pdfbox/glyphlayout/fop/GlyphLayoutProcessorFop.java index eae5e72458c..21c833d3dda 100644 --- a/pdfbox-layout-fop/src/main/java/org/apache/pdfbox/glyphlayout/fop/GlyphLayoutProcessorFop.java +++ b/pdfbox-layout-fop/src/main/java/org/apache/pdfbox/glyphlayout/fop/GlyphLayoutProcessorFop.java @@ -45,7 +45,7 @@ public class GlyphLayoutProcessorFop extends AbstractGlyphLayoutProcessor implements GlyphLayoutProcessorInterface { - private final GlyphLayoutFontLoaderFop glyphLayoutFontLoaderFop; + private final GlyphLayoutFontLoaderFop glyphLayoutFontLoaderFop = new GlyphLayoutFontLoaderFop(); /* Before you call GlyphMapping.doGlyphMapping to position the glyphs, @@ -54,13 +54,22 @@ public class GlyphLayoutProcessorFop extends AbstractGlyphLayoutProcessor implem */ private static final float FOP_FONTSIZE_FACTOR = 1000f; + /** + * Constructs a GlyphLayoutProcessorFop with options + * + */ + public GlyphLayoutProcessorFop(AbstractGlyphLayoutProcessor.GlyphLayoutProcessorOptions options) + { + super(options); + } + /** * Constructs a GlyphLayoutProcessorFop * */ public GlyphLayoutProcessorFop() { - this.glyphLayoutFontLoaderFop = new GlyphLayoutFontLoaderFop(); + super(); } /** diff --git a/pdfbox-layout-fop/src/test/java/org/apache/pdfbox/glyphlayout/fop/GlyphLayoutDin91379Test.java b/pdfbox-layout-fop/src/test/java/org/apache/pdfbox/glyphlayout/fop/GlyphLayoutDin91379Test.java index dc6dd74630f..b97667d6148 100644 --- a/pdfbox-layout-fop/src/test/java/org/apache/pdfbox/glyphlayout/fop/GlyphLayoutDin91379Test.java +++ b/pdfbox-layout-fop/src/test/java/org/apache/pdfbox/glyphlayout/fop/GlyphLayoutDin91379Test.java @@ -25,9 +25,11 @@ import java.io.OutputStreamWriter; import java.io.Writer; import java.net.URISyntaxException; +import java.nio.charset.StandardCharsets; import static org.junit.jupiter.api.Assertions.assertEquals; +import org.apache.pdfbox.pdmodel.AbstractGlyphLayoutProcessor; import org.junit.jupiter.api.Test; import org.apache.pdfbox.Loader; @@ -90,14 +92,44 @@ class GlyphLayoutDin91379Test extends TestBase + "⁹ ⁿ ₀ ₁ ₂ ₃ ₄ ₅ ₆ ₇ ₈ ₉ ™ ∞ ≤ ≥\n" + "Additional non-letters (not included in DIN 91379): – — •�"; + /** + * Test, no ActualText + * @throws IOException + * @throws URISyntaxException + */ + @Test + void testGlyphLayoutDin91379NoActualText() throws IOException, URISyntaxException { + testGlyphLayoutDin91379(false, ""); + } + + /** + * Test with ActualText + * @throws IOException + * @throws URISyntaxException + */ @Test - void testGlyphLayoutDin91379() throws IOException, URISyntaxException + void testGlyphLayoutDin91379UseActualText() throws IOException, URISyntaxException { + testGlyphLayoutDin91379(true, "_ActualText"); + } + + /** + * Test GlyphLayoutProcessorAwt with letters and sequences from DIN 91379 + * @param useActualText + * @throws IOException + * @throws URISyntaxException + */ + void testGlyphLayoutDin91379(boolean useActualText, String sActualText) throws IOException, URISyntaxException { - GlyphLayoutProcessorFop glyphLayoutProcessor = new GlyphLayoutProcessorFop(); + AbstractGlyphLayoutProcessor.GlyphLayoutProcessorOptions options = new AbstractGlyphLayoutProcessor.GlyphLayoutProcessorOptions(); + if (useActualText) { + options.useActualText(); + } + GlyphLayoutProcessorFop glyphLayoutProcessor = new GlyphLayoutProcessorFop(options); - String outputName = "GlyphLayoutDIN91379.pdf"; + String outputName = String.format("GlyphLayoutDIN91379%s.pdf", sActualText); String outputPDFFilename = "target/" + outputName; - String outputTextFilename = "target/GlyphLayoutDIN91379.txt"; + String outputTextFilename = String.format("target/GlyphLayoutDIN91379%s.txt", sActualText); + float fontSize = 12.0f; try (PDDocument doc = new PDDocument()) @@ -132,7 +164,7 @@ void testGlyphLayoutDin91379() throws IOException, URISyntaxException os.write (0xBB); os.write (0xBF); - try (Writer writer = new BufferedWriter(new OutputStreamWriter(os, "utf-8"))) + try (Writer writer = new BufferedWriter(new OutputStreamWriter(os, StandardCharsets.UTF_8))) { //TODO compare this output with the input, like in TextStripper test // Not yet correct as of 4.7.2026 @@ -157,7 +189,7 @@ private void showComposites(PDPageContentStream cs, PDType0Font font, float font if (!line.isEmpty()) { showCompositesLine(cs, font, fontSize, x, y, line); - y -= fontSize * 1.5; + y -= fontSize * 1.5f; } } } diff --git a/pdfbox-layout-fop/src/test/resources/pdf/GlyphLayoutDIN91379_ActualText.pdf b/pdfbox-layout-fop/src/test/resources/pdf/GlyphLayoutDIN91379_ActualText.pdf new file mode 100644 index 00000000000..b27c4764110 Binary files /dev/null and b/pdfbox-layout-fop/src/test/resources/pdf/GlyphLayoutDIN91379_ActualText.pdf differ diff --git a/pdfbox/src/main/java/org/apache/pdfbox/cos/COSName.java b/pdfbox/src/main/java/org/apache/pdfbox/cos/COSName.java index a0958699393..844e51bad4d 100644 --- a/pdfbox/src/main/java/org/apache/pdfbox/cos/COSName.java +++ b/pdfbox/src/main/java/org/apache/pdfbox/cos/COSName.java @@ -559,6 +559,7 @@ public final class COSName extends COSBase implements Comparable public static final COSName SORT = getPDFName("Sort"); public static final COSName SOUND = getPDFName("Sound"); public static final COSName SPLIT = getPDFName("Split"); + public static final COSName SPAN = getPDFName("Span"); public static final COSName SS = getPDFName("SS"); public static final COSName ST = getPDFName("St"); public static final COSName STANDARD_ENCODING = getPDFName("StandardEncoding"); diff --git a/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/AbstractGlyphLayoutProcessor.java b/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/AbstractGlyphLayoutProcessor.java index 52dd42120d1..67cde9eef31 100644 --- a/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/AbstractGlyphLayoutProcessor.java +++ b/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/AbstractGlyphLayoutProcessor.java @@ -17,6 +17,10 @@ package org.apache.pdfbox.pdmodel; +import org.apache.pdfbox.cos.COSDictionary; +import org.apache.pdfbox.cos.COSName; +import org.apache.pdfbox.cos.COSString; +import org.apache.pdfbox.pdmodel.documentinterchange.markedcontent.PDPropertyList; import org.apache.pdfbox.pdmodel.font.PDType0Font; import java.io.IOException; @@ -33,6 +37,35 @@ */ public abstract class AbstractGlyphLayoutProcessor implements GlyphLayoutProcessorInterface { + /** + * + */ + private final boolean useActualText; + + /** + * Options for GlyphLayoutProcessor + */ + public static class GlyphLayoutProcessorOptions { + private boolean useActualText; + + /** + * Turn usage of ActualText on + * @return this + */ + public GlyphLayoutProcessorOptions useActualText() { + useActualText = true; + return this; + } + + /** + * Returns the state of useActualText + * @return true, if ActualText is used, falls otherwise + */ + public boolean getUseActualText() { + return useActualText; + } + } + /** * Class for text and Bidi-Level */ @@ -58,6 +91,22 @@ public int getBidiLevel() } } + /** + * Creates an AbstractGlyphLayoutProcessor with the given options + * @param options Options for creation + */ + public AbstractGlyphLayoutProcessor(GlyphLayoutProcessorOptions options) { + useActualText = options.useActualText; + } + + /** + * Creates an AbstractGlyphLayoutProcessor with standard options + */ + public AbstractGlyphLayoutProcessor() { + useActualText = false; + } + + /** * Compute the string width for a unidirectional string * @param font to be used @@ -88,6 +137,30 @@ public float getStringWidth(PDType0Font font, float fontSize, String text) throw return width; } + + /** + * Begin a marked content sequence for ActualText + * + * @param contentStream the content stream + * @param text the text to be written as ActualText + * @throws IOException If the content stream could not be written + */ + protected void beginMarkedContentForActualText(ContentStreamForGlyphLayoutInterface contentStream, String text) throws IOException { + COSDictionary dict = new COSDictionary(); + dict.setItem(COSName.ACTUAL_TEXT, new COSString(text)); + PDPropertyList propertyList = PDPropertyList.create(dict); + contentStream.beginMarkedContent(COSName.SPAN, propertyList); + } + + /** + * End a marked content sequence. + * + * @throws IOException If the content stream could not be written + */ + protected void endMarkedContent(ContentStreamForGlyphLayoutInterface contentStream) throws IOException { + contentStream.endMarkedContent(); + } + /** * Shows unidirectional text using glyph positioning (if needed) * @@ -117,11 +190,17 @@ protected abstract void showTextUni(ContentStreamForGlyphLayoutInterface content public void showText(ContentStreamForGlyphLayoutInterface contentStream, PDType0Font font, float fontSize, String text) throws IOException { + if (useActualText) { + beginMarkedContentForActualText(contentStream, text); + } List textAndBidiLevels = doBidiSplittingAndReordering(text); for (TextAndBidiLevel textAndBidiLevel: textAndBidiLevels) { showTextUni(contentStream, font, fontSize, textAndBidiLevel.getText(), textAndBidiLevel.getBidiLevel()); } + if (useActualText) { + endMarkedContent(contentStream); + } } /** diff --git a/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/ContentStreamForGlyphLayoutInterface.java b/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/ContentStreamForGlyphLayoutInterface.java index 292ea4a689c..d0766c3c196 100644 --- a/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/ContentStreamForGlyphLayoutInterface.java +++ b/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/ContentStreamForGlyphLayoutInterface.java @@ -16,6 +16,9 @@ */ package org.apache.pdfbox.pdmodel; +import org.apache.pdfbox.cos.COSName; +import org.apache.pdfbox.pdmodel.documentinterchange.markedcontent.PDPropertyList; + import java.io.IOException; public interface ContentStreamForGlyphLayoutInterface @@ -46,4 +49,21 @@ public interface ContentStreamForGlyphLayoutInterface * @throws IOException If the content stream could not be written. */ void setTextRise(float rise) throws IOException; + + + /** + * Begin a marked content sequence with a reference to an entry in the page resources' Properties dictionary. + * + * @param tag the tag to be added to the content stream + * @param propertyList property list to be added to the content stream + * @throws IOException If the content stream could not be written + */ + void beginMarkedContent(COSName tag, PDPropertyList propertyList) throws IOException; + + /** + * End a marked content sequence. + * + * @throws IOException If the content stream could not be written + */ + void endMarkedContent() throws IOException; }