Author: tilman
Date: Fri Jul  3 14:19:26 2026
New Revision: 1935842

Log:
PDFBOX-4951: use glyph layout processor, by Volker Kunert

Modified:
   
pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/PDAbstractContentStream.java
   
pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/interactive/form/AppearanceGeneratorHelper.java
   
pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/interactive/form/PDAcroForm.java

Modified: 
pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/PDAbstractContentStream.java
==============================================================================
--- 
pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/PDAbstractContentStream.java
    Fri Jul  3 12:45:25 2026        (r1935841)
+++ 
pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/PDAbstractContentStream.java
    Fri Jul  3 14:19:26 2026        (r1935842)
@@ -74,7 +74,7 @@ import org.apache.pdfbox.util.StringUtil
  *
  * @author Ben Litchfield
  */
-abstract class PDAbstractContentStream implements Closeable
+abstract class PDAbstractContentStream implements 
ContentStreamForGlyphLayoutInterface, Closeable
 {
     private static final Logger LOG = 
LogManager.getLogger(PDAbstractContentStream.class);
 
@@ -85,6 +85,7 @@ abstract class PDAbstractContentStream i
 
     protected boolean inTextMode = false;
     protected final Deque<PDFont> fontStack = new ArrayDeque<>();
+    protected final Deque<Float> fontSizeStack = new ArrayDeque<>();
 
     protected final Deque<PDColorSpace> nonStrokingColorSpaceStack = new 
ArrayDeque<>();
     protected final Deque<PDColorSpace> strokingColorSpaceStack = new 
ArrayDeque<>();
@@ -95,6 +96,7 @@ abstract class PDAbstractContentStream i
 
     private final Map<PDType0Font, GsubWorker> gsubWorkers = new HashMap<>();
     private final GsubWorkerFactory gsubWorkerFactory = new 
GsubWorkerFactory();
+    private GlyphLayoutProcessorInterface glyphLayoutProcessor;
 
     /**
      * Create a new appearance stream.
@@ -114,6 +116,16 @@ abstract class PDAbstractContentStream i
     }
 
     /**
+     * Sets the glyph layout processor
+     *
+     * @param glyphLayoutProcessor glyph layout processor
+     */
+    public void setGlyphLayoutProcessor(GlyphLayoutProcessorInterface 
glyphLayoutProcessor)
+    {
+        this.glyphLayoutProcessor = glyphLayoutProcessor;
+    }
+
+    /**
      * Sets the maximum number of digits allowed for fractional numbers.
      * 
      * @see NumberFormat#setMaximumFractionDigits(int)
@@ -177,6 +189,16 @@ abstract class PDAbstractContentStream i
             fontStack.push(font);
         }
 
+        if (fontSizeStack.isEmpty())
+        {
+            fontSizeStack.add(fontSize);
+        }
+        else
+        {
+            fontSizeStack.pop();
+            fontSizeStack.push(fontSize);
+        }
+
         // keep track of fonts which are configured for subsetting
         if (font.willBeSubset())
         {
@@ -253,6 +275,47 @@ abstract class PDAbstractContentStream i
     }
 
     /**
+     * Show the given glyphs at the specified positions
+     *
+     * @param glyphsAndPositions List of glyphs and positions
+     * @throws IOException if an IO error occurs
+     */
+    @Override
+    public void showGlyphsWithPositioning(GlyphsAndPositions 
glyphsAndPositions) throws IOException
+    {
+        write("[");
+
+        for (Object obj : glyphsAndPositions.toArray())
+        {
+            if (obj instanceof GlyphsAndPositions.GlyphSubList)
+            {
+                GlyphsAndPositions.GlyphSubList glyphSubList = 
(GlyphsAndPositions.GlyphSubList) obj;
+                int[] intGlyphArray = new int[glyphSubList.size()];
+                // Convert Type to int[]
+                for (int i = 0; i < intGlyphArray.length; i++)
+                {
+                    intGlyphArray[i] = glyphSubList.get(i);
+                }
+                writeTextPDType0Font(intGlyphArray);
+            }
+            else if (obj instanceof Float)
+            {
+                writeOperand((Float) obj);
+            }
+            else
+            {
+                if (obj == null)
+                {
+                    throw new NullPointerException("Argument contains null 
entry");
+                }
+                throw new IllegalArgumentException("Argument must consist of 
array of Float and GlyphsAndPositions.GlyphSubList types, not " + 
obj.getClass().getName());
+            }
+        }
+        write("] ");
+        writeOperator(OperatorName.SHOW_TEXT_ADJUSTED);
+    }
+
+    /**
      * Shows the given text at the location specified by the current text 
matrix.
      *
      * @param text The Unicode text to show.
@@ -261,12 +324,95 @@ abstract class PDAbstractContentStream i
      */
     public void showText(String text) throws IOException
     {
-        showTextInternal(text);
+        if (!inTextMode)
+        {
+            throw new IllegalStateException("Must call beginText() before 
showText()");
+        }
+        if (fontStack.isEmpty())
+        {
+            throw new IllegalStateException("Must call setFont() before 
showText()");
+        }
+        if (fontSizeStack.isEmpty())
+        {
+            throw new IllegalStateException("Font is set, but fontSize is not 
set");
+        }
+        PDFont font = fontStack.peek();
+        if (glyphLayoutProcessor != null && 
glyphLayoutProcessor.supportsFont(font))
+        {
+            float fontSize = fontSizeStack.peek();
+            glyphLayoutProcessor.showText(this, (PDType0Font) font, fontSize, 
text);
+        }
+        else
+        {
+            showTextInternal(text);
+            write(" ");
+            writeOperator(OperatorName.SHOW_TEXT);
+        }
+    }
+
+    /**
+     * Shows the glyphs for the given glyph codes - only for PDType0Font
+     *
+     * @param glyphCodes Array of glyph codes of the content font
+     * @throws IOException if an I/O exception occurs
+     */
+    @Override
+    public void showGlyphCodes(int[] glyphCodes) throws IOException
+    {
+        writeTextPDType0Font(glyphCodes);
         write(" ");
         writeOperator(OperatorName.SHOW_TEXT);
     }
 
     /**
+     * Outputs the given glyph codes - only for PDType0Font
+     *
+     * @param glyphCodes The glyph codes to write
+     *
+     * @throws IOException in case of I/O error
+     */
+    protected void writeTextPDType0Font(int[] glyphCodes) throws IOException
+    {
+        if (!inTextMode)
+        {
+            throw new IllegalStateException("Must call beginText() before 
showText()");
+        }
+        if (fontStack.isEmpty())
+        {
+            throw new IllegalStateException("Must call setFont() before 
showText()");
+        }
+        PDFont font = fontStack.peek();
+        if (!(font instanceof PDType0Font))
+        {
+            throw new IllegalStateException("Must be called with current font 
instance of PDType0Font");
+
+        }
+        PDType0Font pdType0Font = (PDType0Font) font;
+
+        // encode glyphs, update set of used glyphs
+        ByteArrayOutputStream out = new ByteArrayOutputStream();
+        Set<Integer> glyphIds = new HashSet<>();
+
+        for (int glyphCode : glyphCodes)
+        {
+            out.write(pdType0Font.encodeGlyphId(glyphCode));
+            if (glyphCode < 0xFFFF)
+            {
+                glyphIds.add(glyphCode);
+            }
+        }
+        byte[] encodedText = out.toByteArray();
+
+        // add glyphs to subset
+        if (pdType0Font.willBeSubset())
+        {
+            pdType0Font.addGlyphsToSubset(glyphIds);
+        }
+        // write encoded text and the PDF operator
+        COSWriter.writeString(encodedText, outputStream);
+    }
+
+    /**
      * Outputs a string using the correct encoding and subsetting as required.
      *
      * @param text The Unicode text to show.
@@ -1755,4 +1901,4 @@ abstract class PDAbstractContentStream i
 
         return glyphIdsAfterGsub;
     }
-}
+}
\ No newline at end of file

Modified: 
pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/interactive/form/AppearanceGeneratorHelper.java
==============================================================================
--- 
pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/interactive/form/AppearanceGeneratorHelper.java
 Fri Jul  3 12:45:25 2026        (r1935841)
+++ 
pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/interactive/form/AppearanceGeneratorHelper.java
 Fri Jul  3 14:19:26 2026        (r1935842)
@@ -34,6 +34,7 @@ import org.apache.pdfbox.cos.COSName;
 import org.apache.pdfbox.cos.COSString;
 import org.apache.pdfbox.pdfparser.PDFStreamParser;
 import org.apache.pdfbox.pdfwriter.ContentStreamWriter;
+import org.apache.pdfbox.pdmodel.GlyphLayoutProcessorInterface;
 import org.apache.pdfbox.pdmodel.PDResources;
 import org.apache.pdfbox.pdmodel.common.PDRectangle;
 import org.apache.pdfbox.pdmodel.font.PDFont;
@@ -494,6 +495,11 @@ class AppearanceGeneratorHelper
     {
         try (PDAppearanceContentStream contents = new 
PDAppearanceContentStream(appearanceStream, output))
         {
+            GlyphLayoutProcessorInterface glyphLayoutProcessor = 
field.getAcroForm().getGlyphLayoutProcessor();
+            if (glyphLayoutProcessor != null)
+            {
+                contents.setGlyphLayoutProcessor(glyphLayoutProcessor);
+            }
             PDRectangle bbox = resolveBoundingBox(widget, appearanceStream);
             
             // Acrobat calculates the left and right padding dependent on the 
offset of the border edge

Modified: 
pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/interactive/form/PDAcroForm.java
==============================================================================
--- 
pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/interactive/form/PDAcroForm.java
        Fri Jul  3 12:45:25 2026        (r1935841)
+++ 
pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/interactive/form/PDAcroForm.java
        Fri Jul  3 14:19:26 2026        (r1935842)
@@ -37,6 +37,7 @@ import org.apache.pdfbox.cos.COSArray;
 import org.apache.pdfbox.cos.COSBase;
 import org.apache.pdfbox.cos.COSDictionary;
 import org.apache.pdfbox.cos.COSName;
+import org.apache.pdfbox.pdmodel.GlyphLayoutProcessorInterface;
 import org.apache.pdfbox.pdmodel.PDDocument;
 import org.apache.pdfbox.pdmodel.PDPage;
 import org.apache.pdfbox.pdmodel.PDPageContentStream;
@@ -78,6 +79,8 @@ public final class PDAcroForm implements
 
     private final Map<COSName, SoftReference<PDFont>> directFontCache = new 
HashMap<>();
 
+    private GlyphLayoutProcessorInterface glyphLayoutProcessor;
+
     /**
      * Constructor.
      *
@@ -103,6 +106,26 @@ public final class PDAcroForm implements
     }
 
     /**
+     * Sets the glyph layout processor
+     *
+     * @param glyphLayoutProcessor glyph layout processor
+     */
+    public void setGlyphLayoutProcessor(GlyphLayoutProcessorInterface 
glyphLayoutProcessor)
+    {
+        this.glyphLayoutProcessor = glyphLayoutProcessor;
+    }
+
+    /**
+     * Returns the glyph layout processor or null
+     *
+     * @return the glyph layout processor or null
+     */
+    public GlyphLayoutProcessorInterface getGlyphLayoutProcessor()
+    {
+        return glyphLayoutProcessor;
+    }
+
+    /**
      * This will get the document associated with this form.
      *
      * @return The PDF document.
@@ -289,6 +312,10 @@ public final class PDAcroForm implements
                     try (PDPageContentStream contentStream = new 
PDPageContentStream(
                             document, page, AppendMode.APPEND, true, 
!isContentStreamWrapped))
                     {
+                        if (glyphLayoutProcessor != null)
+                        {
+                            
contentStream.setGlyphLayoutProcessor(glyphLayoutProcessor);
+                        }
                         isContentStreamWrapped = true;
 
                         PDAppearanceStream appearanceStream = 
annotation.getNormalAppearanceStream();

Reply via email to