]> source.dussan.org Git - xmlgraphics-fop.git/commitdiff
FOP-2391: enable bidi processing of SVG text chunks
authorGlenn Adams <gadams@apache.org>
Sat, 23 Aug 2014 03:50:42 +0000 (03:50 +0000)
committerGlenn Adams <gadams@apache.org>
Sat, 23 Aug 2014 03:50:42 +0000 (03:50 +0000)
git-svn-id: https://svn.apache.org/repos/asf/xmlgraphics/fop/trunk@1619960 13f79535-47bb-0310-9956-ffa450edef68

build.xml
findbugs-exclude.xml
lib/batik-all-trunk.jar
src/java/org/apache/fop/complexscripts/util/CharMirror.java
src/java/org/apache/fop/fo/FOText.java
src/java/org/apache/fop/fonts/GlyphMapping.java
src/java/org/apache/fop/fonts/TextFragment.java
src/java/org/apache/fop/svg/NativeTextPainter.java
src/java/org/apache/fop/svg/PDFTextPainter.java
src/java/org/apache/fop/svg/font/ComplexGlyphVector.java
src/java/org/apache/fop/svg/font/FOPGVTGlyphVector.java

index 5c0434a908caca361cf06a307b417a318103de70..9194d99a60d170fbb878856cbe3e2865b93f62f6 100644 (file)
--- a/build.xml
+++ b/build.xml
@@ -564,6 +564,9 @@ list of possible build targets.
       <include name="org/apache/fop/apps/FOPException.class"/>
       <include name="org/apache/fop/apps/io/**"/>
       <include name="org/apache/fop/area/AreaTreeControl*"/>
+      <include name="org/apache/fop/complexscripts/bidi/BidiClass.class"/>
+      <include name="org/apache/fop/complexscripts/bidi/BidiConstants.class"/>
+      <include name="org/apache/fop/complexscripts/bidi/UnicodeBidiAlgorithm.class"/>
       <include name="org/apache/fop/complexscripts/fonts/*.class"/>
       <include name="org/apache/fop/complexscripts/util/GlyphTester.class"/>
       <include name="org/apache/fop/events/EventProducer.class"/>
@@ -573,7 +576,9 @@ list of possible build targets.
       <include name="org/apache/fop/svg/**"/>
       <include name="org/apache/fop/fonts/**"/>
       <include name="org/apache/fop/render/shading/**"/>
+      <include name="org/apache/fop/traits/Direction.class"/>
       <include name="org/apache/fop/traits/MinOptMax.class"/>
+      <include name="org/apache/fop/traits/TraitEnum.class"/>
       <include name="org/apache/fop/util/CMYKColorSpace*.class"/>
       <include name="org/apache/fop/util/Color*.class"/>
       <include name="org/apache/fop/util/ASCII*.class"/>
index 246e7b0312b2cbe8704d2eaa030671e91507c767..5124f00793d25d7fe99612968b40532d534bdb9a 100644 (file)
         <Class name="org.apache.fop.render.intermediate.IFGraphicContext"/>
         <Method name="clone"/>
       </And>
+      <And>
+        <Class name="org.apache.fop.svg.text.BidiAttributedCharacterIterator"/>
+        <Method name="clone"/>
+      </And>
     </Or>
   </Match>
   <Match>
index 347757f32562110761f4f660736f0060a3018df3..79293f70a0cbd9e1ea54eb5e5be73363796fc5e6 100644 (file)
Binary files a/lib/batik-all-trunk.jar and b/lib/batik-all-trunk.jar differ
index 8de2c1fab527b8ff94a018117ee70e34835fbba9..5e905931a3cb2865cba019ae5278bb9514ec675f 100644 (file)
@@ -38,12 +38,27 @@ public final class CharMirror {
      */
     public static String mirror(String s) {
         StringBuffer sb = new StringBuffer(s);
-        for (int i = 0, n = sb.length(); i < n; i++) {
+        for (int i = 0, n = sb.length(); i < n; ++i) {
             sb.setCharAt(i, (char) mirror(sb.charAt(i)));
         }
         return sb.toString();
     }
 
+    /**
+     * Determine if string has a mirrorable character.
+     * @param s a string whose characters are to be tested for mirrorability
+     * @return true if some character can be mirrored
+     */
+    public static boolean hasMirrorable(String s) {
+        for (int i = 0, n = s.length(); i < n; ++i) {
+            char c = s.charAt(i);
+            if (Arrays.binarySearch(mirroredCharacters, c) >= 0) {
+                return true;
+            }
+        }
+        return false;
+    }
+
     private static int[] mirroredCharacters = {
         0x0028,
         0x0029,
index 305e2db577f1f80dfb26e762dd7c699eec3eaeb5..9f286d888127a07bf33d04c9b27f7ae46501ce80 100644 (file)
@@ -21,6 +21,8 @@ package org.apache.fop.fo;
 
 import java.awt.Color;
 import java.nio.CharBuffer;
+import java.text.CharacterIterator;
+import java.text.StringCharacterIterator;
 import java.util.NoSuchElementException;
 import java.util.Stack;
 
@@ -48,6 +50,9 @@ public class FOText extends FONode implements CharSequence, TextFragment {
     /** the <code>CharBuffer</code> containing the text */
     private CharBuffer charBuffer;
 
+    // cached iterator
+    private CharacterIterator charIterator;
+
     // The value of FO traits (refined properties) that apply to #PCDATA
     // (aka implicit sequence of fo:character)
     private CommonFont commonFont;
@@ -649,16 +654,39 @@ public class FOText extends FONode implements CharSequence, TextFragment {
         return country;
     }
 
-    /** @return the language trait */
+    @Override
+    public synchronized CharacterIterator getIterator() {
+        if (charIterator != null) {
+            charIterator = new StringCharacterIterator(toString());
+        }
+        return charIterator;
+    }
+
+    @Override
+    public int getBeginIndex() {
+        return 0;
+    }
+
+    @Override
+    public int getEndIndex() {
+        return length();
+    }
+
+    @Override
     public String getLanguage() {
         return language;
     }
 
-    /** @return the script trait */
+    @Override
     public String getScript() {
         return script;
     }
 
+    @Override
+    public int getBidiLevel() {
+        return length() > 0 ? bidiLevelAt(0) : -1;
+    }
+
     /** {@inheritDoc} */
     public String toString() {
         if (charBuffer == null) {
index f0fadaad476952ebc999c7dc270cf04220386700..8c80eeb7c46ec2e6c2ebccca4e02745da47afc75 100644 (file)
@@ -19,7 +19,6 @@
 
 package org.apache.fop.fonts;
 
-import java.util.Collections;
 import java.util.List;
 
 import org.apache.commons.logging.Log;
@@ -320,30 +319,6 @@ public class GlyphMapping {
         areaIPD = areaIPD.plus(idp);
     }
 
-    public void reverse() {
-        if (mapping == null) {
-            return;
-        }
-        if (mapping.length() > 0) {
-            mapping = new StringBuffer(mapping).reverse().toString();
-        }
-        if (associations != null) {
-            Collections.reverse(associations);
-        }
-        if (gposAdjustments != null) {
-            reverse(gposAdjustments);
-        }
-    }
-
-    private static void reverse(int[][] aa) {
-        for (int i = 0, n = aa.length, m = n / 2; i < m; i++) {
-            int k = n - i - 1;
-            int[] t = aa [ k ];
-            aa [ k ] = aa [ i ];
-            aa [ i ] = t;
-        }
-    }
-
     public String toString() {
         return super.toString() + "{"
                 + "interval = [" + startIndex + "," + endIndex + "]"
index ad72db8e06c73c484092233ef8c3bb0af893146d..8722ecf2ecf8f0af0392ee24e962f422801fa4fd 100644 (file)
 
 package org.apache.fop.fonts;
 
+import java.text.CharacterIterator;
+
+/**
+ * Encapsulates a sub-sequence (fragement) of a text iterator (or other text source),
+ * where begin index and end index are indices into larger text iterator that denote
+ * [begin,end) of sub-sequence range. Additionally associated with a designated script
+ * (or "auto"), a designated language (or "none"), and a (single) bidi level (or -1
+ * if not known).
+ */
 public interface TextFragment {
 
+    /**
+     * Obtain reference to underlying iterator.
+     */
+    CharacterIterator getIterator();
+
+    /**
+     * Obtain beginning index (inclusive) of sub-sequence of fragment in overall text source.
+     */
+    int getBeginIndex();
+
+    /**
+     * Obtain ending index (exclusive) of sub-sequence of fragment in overall text source.
+     */
+    int getEndIndex();
+
+    /**
+     * Obtain associated script (if designated) or "auto" if not.
+     */
     String getScript();
 
+    /**
+     * Obtain associated language (if designated) or "none" if not.
+     */
     String getLanguage();
 
-    char charAt(int index);
+    /**
+     * Obtain associated bidi level (if known) or -1 if not.
+     */
+    int getBidiLevel();
+
+    /**
+     * Obtain character at specified index within this fragment's sub-sequence,
+     * where index 0 corresponds to beginning index in overal text source, and
+     * subSequenceIndex must be less than ending index - beginning index.
+     */
+    char charAt(int subSequenceIndex);
 
     CharSequence subSequence(int startIndex, int endIndex);
 }
index b5c7d7882de9971a97b2be2a6f3df14995f2751c..18c140163868c1aa61811ccd1c1175bb850d14bd 100644 (file)
@@ -31,14 +31,17 @@ import java.awt.geom.Point2D;
 import java.io.IOException;
 import java.text.AttributedCharacterIterator;
 import java.util.List;
+import java.util.Set;
 
 import org.apache.commons.logging.Log;
 import org.apache.commons.logging.LogFactory;
 
 import org.apache.batik.bridge.SVGGVTFont;
+import org.apache.batik.gvt.TextNode;
 import org.apache.batik.gvt.font.FontFamilyResolver;
 import org.apache.batik.gvt.font.GVTGlyphVector;
 import org.apache.batik.gvt.renderer.StrokingTextPainter;
+import org.apache.batik.gvt.renderer.StrokingTextPainter.TextChunk;
 import org.apache.batik.gvt.text.GlyphLayout;
 import org.apache.batik.gvt.text.TextLayoutFactory;
 import org.apache.batik.gvt.text.TextPaintInfo;
@@ -48,6 +51,7 @@ import org.apache.fop.fonts.Font;
 import org.apache.fop.fonts.FontInfo;
 import org.apache.fop.svg.font.FOPFontFamilyResolverImpl;
 import org.apache.fop.svg.font.FOPGVTFont;
+import org.apache.fop.svg.text.BidiAttributedCharacterIterator;
 import org.apache.fop.svg.text.ComplexGlyphLayout;
 import org.apache.fop.util.CharUtilities;
 
@@ -213,6 +217,114 @@ public abstract class NativeTextPainter extends StrokingTextPainter {
         return chars;
     }
 
+    // Use FOP's bidi algorithm implementation and sub-divide each chunk into runs
+    // that respect bidi level boundaries. N.B. batik does not sub-divide chunks at
+    // bidi level boundaries because it performs eager reordering. In FOP, we need
+    // to perform lazy reordering after character to glyph mapping occurs since
+    // that mapping process requires logical (not visual) ordered input.
+    @Override
+    public List computeTextRuns(TextNode node, AttributedCharacterIterator nodeACI,
+        AttributedCharacterIterator [] chunkACIs) {
+        nodeACI.first();
+        int defaultBidiLevel = (nodeACI.getAttribute(WRITING_MODE) == WRITING_MODE_RTL) ? 1 : 0;
+        for (int i = 0, n = chunkACIs.length; i < n; ++i) {
+            chunkACIs[i] = new BidiAttributedCharacterIterator(chunkACIs[i], defaultBidiLevel);
+        }
+        return super.computeTextRuns(node, nodeACI, chunkACIs, (int[][]) null);
+    }
+
+    // We want to sub-divide text chunks into distinct runs at bidi level boundaries.
+    @Override
+    protected Set getTextRunBoundaryAttributes() {
+        Set textRunBoundaryAttributes = super.getTextRunBoundaryAttributes();
+        if (!textRunBoundaryAttributes.contains(BIDI_LEVEL)) {
+            textRunBoundaryAttributes.add(BIDI_LEVEL);
+        }
+        return textRunBoundaryAttributes;
+    }
+
+    // Perform reordering of runs.
+    @Override
+    protected List reorderTextRuns(TextChunk chunk, List runs) {
+        // 1. determine min/max bidi levels for runs
+        int mn = -1;
+        int mx = -1;
+        for (TextRun r : (List<TextRun>) runs) {
+            int level = r.getBidiLevel();
+            if (level >= 0) {
+                if ((mn < 0) || (level < mn)) {
+                    mn = level;
+                }
+                if ((mx < 0) || (level > mx)) {
+                    mx = level;
+                }
+            }
+        }
+
+        // 2. reorder from maximum level to minimum odd level
+        if (mx > 0) {
+            for (int l1 = mx, l2 = ((mn & 1) == 0) ? (mn + 1) : mn; l1 >= l2; l1--) {
+                runs = reorderRuns(runs, l1);
+            }
+        }
+
+        // 3. reverse glyphs (and perform mirroring) in runs as needed
+        boolean mirror = true;
+        reverseGlyphs(runs, mirror);
+
+        return runs;
+    }
+
+    private List reorderRuns(List runs, int level) {
+        assert level >= 0;
+        List runsNew = new java.util.ArrayList();
+        for (int i = 0, n = runs.size(); i < n; i++) {
+            TextRun tri = (TextRun) runs.get(i);
+            if (tri.getBidiLevel() < level) {
+                runsNew.add(tri);
+            } else {
+                int s = i;
+                int e = s;
+                while (e < n) {
+                    TextRun tre = (TextRun) runs.get(e);
+                    if (tre.getBidiLevel() < level) {
+                        break;
+                    } else {
+                        e++;
+                    }
+                }
+                if (s < e) {
+                    runsNew.addAll(reverseRuns(runs, s, e));
+                }
+                i = e - 1;
+            }
+        }
+        if (!runsNew.equals(runs)) {
+            runs = runsNew;
+        }
+        return runs;
+    }
+
+    private List reverseRuns(List runs, int s, int e) {
+        int n = e - s;
+        List runsNew = new java.util.ArrayList(n);
+        if (n > 0) {
+            for (int i = 0; i < n; i++) {
+                int k = (n - i - 1);
+                TextRun tr = (TextRun) runs.get(s + k);
+                tr.reverse();
+                runsNew.add(tr);
+            }
+        }
+        return runsNew;
+    }
+
+    private void reverseGlyphs(List runs, boolean mirror) {
+        for (TextRun r : (List<TextRun>) runs) {
+            r.maybeReverseGlyphs(mirror);
+        }
+    }
+
     protected abstract void preparePainting(Graphics2D g2d);
 
     protected abstract void saveGraphicsState() throws IOException;
index c3ff744d5f0df0ce1c74d37992fb201e6f832770..c5fa9f04e20ba0c66d683aa58daf232b2627aa26 100644 (file)
@@ -26,10 +26,7 @@ import java.awt.Shape;
 import java.awt.Stroke;
 import java.awt.geom.AffineTransform;
 import java.awt.geom.Point2D;
-import java.text.AttributedCharacterIterator;
-import java.util.List;
 
-import org.apache.batik.gvt.TextNode;
 import org.apache.batik.gvt.text.TextPaintInfo;
 
 import org.apache.fop.fonts.FontInfo;
@@ -185,12 +182,4 @@ class PDFTextPainter extends NativeTextPainter {
         textUtil.writeTJMappedChar(glyph);
     }
 
-    @Override
-    public List computeTextRuns(TextNode node,
-                                AttributedCharacterIterator nodeACI,
-                                AttributedCharacterIterator [] chunkACIs) {
-        // skip Batik's bidi reordering and use identity character index maps
-        return super.computeTextRuns(node, nodeACI, chunkACIs, (int[][]) null);
-    }
-
 }
index 55f2eb3f24b5be08cd0c07422f563cefef488217..8fa705e4cea68c394747c599281e75ced7f1b4ed 100644 (file)
 package org.apache.fop.svg.font;
 
 import java.awt.font.FontRenderContext;
+import java.awt.geom.AffineTransform;
+import java.awt.geom.Rectangle2D;
 import java.text.AttributedCharacterIterator;
 import java.text.CharacterIterator;
+import java.util.Collections;
+import java.util.List;
 
 import org.apache.batik.gvt.text.GVTAttributedCharacterIterator;
 
-import org.apache.fop.fonts.GlyphMapping;
+import org.apache.fop.complexscripts.util.CharAssociation;
+import org.apache.fop.complexscripts.util.CharMirror;
+import org.apache.fop.fonts.Font;
 
 class ComplexGlyphVector extends FOPGVTGlyphVector {
 
@@ -35,6 +41,9 @@ class ComplexGlyphVector extends FOPGVTGlyphVector {
     public static final Integer WRITING_MODE_RTL
         = GVTAttributedCharacterIterator.TextAttribute.WRITING_MODE_RTL;
 
+    private boolean reversed; // true if this GV was reversed
+    private boolean mirrored; // true if this GV required some mirroring
+
     ComplexGlyphVector(FOPGVTFont font, final CharacterIterator iter, FontRenderContext frc) {
         super(font, iter, frc);
     }
@@ -43,14 +52,129 @@ class ComplexGlyphVector extends FOPGVTGlyphVector {
         super.performDefaultLayout();
     }
 
-    protected void maybeReverse(GlyphMapping mapping) {
-        if (charIter instanceof AttributedCharacterIterator) {
-            AttributedCharacterIterator aci = (AttributedCharacterIterator) charIter;
-            aci.first();
-            if (aci.getAttribute(WRITING_MODE) == WRITING_MODE_RTL) {
-                mapping.reverse();
+    public boolean isReversed() {
+        return reversed;
+    }
+
+    public void maybeReverse(boolean mirror) {
+        if (!reversed) {
+            if (glyphs != null) {
+                if (glyphs.length > 1) {
+                    reverse(glyphs);
+                    if (associations != null) {
+                        Collections.reverse(associations);
+                    }
+                    if (positions != null) {
+                        reverse(positions);
+                    }
+                    if (boundingBoxes != null) {
+                        reverse(boundingBoxes);
+                    }
+                    if (glyphTransforms != null) {
+                        reverse(glyphTransforms);
+                    }
+                    if (glyphVisibilities != null) {
+                        reverse(glyphVisibilities);
+                    }
+                }
+                if (maybeMirror()) {
+                    mirrored = true;
+                }
+            }
+            reversed = true;
+        }
+    }
+
+    // For each mirrorable character in source text, perform substitution of
+    // associated glyph with a mirrored glyph. N.B. The source text is NOT
+    // modified, only the mapped glyphs.
+    private boolean maybeMirror() {
+        boolean mirrored = false;
+        String s = text.subSequence(text.getBeginIndex(), text.getEndIndex()).toString();
+        if (CharMirror.hasMirrorable(s)) {
+            String m = CharMirror.mirror(s);
+            assert m.length() == s.length();
+            for (int i = 0, n = m.length(); i < n; ++i) {
+                char cs = s.charAt(i);
+                char cm = m.charAt(i);
+                if (cm != cs) {
+                    if (substituteMirroredGlyph(i, cm)) {
+                        mirrored = true;
+                    }
+                }
             }
         }
+        return mirrored;
     }
 
+    private boolean substituteMirroredGlyph(int index, char mirror) {
+        Font f = font.getFont();
+        int gi = 0;
+        for (CharAssociation ca : (List<CharAssociation>) associations) {
+            if (ca.contained(index, 1)) {
+                setGlyphCode(gi, f.mapChar(mirror));
+                return true;
+            } else {
+                ++gi;
+            }
+        }
+        return false;
+    }
+
+    private static void reverse(boolean[] ba) {
+        for (int i = 0, n = ba.length, m = n / 2; i < m; i++) {
+            int k = n - i - 1;
+            boolean t = ba [ k ];
+            ba [ k ] = ba [ i ];
+            ba [ i ] = t;
+        }
+    }
+
+    private static void reverse(int[] ia) {
+        for (int i = 0, n = ia.length, m = n / 2; i < m; i++) {
+            int k = n - i - 1;
+            int t = ia [ k ];
+            ia [ k ] = ia [ i ];
+            ia [ i ] = t;
+        }
+    }
+
+    private static void reverse(float[] fa) {
+        int skip = 2;
+        int numPositions = fa.length / skip;
+        for (int i = 0, n = numPositions, m = n / 2; i < m; ++i) {
+            int j = n - i - 1;
+            for (int k = 0; k < skip; ++k) {
+                int l1 = i * skip + k;
+                int l2 = j * skip + k;
+                float t = fa [ l2 ];
+                fa [ l2 ] = fa [ l1 ];
+                fa [ l1 ] = t;
+            }
+        }
+        float runAdvanceX = fa [ 0 ];
+        for (int i = 0, n = fa.length; i < n; i += 2) {
+            fa [ i ] = runAdvanceX - fa [ i ];
+        }
+    }
+
+    private static void reverse(Rectangle2D[] ra) {
+        for (int i = 0, n = ra.length, m = n / 2; i < m; i++) {
+            int k = n - i - 1;
+            Rectangle2D t = ra [ k ];
+            ra [ k ] = ra [ i ];
+            ra [ i ] = t;
+        }
+    }
+
+    private static void reverse(AffineTransform[] ta) {
+        for (int i = 0, n = ta.length, m = n / 2; i < m; i++) {
+            int k = n - i - 1;
+            AffineTransform t = ta [ k ];
+            ta [ k ] = ta [ i ];
+            ta [ i ] = t;
+        }
+    }
+
+
 }
index 40a1e8ad3d1bdbbf903c86c4835f649b8aebb638..2b2115935d747a103fcce3e220c3d7f5c7580595 100644 (file)
@@ -49,9 +49,9 @@ import org.apache.fop.traits.MinOptMax;
 
 class FOPGVTGlyphVector implements GVTGlyphVector {
 
-    protected final CharacterIterator charIter;
+    protected final TextFragment text;
 
-    private final FOPGVTFont font;
+    protected final FOPGVTFont font;
 
     private final int fontSize;
 
@@ -65,18 +65,18 @@ class FOPGVTGlyphVector implements GVTGlyphVector {
 
     protected float[] positions;
 
-    private Rectangle2D[] boundingBoxes;
+    protected Rectangle2D[] boundingBoxes;
 
-    private GeneralPath outline;
+    protected GeneralPath outline;
 
-    private AffineTransform[] glyphTransforms;
+    protected AffineTransform[] glyphTransforms;
 
-    private boolean[] glyphVisibilities;
+    protected boolean[] glyphVisibilities;
 
-    private Rectangle2D logicalBounds;
+    protected Rectangle2D logicalBounds;
 
     FOPGVTGlyphVector(FOPGVTFont font, final CharacterIterator iter, FontRenderContext frc) {
-        this.charIter = iter;
+        this.text = new SVGTextFragment(iter);
         this.font = font;
         Font f = font.getFont();
         this.fontSize = f.getFontSize();
@@ -86,14 +86,12 @@ class FOPGVTGlyphVector implements GVTGlyphVector {
 
     public void performDefaultLayout() {
         Font f = font.getFont();
-        TextFragment text = new SVGTextFragment(charIter);
         MinOptMax letterSpaceIPD = MinOptMax.ZERO;
-        MinOptMax[] letterSpaceAdjustments = new MinOptMax[charIter.getEndIndex() - charIter.getBeginIndex()];
-        GlyphMapping mapping = GlyphMapping.doGlyphMapping(text, charIter.getBeginIndex(), charIter.getEndIndex(),
-            f, letterSpaceIPD, letterSpaceAdjustments, '\0', '\0', false, 0, true, true);
-        maybeReverse(mapping);
+        MinOptMax[] letterSpaceAdjustments = new MinOptMax[text.getEndIndex() - text.getBeginIndex()];
+        GlyphMapping mapping = GlyphMapping.doGlyphMapping(text, text.getBeginIndex(), text.getEndIndex(),
+            f, letterSpaceIPD, letterSpaceAdjustments, '\0', '\0', false, text.getBidiLevel(), true, true);
         CharacterIterator glyphAsCharIter =
-            mapping.mapping != null ? new StringCharacterIterator(mapping.mapping) : charIter;
+            mapping.mapping != null ? new StringCharacterIterator(mapping.mapping) : text.getIterator();
         this.glyphs = buildGlyphs(f, glyphAsCharIter);
         this.associations = mapping.associations;
         this.positions = buildGlyphPositions(glyphAsCharIter, mapping.gposAdjustments, letterSpaceAdjustments);
@@ -102,9 +100,6 @@ class FOPGVTGlyphVector implements GVTGlyphVector {
         this.glyphTransforms = new AffineTransform[this.glyphs.length];
     }
 
-    protected void maybeReverse(GlyphMapping mapping) {
-    }
-
     private static class SVGTextFragment implements TextFragment {
 
         private final CharacterIterator charIter;
@@ -113,6 +108,8 @@ class FOPGVTGlyphVector implements GVTGlyphVector {
 
         private String language;
 
+        private int level = -1;
+
         SVGTextFragment(CharacterIterator charIter) {
             this.charIter = charIter;
             if (charIter instanceof AttributedCharacterIterator) {
@@ -120,9 +117,27 @@ class FOPGVTGlyphVector implements GVTGlyphVector {
                 aci.first();
                 this.script = (String) aci.getAttribute(GVTAttributedCharacterIterator.TextAttribute.SCRIPT);
                 this.language = (String) aci.getAttribute(GVTAttributedCharacterIterator.TextAttribute.LANGUAGE);
+                Integer level = (Integer) aci.getAttribute(GVTAttributedCharacterIterator.TextAttribute.BIDI_LEVEL);
+                if (level != null) {
+                    this.level = level.intValue();
+                }
             }
         }
 
+        public CharacterIterator getIterator() {
+            return charIter;
+        }
+
+        public int getBeginIndex() {
+            return charIter.getBeginIndex();
+        }
+
+        public int getEndIndex() {
+            return charIter.getEndIndex();
+        }
+
+        // TODO - [GA] the following appears to be broken because it ignores
+        // sttart and end index arguments
         public CharSequence subSequence(int startIndex, int endIndex) {
             StringBuilder sb = new StringBuilder();
             for (char c = charIter.first(); c != CharacterIterator.DONE; c = charIter.next()) {
@@ -147,6 +162,10 @@ class FOPGVTGlyphVector implements GVTGlyphVector {
             }
         }
 
+        public int getBidiLevel() {
+            return level;
+        }
+
         public char charAt(int index) {
             return charIter.setIndex(index - charIter.getBeginIndex());
         }
@@ -225,6 +244,10 @@ class FOPGVTGlyphVector implements GVTGlyphVector {
         return frc;
     }
 
+    public void setGlyphCode(int glyphIndex, int glyphCode) {
+        glyphs[glyphIndex] = glyphCode;
+    }
+
     public int getGlyphCode(int glyphIndex) {
         return glyphs[glyphIndex];
     }
@@ -372,6 +395,13 @@ class FOPGVTGlyphVector implements GVTGlyphVector {
         return endGlyphIndex - startGlyphIndex + 1;
     }
 
+    public boolean isReversed() {
+        return false;
+    }
+
+    public void maybeReverse(boolean mirror) {
+    }
+
     public void draw(Graphics2D graphics2d, AttributedCharacterIterator aci) {
         // NOP
     }