Author: tilman
Date: Mon Jul 20 08:02:46 2026
New Revision: 1936347

Log:
PDFBOX-4951: add glyph layout, by Volker Kunert

Added:
   
pdfbox/branches/3.0/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/ContentStreamForGlyphLayoutInterface.java
      - copied unchanged from r1936107, 
pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/ContentStreamForGlyphLayoutInterface.java
   
pdfbox/branches/3.0/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/GlyphLayoutProcessorInterface.java
      - copied unchanged from r1936107, 
pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/GlyphLayoutProcessorInterface.java
   
pdfbox/branches/3.0/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/GlyphsAndPositions.java
      - copied unchanged from r1936107, 
pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/GlyphsAndPositions.java
Modified:
   
pdfbox/branches/3.0/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/PDAbstractContentStream.java
   
pdfbox/branches/3.0/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/interactive/form/AppearanceGeneratorHelper.java
   
pdfbox/branches/3.0/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/interactive/form/PDAcroForm.java

Copied: 
pdfbox/branches/3.0/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/ContentStreamForGlyphLayoutInterface.java
 (from r1936107, 
pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/ContentStreamForGlyphLayoutInterface.java)
==============================================================================
--- /dev/null   00:00:00 1970   (empty, because file is newly added)
+++ 
pdfbox/branches/3.0/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/ContentStreamForGlyphLayoutInterface.java
        Mon Jul 20 08:02:46 2026        (r1936347, copy of r1936107, 
pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/ContentStreamForGlyphLayoutInterface.java)
@@ -0,0 +1,49 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.pdfbox.pdmodel;
+
+import java.io.IOException;
+
+public interface ContentStreamForGlyphLayoutInterface
+{
+
+    /**
+     * Show the given glyphs at the specified positions
+     *
+     * @param glyphsAndPositions List of glyphs and positions
+     * @throws IOException if an IO error occurs
+     */
+    void showGlyphsWithPositioning(GlyphsAndPositions glyphsAndPositions) 
throws IOException;
+
+    /**
+     * Shows the glyphs for the given glyph codes
+     *
+     * @param glyphCodes Array of glyph codes of the content font
+     * @throws IOException if an I/O exception occurs
+     */
+    void showGlyphCodes(int[] glyphCodes) throws IOException;
+
+    /**
+     * Set the text rise value, i.e. move the baseline up or down. This is 
useful for drawing
+     * superscripts or subscripts.
+     *
+     * @param rise Specifies the distance, in unscaled text space units, to 
move the baseline up or
+     * down from its default location. 0 restores the default location.
+     * @throws IOException If the content stream could not be written.
+     */
+    void setTextRise(float rise) throws IOException;
+}

Copied: 
pdfbox/branches/3.0/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/GlyphLayoutProcessorInterface.java
 (from r1936107, 
pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/GlyphLayoutProcessorInterface.java)
==============================================================================
--- /dev/null   00:00:00 1970   (empty, because file is newly added)
+++ 
pdfbox/branches/3.0/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/GlyphLayoutProcessorInterface.java
       Mon Jul 20 08:02:46 2026        (r1936347, copy of r1936107, 
pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/GlyphLayoutProcessorInterface.java)
@@ -0,0 +1,52 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.pdfbox.pdmodel;
+
+import org.apache.pdfbox.pdmodel.font.PDFont;
+import org.apache.pdfbox.pdmodel.font.PDType0Font;
+
+import java.io.IOException;
+
+/**
+ * Interface for glyph layout that is independent of a specific implementation 
so that more
+ * implementations can be tried in the future.
+ *
+ * @author Volker Kunert
+ */
+public interface GlyphLayoutProcessorInterface
+{
+
+    /**
+     * Checks if the font is supported
+     *
+     * @param font to be checked
+     * @return true if glyph layout is supported for this font and this font 
is a PDType0Font
+     */
+    boolean supportsFont(PDFont font);
+
+    /**
+     * Shows a text using glyph positioning (if needed)
+     *
+     * @param contentStream the content stream
+     * @param font to be used
+     * @param fontSize font size
+     * @param text text to show
+     * @throws IOException if an I/O exception occurs
+     * @throws IllegalArgumentException if glyphs are missing
+     */
+    void showText(ContentStreamForGlyphLayoutInterface contentStream, 
PDType0Font font, float fontSize, String text) throws IOException;
+}

Copied: 
pdfbox/branches/3.0/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/GlyphsAndPositions.java
 (from r1936107, 
pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/GlyphsAndPositions.java)
==============================================================================
--- /dev/null   00:00:00 1970   (empty, because file is newly added)
+++ 
pdfbox/branches/3.0/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/GlyphsAndPositions.java
  Mon Jul 20 08:02:46 2026        (r1936347, copy of r1936107, 
pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/GlyphsAndPositions.java)
@@ -0,0 +1,98 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.pdfbox.pdmodel;
+
+import java.util.ArrayList;
+import java.util.Collections;
+
+/**
+ * Stores sublists of glyphs and positions in a list
+ *
+ * @author Volker Kunert
+ */
+public class GlyphsAndPositions
+{
+
+    private final ArrayList<Object> list = new ArrayList<>();
+
+    /**
+     * Sublist to store adjacent glyphs
+     */
+    public static class GlyphSubList extends ArrayList<Integer>
+    {
+    }
+
+    /**
+     * Adds a glyph
+     *
+     * @param glyph to be added
+     */
+    public void add(Integer glyph)
+    {
+        Object last = !list.isEmpty() ? list.get(list.size() - 1) : null;
+        GlyphSubList glyphSubList;
+        if (!(last instanceof GlyphSubList))
+        {
+            glyphSubList = new GlyphSubList();
+            list.add(glyphSubList);
+        }
+        else
+        {
+            glyphSubList = (GlyphSubList) last;
+        }
+        glyphSubList.add(glyph);
+    }
+
+    /**
+     * Add a position
+     *
+     * @param position to be added
+     */
+    public void add(Float position)
+    {
+        list.add(position);
+    }
+
+    /**
+     * Checks if the list is empty
+     *
+     * @return true if it is empty
+     */
+    public boolean isEmpty()
+    {
+        return list.isEmpty();
+    }
+
+    /**
+     * Clears the list
+     */
+    public void clear()
+    {
+        list.clear();
+    }
+
+    /**
+     * Converts GlyphsAndPositions to an array of objects (GlyphSubList and 
Float)
+     *
+     * @return the array
+     */
+    public Object[] toArray()
+    {
+        return Collections.unmodifiableList(list).toArray();
+    }
+}

Modified: 
pdfbox/branches/3.0/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/PDAbstractContentStream.java
==============================================================================
--- 
pdfbox/branches/3.0/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/PDAbstractContentStream.java
     Mon Jul 20 07:30:14 2026        (r1936346)
+++ 
pdfbox/branches/3.0/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/PDAbstractContentStream.java
     Mon Jul 20 08:02:46 2026        (r1936347)
@@ -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 Log LOG = 
LogFactory.getLog(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> gsubWorkerMap = 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())
         {
@@ -234,6 +256,16 @@ abstract class PDAbstractContentStream i
      */
     public void showTextWithPositioning(Object[] textWithPositioningArray) 
throws IOException
     {
+        if (!inTextMode)
+        {
+            throw new IllegalStateException("Must call beginText() before 
showTextWithPositioning()");
+        }
+
+        if (fontStack.isEmpty())
+        {
+            throw new IllegalStateException("Must call setFont() before 
showTextWithPositioning()");
+        }
+
         write("[");
         for (Object obj : textWithPositioningArray)
         {
@@ -255,6 +287,49 @@ abstract class PDAbstractContentStream i
     }
 
     /**
+     * Show the given glyphs at the specified positions. This method is meant 
to be called from
+     * within a GlyphLayoutProcessorInterface implementation and only for 
PDType0Font.
+     *
+     * @param glyphsAndPositions List of glyphs and positions
+     * @throws IOException if an IO error occurs
+     * @throws IllegalStateException if the current font isn't a PDType0Font.
+     */
+    @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.
@@ -263,30 +338,104 @@ 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
+     * @throws IllegalStateException if the current font isn't a PDType0Font.
+     */
+    @Override
+    public void showGlyphCodes(int[] glyphCodes) throws IOException
+    {
+        writeTextPDType0Font(glyphCodes);
         write(" ");
         writeOperator(OperatorName.SHOW_TEXT);
     }
 
     /**
-     * Outputs a string using the correct encoding and subsetting as required.
+     * Outputs the given glyph codes - only for PDType0Font
      *
-     * @param text The Unicode text to show.
-     * 
-     * @throws IOException If an io exception occurs.
+     * @param glyphCodes The glyph codes to write
+     *
+     * @throws IOException in case of I/O error
+     * @throws IllegalStateException if the current font isn't a PDType0Font.
      */
-    protected void showTextInternal(String text) throws IOException
+    protected void writeTextPDType0Font(int[] glyphCodes) throws IOException
     {
         if (!inTextMode)
         {
-            throw new IllegalStateException("Must call beginText() before 
showText()");
+            throw new IllegalStateException("Must call beginText() before 
writeTextPDType0Font()");
         }
-
         if (fontStack.isEmpty())
         {
-            throw new IllegalStateException("Must call setFont() before 
showText()");
+            throw new IllegalStateException("Must call setFont() before 
writeTextPDType0Font()");
+        }
+        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.
+     * 
+     * @throws IOException If an io exception occurs.
+     */
+    protected void showTextInternal(String text) throws IOException
+    {
         PDFont font = fontStack.peek();
 
         // complex text layout

Modified: 
pdfbox/branches/3.0/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/interactive/form/AppearanceGeneratorHelper.java
==============================================================================
--- 
pdfbox/branches/3.0/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/interactive/form/AppearanceGeneratorHelper.java
  Mon Jul 20 07:30:14 2026        (r1936346)
+++ 
pdfbox/branches/3.0/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/interactive/form/AppearanceGeneratorHelper.java
  Mon Jul 20 08:02:46 2026        (r1936347)
@@ -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;
@@ -487,6 +488,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/branches/3.0/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/interactive/form/PDAcroForm.java
==============================================================================
--- 
pdfbox/branches/3.0/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/interactive/form/PDAcroForm.java
 Mon Jul 20 07:30:14 2026        (r1936346)
+++ 
pdfbox/branches/3.0/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/interactive/form/PDAcroForm.java
 Mon Jul 20 08:02:46 2026        (r1936347)
@@ -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.
@@ -280,6 +303,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