Author: tilman
Date: Mon Aug 10 11:01:35 2026
New Revision: 1937028

Log:
PDFBOX-4951: Refactoring, separate Bidi from showText, introduce 
glyphLayoutProcessor.getStringWidth, by Volker Kunert; closes #493

Added:
   
pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/AbstractGlyphLayoutProcessor.java
   (contents, props changed)
Modified:
   
pdfbox/trunk/pdfbox-layout-awt/src/main/java/org/apache/pdfbox/glyphlayout/awt/GlyphLayoutProcessorAwt.java
   
pdfbox/trunk/pdfbox-layout-awt/src/test/java/org/apache/pdfbox/glyphlayout/awt/GlyphLayoutDin91379Test.java
   
pdfbox/trunk/pdfbox-layout-awt/src/test/java/org/apache/pdfbox/glyphlayout/awt/GlyphLayoutLigaturesAndKerningTest.java
   
pdfbox/trunk/pdfbox-layout-awt/src/test/java/org/apache/pdfbox/glyphlayout/awt/GlyphLayoutSMPTest.java
   
pdfbox/trunk/pdfbox-layout-fop/src/main/java/org/apache/pdfbox/glyphlayout/fop/GlyphLayoutProcessorFop.java
   
pdfbox/trunk/pdfbox-layout-fop/src/test/java/org/apache/pdfbox/glyphlayout/fop/GlyphLayoutDin91379Test.java
   
pdfbox/trunk/pdfbox-layout-fop/src/test/java/org/apache/pdfbox/glyphlayout/fop/GlyphLayoutLigaturesAndKerningTest.java
   
pdfbox/trunk/pdfbox-layout-fop/src/test/java/org/apache/pdfbox/glyphlayout/fop/GlyphLayoutSMPTest.java
   
pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/GlyphLayoutProcessorInterface.java

Modified: 
pdfbox/trunk/pdfbox-layout-awt/src/main/java/org/apache/pdfbox/glyphlayout/awt/GlyphLayoutProcessorAwt.java
==============================================================================
--- 
pdfbox/trunk/pdfbox-layout-awt/src/main/java/org/apache/pdfbox/glyphlayout/awt/GlyphLayoutProcessorAwt.java
 Mon Aug 10 11:01:27 2026        (r1937027)
+++ 
pdfbox/trunk/pdfbox-layout-awt/src/main/java/org/apache/pdfbox/glyphlayout/awt/GlyphLayoutProcessorAwt.java
 Mon Aug 10 11:01:35 2026        (r1937028)
@@ -14,6 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+
 package org.apache.pdfbox.glyphlayout.awt;
 
 
@@ -23,11 +24,12 @@ import java.awt.font.FontRenderContext;
 import java.awt.font.GlyphVector;
 import java.awt.geom.AffineTransform;
 import java.awt.geom.Point2D;
+import java.awt.geom.Rectangle2D;
 import java.io.IOException;
 import java.io.InputStream;
-import java.text.Bidi;
 import java.util.Objects;
 
+import org.apache.pdfbox.pdmodel.AbstractGlyphLayoutProcessor;
 import org.apache.pdfbox.pdmodel.ContentStreamForGlyphLayoutInterface;
 import org.apache.pdfbox.pdmodel.GlyphLayoutProcessorInterface;
 import org.apache.pdfbox.pdmodel.GlyphsAndPositions;
@@ -43,7 +45,7 @@ import org.apache.pdfbox.pdmodel.font.PD
  *
  * @author Volker Kunert
  */
-public class GlyphLayoutProcessorAwt implements GlyphLayoutProcessorInterface
+public class GlyphLayoutProcessorAwt extends AbstractGlyphLayoutProcessor 
implements GlyphLayoutProcessorInterface
 {
 
     private final GlyphLayoutFontLoaderAwt glyphLayoutFontLoaderAwt;
@@ -132,7 +134,7 @@ public class GlyphLayoutProcessorAwt imp
      *
      * @param pdDocument document
      * @param inputStream of the font
-     * @param fontOptions
+     * @param fontOptions options for font loading
      *
      * @return a PDType0Font font.
      *
@@ -154,7 +156,7 @@ public class GlyphLayoutProcessorAwt imp
      * @param fontOptions options for font
      *
      * @return a PDType0Font font.
-     * 
+     *
      * @throws IOException if font can not be loaded
      * @throws FontFormatException if the font is bad
      */
@@ -194,60 +196,19 @@ public class GlyphLayoutProcessorAwt imp
     }
 
     /**
-     * Shows a text using glyph positioning (if needed)
-     *
-     * @param contentStream the content stream
+     * Compute the string width for a unidirectional string
      * @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
+     * @param text text
+     * @param bidiLevel Bidi Level
+     * @return string width
      */
     @Override
-    public void showText(ContentStreamForGlyphLayoutInterface contentStream, 
PDType0Font font, float fontSize, String text) throws IOException
+    protected float getStringWidthUni(PDType0Font font, float fontSize, String 
text, int bidiLevel)
     {
-        Objects.requireNonNull(text, "Text must be set");
-
-        if (Bidi.requiresBidi(text.toCharArray(), 0, text.length()))
-        {
-            Bidi bidi = new Bidi(text, Bidi.DIRECTION_DEFAULT_LEFT_TO_RIGHT);
-            if (bidi.isMixed())
-            {
-                // Split and Reorder
-                // See PDFTextStripper.handleDirection
-                // collect individual bidi information
-                int runCount = bidi.getRunCount();
-                byte[] levels = new byte[runCount];
-                Integer[] runs = new Integer[runCount];
-
-                for (int i = 0; i < runCount; i++)
-                {
-                    levels[i] = (byte) bidi.getRunLevel(i);
-                    runs[i] = i;
-                }
-                // reorder individual parts based on their levels
-                Bidi.reorderVisually(levels, 0, runs, 0, runCount);
-
-                for (int i = 0; i < runCount; i++)
-                {
-                    int index = runs[i];
-                    int start = bidi.getRunStart(index);
-                    int limit = bidi.getRunLimit(index);
-                    int bidiLevel = levels[index];
-                    String part = text.substring(start, limit);
-                    showTextUni(contentStream, font, fontSize, part, 
bidiLevel);
-                }
-            }
-            else
-            {
-                showTextUni(contentStream, font, fontSize, text, 
bidi.getBaseLevel());
-            }
-        }
-        else
-        {
-            showTextUni(contentStream, font, fontSize, text, 
Bidi.DIRECTION_LEFT_TO_RIGHT);
-        }
+        GlyphVector glyphVector = computeGlyphVector(font, fontSize, text, 
bidiLevel);
+        Rectangle2D rect = glyphVector.getLogicalBounds();
+        return (float) rect.getWidth();
     }
 
     /**
@@ -261,13 +222,14 @@ public class GlyphLayoutProcessorAwt imp
      * @throws IOException if an IO-exception occurs
      * @throws IllegalArgumentException if glyphs are missing
      */
+    @Override
     protected void showTextUni(ContentStreamForGlyphLayoutInterface 
contentStream, PDType0Font font, float fontSize, String text, int bidiLevel) 
throws IOException
     {
         Objects.requireNonNull(text, "Text must be set");
         Objects.requireNonNull(contentStream, "contentStream must be set");
 
         GlyphVector glyphVector = computeGlyphVector(font, fontSize, text, 
bidiLevel);
-        
+
         // check for adjustment not needed:
         // glyphVector.getLayoutFlags() & FLAG_HAS_POSITION_ADJUSTMENTS is 
always true
         // because of horizontal adjustments in every string except one 
character string

Modified: 
pdfbox/trunk/pdfbox-layout-awt/src/test/java/org/apache/pdfbox/glyphlayout/awt/GlyphLayoutDin91379Test.java
==============================================================================
--- 
pdfbox/trunk/pdfbox-layout-awt/src/test/java/org/apache/pdfbox/glyphlayout/awt/GlyphLayoutDin91379Test.java
 Mon Aug 10 11:01:27 2026        (r1937027)
+++ 
pdfbox/trunk/pdfbox-layout-awt/src/test/java/org/apache/pdfbox/glyphlayout/awt/GlyphLayoutDin91379Test.java
 Mon Aug 10 11:01:35 2026        (r1937028)
@@ -150,7 +150,7 @@ class GlyphLayoutDin91379Test extends Te
             float x, float y, String s) throws IOException
     {
 
-        s = s.replaceAll("\t", "    ");
+        s = s.replace("\t", "    ");
         String[] lines = s.split("[\n]");
 
         for (String line : lines)

Modified: 
pdfbox/trunk/pdfbox-layout-awt/src/test/java/org/apache/pdfbox/glyphlayout/awt/GlyphLayoutLigaturesAndKerningTest.java
==============================================================================
--- 
pdfbox/trunk/pdfbox-layout-awt/src/test/java/org/apache/pdfbox/glyphlayout/awt/GlyphLayoutLigaturesAndKerningTest.java
      Mon Aug 10 11:01:27 2026        (r1937027)
+++ 
pdfbox/trunk/pdfbox-layout-awt/src/test/java/org/apache/pdfbox/glyphlayout/awt/GlyphLayoutLigaturesAndKerningTest.java
      Mon Aug 10 11:01:35 2026        (r1937028)
@@ -17,16 +17,6 @@
 
 package org.apache.pdfbox.glyphlayout.awt;
 
-/**
- * Examples for ligatures and kerning
- * See <a 
href="https://issues.apache.org/jira/browse/PDFBOX-4951";>PDFBOX-4951</a>
- *
- * The default processing of GlyphLayoutProcessor is with ligatures and 
kerning disabled.
- * You can enable ligatures and kerning using FontOptions, see below.
- *
- * @author Volker Kunert
- */
-
 import java.awt.FontFormatException;
 import java.io.IOException;
 import java.net.URISyntaxException;
@@ -40,6 +30,16 @@ import org.apache.pdfbox.pdmodel.PDPage;
 import org.apache.pdfbox.pdmodel.PDPageContentStream;
 import org.apache.pdfbox.pdmodel.font.PDType0Font;
 
+/**
+ * Examples for ligatures and kerning
+ * See <a 
href="https://issues.apache.org/jira/browse/PDFBOX-4951";>PDFBOX-4951</a>
+ *
+ * The default processing of GlyphLayoutProcessor is with ligatures and 
kerning disabled.
+ * You can enable ligatures and kerning using FontOptions, see below.
+ *
+ * @author Volker Kunert
+ * @author Tilman Hausherr
+ */
 class GlyphLayoutLigaturesAndKerningTest extends TestBase
 {
     static final String FIRACODE_STRING = "!= == === >= <=";
@@ -52,7 +52,7 @@ class GlyphLayoutLigaturesAndKerningTest
      * Check that missing glyph is caught like in main pdfbox.
      *
      * @throws IOException
-     * @throws FontFormatException 
+     * @throws FontFormatException
      */
     @Test
     void testMissingGlyph() throws IOException, FontFormatException
@@ -63,8 +63,8 @@ class GlyphLayoutLigaturesAndKerningTest
 
         try (PDDocument doc = new PDDocument())
         {
-            PDType0Font lohitBengaliFont = 
createPdType0Font(glyphLayoutProcessor, doc, lohitBengaliPath, 
-                            new GlyphLayoutFontLoaderAwt.FontOptions());
+            PDType0Font lohitBengaliFont = 
createPdType0Font(glyphLayoutProcessor, doc, lohitBengaliPath,
+                    new GlyphLayoutFontLoaderAwt.FontOptions());
 
             PDPage page = new PDPage();
             doc.addPage(page);
@@ -72,7 +72,7 @@ class GlyphLayoutLigaturesAndKerningTest
             {
                 cs.setGlyphLayoutProcessor(glyphLayoutProcessor);
 
-                IllegalArgumentException ex = 
assertThrows(IllegalArgumentException.class, () -> 
+                IllegalArgumentException ex = 
assertThrows(IllegalArgumentException.class, () ->
                         showComposites(cs, lohitBengaliFont, 1, 0, 0, 
"123ABC"));
                 assertEquals("Missing glyph in font 'Lohit Bengali' for the 
character 'A', codePoint: 65 (U+0041).", ex.getMessage());
 
@@ -80,7 +80,7 @@ class GlyphLayoutLigaturesAndKerningTest
             }
         }
     }
-  
+
     @Test
     void testLigaturesAndKerning() throws IOException, FontFormatException, 
URISyntaxException
     {
@@ -100,15 +100,15 @@ class GlyphLayoutLigaturesAndKerningTest
             PDType0Font firaFont = createPdType0Font(glyphLayoutProcessor, 
doc, firaPath);
             PDType0Font firaLigFont = createPdType0Font(glyphLayoutProcessor, 
doc, firaPath,
                     new 
GlyphLayoutFontLoaderAwt.FontOptions().setLigaturesOn());
-            
+
             PDType0Font dejavuFont = createPdType0Font(glyphLayoutProcessor, 
doc, dejavuPath);
-            
+
             PDType0Font dejavuLigFont = 
createPdType0Font(glyphLayoutProcessor, doc, dejavuPath,
                     new 
GlyphLayoutFontLoaderAwt.FontOptions().setLigaturesOn());
-            
+
             PDType0Font dejavuKernFont = 
createPdType0Font(glyphLayoutProcessor, doc, dejavuPath,
                     new GlyphLayoutFontLoaderAwt.FontOptions().setKerningOn());
-            
+
             PDType0Font dejavuLigKernFont = 
createPdType0Font(glyphLayoutProcessor, doc, dejavuPath,
                     new 
GlyphLayoutFontLoaderAwt.FontOptions().setLigaturesOn().setKerningOn());
 
@@ -123,7 +123,7 @@ class GlyphLayoutLigaturesAndKerningTest
             try (PDPageContentStream cs = new PDPageContentStream(doc, page))
             {
                 cs.setGlyphLayoutProcessor(glyphLayoutProcessor);
-                
+
                 float x = page.getBBox().getLowerLeftX() + fontSize;
                 float y = page.getBBox().getUpperRightY() - fontSize;
                 y = showComposites(cs, firaFont, fontSize, x, y, 
FIRACODE_STRING);
@@ -154,11 +154,11 @@ class GlyphLayoutLigaturesAndKerningTest
      * break the text into lines and show them
      */
     private float showComposites(PDPageContentStream cs, PDType0Font font, 
float fontSize,
-            float x, float y, String s) throws IOException
+                                 float x, float y, String s) throws IOException
     {
 
-        s = s.replaceAll("\t", "    ");
-        String[] lines = s.split("[\n]");
+        s = s.replace("\t", "    ");
+        String[] lines = s.split("[\\n]");
 
         float height = font.getBoundingBox().getHeight();
 

Modified: 
pdfbox/trunk/pdfbox-layout-awt/src/test/java/org/apache/pdfbox/glyphlayout/awt/GlyphLayoutSMPTest.java
==============================================================================
--- 
pdfbox/trunk/pdfbox-layout-awt/src/test/java/org/apache/pdfbox/glyphlayout/awt/GlyphLayoutSMPTest.java
      Mon Aug 10 11:01:27 2026        (r1937027)
+++ 
pdfbox/trunk/pdfbox-layout-awt/src/test/java/org/apache/pdfbox/glyphlayout/awt/GlyphLayoutSMPTest.java
      Mon Aug 10 11:01:35 2026        (r1937028)
@@ -65,8 +65,8 @@ class GlyphLayoutSMPTest extends TestBas
             float x, float y, String s) throws IOException
     {
 
-        s = s.replaceAll("\t", "    ");
-        String[] lines = s.split("[\n]");
+        s = s.replace("\t", "    ");
+        String[] lines = s.split("[\\n]");
 
         for (String line : lines)
         {

Modified: 
pdfbox/trunk/pdfbox-layout-fop/src/main/java/org/apache/pdfbox/glyphlayout/fop/GlyphLayoutProcessorFop.java
==============================================================================
--- 
pdfbox/trunk/pdfbox-layout-fop/src/main/java/org/apache/pdfbox/glyphlayout/fop/GlyphLayoutProcessorFop.java
 Mon Aug 10 11:01:27 2026        (r1937027)
+++ 
pdfbox/trunk/pdfbox-layout-fop/src/main/java/org/apache/pdfbox/glyphlayout/fop/GlyphLayoutProcessorFop.java
 Mon Aug 10 11:01:35 2026        (r1937028)
@@ -26,6 +26,7 @@ import org.apache.fop.fonts.GlyphMapping
 import org.apache.fop.fonts.MultiByteFont;
 import org.apache.fop.traits.MinOptMax;
 
+import org.apache.pdfbox.pdmodel.AbstractGlyphLayoutProcessor;
 import org.apache.pdfbox.pdmodel.ContentStreamForGlyphLayoutInterface;
 import org.apache.pdfbox.pdmodel.GlyphLayoutProcessorInterface;
 import org.apache.pdfbox.pdmodel.GlyphsAndPositions;
@@ -41,7 +42,7 @@ import org.apache.pdfbox.pdmodel.font.PD
  *
  * @author Volker Kunert
  */
-public class GlyphLayoutProcessorFop implements GlyphLayoutProcessorInterface
+public class GlyphLayoutProcessorFop extends AbstractGlyphLayoutProcessor 
implements GlyphLayoutProcessorInterface
 {
 
     private final GlyphLayoutFontLoaderFop glyphLayoutFontLoaderFop;
@@ -51,7 +52,7 @@ public class GlyphLayoutProcessorFop imp
         the font size must be multiplied by this factor. Otherwise, the
         positioning is wrong.
      */
-    private static final float FOP_FONTSIZE_FACTOR = 1e3f;
+    private static final float FOP_FONTSIZE_FACTOR = 1000f;
 
     /**
      * Constructs a GlyphLayoutProcessorFop
@@ -125,14 +126,66 @@ public class GlyphLayoutProcessorFop imp
     }
 
     /**
+     * Compute the string width for a unidirectional string
+     *
+     * @param font to be used
+     * @param fontSize font size
+     * @param text text
+     * @param bidiLevel Bidi Level
+     * @return string width
+     */
+    @Override
+    protected float getStringWidthUni(PDType0Font font, float fontSize, String 
text, int bidiLevel) throws IOException
+    {
+        TextAndGpa textAndGpa = computeGlyphsAndPositions(font, fontSize, 
text, bidiLevel);
+        // PDType0Font.getStringWidth returns glyph widths in 1000-units. 
Convert to user space using font matrix and fontSize
+        float raw = font.getStringWidth(textAndGpa.getText());
+        float scaleX = font.getFontMatrix().getScaleX();
+        return raw * scaleX * fontSize;
+    }
+
+    /**
+     * Internal class for text, positioning and GPA
+     */
+    protected static class TextAndGpa
+    {
+
+        private final String text;
+        private final boolean positioning;
+        private final int[][] gpa;
+
+        public TextAndGpa(String text, boolean positioning, int[][] gpa)
+        {
+            this.text = text;
+            this.positioning = positioning;
+            this.gpa = gpa;
+        }
+
+        public String getText()
+        {
+            return text;
+        }
+
+        public boolean hasPositioning()
+        {
+            return positioning;
+        }
+
+        public int[][] getGpa()
+        {
+            return gpa;
+        }
+    }
+
+    /**
      * Computes glyph positioning
      *
      * @param font to be used
      * @param fontSize font size
      * @param text text to show
-     * @param bidiLevel
+     * @param bidiLevel Bidi level
      */
-    protected GlyphMapping computeGlyphAndPositions(PDType0Font font, float 
fontSize, String text, int bidiLevel)
+    protected TextAndGpa computeGlyphsAndPositions(PDType0Font font, float 
fontSize, String text, int bidiLevel)
     {
         Objects.requireNonNull(font, "Font must be set");
         Objects.requireNonNull(text, "Text must be set");
@@ -146,11 +199,22 @@ public class GlyphLayoutProcessorFop imp
 
         org.apache.fop.fonts.Font fopFont = new Font(mbf.getFontName(), null, 
mbf, (int) (fontSize * FOP_FONTSIZE_FACTOR));
 
-        return GlyphMapping.doGlyphMapping(
+        GlyphMapping glyphMapping = GlyphMapping.doGlyphMapping(
                 new FopStringTextFragment(text), 0, text.length() - 1, fopFont,
                 letterSpaceIPD, letterSpaceAdjustArray, ' ',
                 ' ', //?
                 false, bidiLevel, false, true, false);
+
+        text = glyphMapping.mapping != null ? glyphMapping.mapping : text;
+        boolean positioning = glyphMapping.gposAdjustments != null;
+        int[][] gpa = positioning ? glyphMapping.gposAdjustments : 
createZeroGpa(text.length());
+
+        if (bidiLevel % 2 == Bidi.DIRECTION_RIGHT_TO_LEFT)
+        {
+            gpa = reverseGpa(gpa);
+            text = new StringBuilder(text).reverse().toString();
+        }
+        return new TextAndGpa(text, positioning, gpa);
     }
 
     /**
@@ -167,76 +231,18 @@ public class GlyphLayoutProcessorFop imp
     }
 
     /**
-     * Shows a text using glyph positioning (if needed)
+     * Shows unidirectional text using positioning
      *
      * @param contentStream the content stream
      * @param font to be used
      * @param fontSize font size
      * @param text text to show
+     * @param bidiLevel Bidi level*
+     *
      * @throws IOException if an I/O exception occurs
      * @throws IllegalArgumentException if glyphs are missing
      */
     @Override
-    public void showText(ContentStreamForGlyphLayoutInterface contentStream, 
PDType0Font font, float fontSize, String text) throws IOException
-    {
-        Objects.requireNonNull(text, "Text must be set");
-
-        if (Bidi.requiresBidi(text.toCharArray(), 0, text.length()))
-        {
-            Bidi bidi = new Bidi(text, Bidi.DIRECTION_DEFAULT_LEFT_TO_RIGHT);
-            if (bidi.isMixed())
-            {
-                // Split and Reorder
-                // See PDFTextStripper.handleDirection
-                // collect individual bidi information
-                int runCount = bidi.getRunCount();
-                byte[] levels = new byte[runCount];
-                Integer[] runs = new Integer[runCount];
-
-                for (int i = 0; i < runCount; i++)
-                {
-                    levels[i] = (byte) bidi.getRunLevel(i);
-                    runs[i] = i;
-                }
-                // reorder individual parts based on their levels
-                Bidi.reorderVisually(levels, 0, runs, 0, runCount);
-
-                for (int i = 0; i < runCount; i++)
-                {
-                    int index = runs[i];
-                    int start = bidi.getRunStart(index);
-                    int limit = bidi.getRunLimit(index);
-                    int bidiLevel = levels[index];
-
-                    // Map to correct value for GlyphMapping.doGlyphMapping
-                    // 0 LTR equal in java.text.Bidi and 
org.apache.fop.traits.Direction
-                    // 1 RTL equal in java.text.Bidi and 
org.apache.fop.traits.Direction
-                    bidiLevel = bidiLevel % 2; // even LTR, odd RTL
-                    String part = text.substring(start, limit);
-                    showTextUni(contentStream, font, fontSize, part, 
bidiLevel);
-                }
-            }
-            else
-            {
-                showTextUni(contentStream, font, fontSize, text, 
bidi.getBaseLevel());
-            }
-        }
-        else
-        {
-            showTextUni(contentStream, font, fontSize, text, 
Bidi.DIRECTION_LEFT_TO_RIGHT);
-        }
-    }
-
-    /**
-     * Shows a text using FOP positioning
-     *
-     * @param contentStream
-     * @param font
-     * @param text the text to show
-     * @param fontSize
-     * @param bidiLevel
-     * @throws IOException if an I/O error occurs
-     */
     protected void showTextUni(ContentStreamForGlyphLayoutInterface 
contentStream, PDType0Font font, float fontSize,
             String text, int bidiLevel) throws IOException
     {
@@ -244,33 +250,20 @@ public class GlyphLayoutProcessorFop imp
         Objects.requireNonNull(text, "Text must be set");
         Objects.requireNonNull(contentStream, "contentStream must be set");
 
-        GlyphMapping glyphMapping = computeGlyphAndPositions(font, fontSize, 
text, bidiLevel);
-
-        text = glyphMapping.mapping == null ? text : glyphMapping.mapping;
-
-        int[][] gpa = glyphMapping.gposAdjustments != null ? 
glyphMapping.gposAdjustments : createZeroGpa(text.length());
+        TextAndGpa textAndGpa = computeGlyphsAndPositions(font, fontSize, 
text, bidiLevel);
+        text = textAndGpa.getText();
+        int[][] gpa = textAndGpa.getGpa();
+        boolean hasPositioning = textAndGpa.hasPositioning();
 
-        if (bidiLevel == Bidi.DIRECTION_RIGHT_TO_LEFT)
-        {
-            gpa = reverseGpa(gpa);
-            text = new StringBuilder(text).reverse().toString();
-        }
         int[] glyphIds = convertCharsToGlyphIds(font, text);
 
-        if (glyphIds.length != text.length())
+        if (glyphIds.length != text.length() && hasPositioning)
         {
-            if (glyphMapping.gposAdjustments == null)
-            {
-                gpa = createZeroGpa(glyphIds.length);
-            }
-            else
-            {
-                // This case:
-                // letters from the supplementary multilingual plane AND 
position adjustments
-                // is not implemented
-                throw new IllegalStateException("glyphIds.length != 
text.length() and gposAdjustments!=null"
-                        + glyphIds.length + " " + text.length() + " " + text);
-            }
+            // This case:
+            // letters from the supplementary multilingual plane AND position 
adjustments
+            // is not implemented
+            throw new IllegalStateException("glyphIds.length != text.length() 
and gposAdjustments!=null"
+                    + glyphIds.length + " " + text.length() + " " + text);
         }
 
         final float delta = Math.ulp(fontSize); // do only adjustments bigger 
than or equal to one ulp of the font size
@@ -282,7 +275,7 @@ public class GlyphLayoutProcessorFop imp
             float px = gpa[i][0] / fontSize * 1000f / FOP_FONTSIZE_FACTOR;
             float py = gpa[i][1] / FOP_FONTSIZE_FACTOR;
             float ax = gpa[i][2] / fontSize * 1000f / FOP_FONTSIZE_FACTOR;
-            float ay = gpa[i][3] / FOP_FONTSIZE_FACTOR; // ignored in 
horizontal typesetting
+            // float ay = gpa[i][3] / FOP_FONTSIZE_FACTOR; // ignored in 
horizontal typesetting
 
             if (Math.abs(py) >= delta)
             {
@@ -334,10 +327,9 @@ public class GlyphLayoutProcessorFop imp
     protected int[][] createZeroGpa(int length)
     {
         int[][] gpa = new int[length][];
-        int[] z4 = new int[] { 0, 0, 0, 0 };
         for (int i = 0; i < length; i++)
         {
-            gpa[i] = z4;
+            gpa[i] = new int[] { 0, 0, 0, 0 };
         }
         return gpa;
     }

Modified: 
pdfbox/trunk/pdfbox-layout-fop/src/test/java/org/apache/pdfbox/glyphlayout/fop/GlyphLayoutDin91379Test.java
==============================================================================
--- 
pdfbox/trunk/pdfbox-layout-fop/src/test/java/org/apache/pdfbox/glyphlayout/fop/GlyphLayoutDin91379Test.java
 Mon Aug 10 11:01:27 2026        (r1937027)
+++ 
pdfbox/trunk/pdfbox-layout-fop/src/test/java/org/apache/pdfbox/glyphlayout/fop/GlyphLayoutDin91379Test.java
 Mon Aug 10 11:01:35 2026        (r1937028)
@@ -149,8 +149,8 @@ class GlyphLayoutDin91379Test extends Te
             float x, float y, String s) throws IOException
     {
 
-        s = s.replaceAll("\t", "    ");
-        String[] lines = s.split("[\n]");
+        s = s.replace("\t", "    ");
+        String[] lines = s.split("[\\n]");
 
         for (String line : lines)
         {

Modified: 
pdfbox/trunk/pdfbox-layout-fop/src/test/java/org/apache/pdfbox/glyphlayout/fop/GlyphLayoutLigaturesAndKerningTest.java
==============================================================================
--- 
pdfbox/trunk/pdfbox-layout-fop/src/test/java/org/apache/pdfbox/glyphlayout/fop/GlyphLayoutLigaturesAndKerningTest.java
      Mon Aug 10 11:01:27 2026        (r1937027)
+++ 
pdfbox/trunk/pdfbox-layout-fop/src/test/java/org/apache/pdfbox/glyphlayout/fop/GlyphLayoutLigaturesAndKerningTest.java
      Mon Aug 10 11:01:35 2026        (r1937028)
@@ -148,8 +148,8 @@ class GlyphLayoutLigaturesAndKerningTest
             float x, float y, String s) throws IOException
     {
 
-        s = s.replaceAll("\t", "    ");
-        String[] lines = s.split("[\n]");
+        s = s.replace("\t", "    ");
+        String[] lines = s.split("[\\n]");
 
         float height = font.getBoundingBox().getHeight();
 

Modified: 
pdfbox/trunk/pdfbox-layout-fop/src/test/java/org/apache/pdfbox/glyphlayout/fop/GlyphLayoutSMPTest.java
==============================================================================
--- 
pdfbox/trunk/pdfbox-layout-fop/src/test/java/org/apache/pdfbox/glyphlayout/fop/GlyphLayoutSMPTest.java
      Mon Aug 10 11:01:27 2026        (r1937027)
+++ 
pdfbox/trunk/pdfbox-layout-fop/src/test/java/org/apache/pdfbox/glyphlayout/fop/GlyphLayoutSMPTest.java
      Mon Aug 10 11:01:35 2026        (r1937028)
@@ -64,8 +64,8 @@ class GlyphLayoutSMPTest extends TestBas
             float x, float y, String s) throws IOException
     {
 
-        s = s.replaceAll("\t", "    ");
-        String[] lines = s.split("[\n]");
+        s = s.replace("\t", "    ");
+        String[] lines = s.split("[\\n]");
 
         for (String line : lines)
         {

Added: 
pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/AbstractGlyphLayoutProcessor.java
==============================================================================
--- /dev/null   00:00:00 1970   (empty, because file is newly added)
+++ 
pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/AbstractGlyphLayoutProcessor.java
       Mon Aug 10 11:01:35 2026        (r1937028)
@@ -0,0 +1,179 @@
+/*
+ * 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.PDType0Font;
+
+import java.io.IOException;
+import java.text.Bidi;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * Abstract super class for classes implementing GlyphLayoutProcessorInterface
+ *
+ * @author Volker Kunert
+ */
+public abstract class AbstractGlyphLayoutProcessor implements 
GlyphLayoutProcessorInterface
+{
+    /**
+     * Class for text and Bidi-Level
+     */
+    protected static class TextAndBidiLevel
+    {
+        private final String text;
+        private final int bidiLevel;
+
+        TextAndBidiLevel(String text, int bidiLevel)
+        {
+            this.text = text;
+            this.bidiLevel = bidiLevel;
+        }
+
+        public String getText()
+        {
+            return text;
+        }
+
+        public int getBidiLevel()
+        {
+            return bidiLevel;
+        }
+    }
+
+    /**
+     * Compute the string width for a unidirectional string
+     * @param font to be used
+     * @param fontSize font size
+     * @param text text
+     * @param bidiLevel Bidi Level
+     * @return string width
+     */
+    protected abstract float getStringWidthUni(PDType0Font font, float 
fontSize, String text, int bidiLevel)
+            throws IOException;
+
+
+    /**
+     * Compute the width for a text
+     * @param font to be used
+     * @param fontSize font size
+     * @param text text
+     * @return string width
+     */
+    public float getStringWidth(PDType0Font font, float fontSize, String text) 
throws IOException
+    {
+        float width = 0f;
+        List<TextAndBidiLevel> textAndBidiLevels = 
doBidiSplittingAndReordering(text);
+        for (TextAndBidiLevel textAndBidiLevel:  textAndBidiLevels)
+        {
+            width += getStringWidthUni(font, fontSize, 
textAndBidiLevel.getText(), textAndBidiLevel.getBidiLevel());
+        }
+        return width;
+    }
+
+    /**
+     * Shows unidirectional 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
+     * @param bidiLevel Bidi level*
+     *
+     * @throws IOException if an I/O exception occurs
+     * @throws IllegalArgumentException if glyphs are missing
+     */
+    protected abstract void showTextUni(ContentStreamForGlyphLayoutInterface 
contentStream, PDType0Font font, float fontSize,
+                               String text, int bidiLevel) throws IOException;
+
+    /**
+     * 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
+     */
+    public void showText(ContentStreamForGlyphLayoutInterface contentStream, 
PDType0Font font, float fontSize, String text)
+            throws IOException
+    {
+        List<TextAndBidiLevel> textAndBidiLevels = 
doBidiSplittingAndReordering(text);
+        for (TextAndBidiLevel textAndBidiLevel:  textAndBidiLevels)
+        {
+            showTextUni(contentStream, font, fontSize, 
textAndBidiLevel.getText(), textAndBidiLevel.getBidiLevel());
+        }
+    }
+
+    /**
+     * Do Bidi splitting and reordering
+     * @param text text
+     * @return list of texts and bidi levels
+     */
+    protected List<TextAndBidiLevel> doBidiSplittingAndReordering(String text)
+    {
+        ArrayList<TextAndBidiLevel> textAndBidiLevels = new ArrayList<>();
+
+        Objects.requireNonNull(text, "Text must be set");
+
+        if (Bidi.requiresBidi(text.toCharArray(), 0, text.length()))
+        {
+            Bidi bidi = new Bidi(text, Bidi.DIRECTION_DEFAULT_LEFT_TO_RIGHT);
+            if (bidi.isMixed())
+            {
+                // Split and Reorder
+                // See PDFTextStripper.handleDirection
+                // collect individual bidi information
+                int runCount = bidi.getRunCount();
+                byte[] levels = new byte[runCount];
+                Integer[] runs = new Integer[runCount];
+
+                for (int i = 0; i < runCount; i++)
+                {
+                    levels[i] = (byte) bidi.getRunLevel(i);
+                    runs[i] = i;
+                }
+                // reorder individual parts based on their levels
+                Bidi.reorderVisually(levels, 0, runs, 0, runCount);
+
+                for (int i = 0; i < runCount; i++)
+                {
+                    int index = runs[i];
+                    int start = bidi.getRunStart(index);
+                    int limit = bidi.getRunLimit(index);
+                    int bidiLevel = levels[index];
+                    String part = text.substring(start, limit);
+                    textAndBidiLevels.add(new TextAndBidiLevel(part, 
bidiLevel));
+                }
+            }
+            else
+            {
+                textAndBidiLevels.add(new TextAndBidiLevel(text, 
bidi.getBaseLevel()));
+            }
+        }
+        else
+        {
+            textAndBidiLevels.add(new TextAndBidiLevel(text, 
Bidi.DIRECTION_LEFT_TO_RIGHT));
+        }
+        return Collections.unmodifiableList(textAndBidiLevels);
+    }
+}

Modified: 
pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/GlyphLayoutProcessorInterface.java
==============================================================================
--- 
pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/GlyphLayoutProcessorInterface.java
      Mon Aug 10 11:01:27 2026        (r1937027)
+++ 
pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/GlyphLayoutProcessorInterface.java
      Mon Aug 10 11:01:35 2026        (r1937028)
@@ -39,6 +39,16 @@ public interface GlyphLayoutProcessorInt
     boolean supportsFont(PDFont font);
 
     /**
+     * Compute the width for a text
+     * @param font to be used
+     * @param fontSize font size
+     * @param text text
+     * @return string width
+     */
+    float getStringWidth(PDType0Font font, float fontSize, String text) throws 
IOException;
+
+
+    /**
      * Shows a text using glyph positioning (if needed)
      *
      * @param contentStream the content stream

Reply via email to