From: Glenn Adams Date: Sat, 23 Aug 2014 03:50:42 +0000 (+0000) Subject: FOP-2391: enable bidi processing of SVG text chunks X-Git-Tag: fop-2_0~72 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=65d3a38fcb19d7658f55ba181ee4ec7c7145f18f;p=xmlgraphics-fop.git FOP-2391: enable bidi processing of SVG text chunks git-svn-id: https://svn.apache.org/repos/asf/xmlgraphics/fop/trunk@1619960 13f79535-47bb-0310-9956-ffa450edef68 --- diff --git a/build.xml b/build.xml index 5c0434a90..9194d99a6 100644 --- a/build.xml +++ b/build.xml @@ -564,6 +564,9 @@ list of possible build targets. + + + @@ -573,7 +576,9 @@ list of possible build targets. + + diff --git a/findbugs-exclude.xml b/findbugs-exclude.xml index 246e7b031..5124f0079 100644 --- a/findbugs-exclude.xml +++ b/findbugs-exclude.xml @@ -54,6 +54,10 @@ + + + + diff --git a/lib/batik-all-trunk.jar b/lib/batik-all-trunk.jar index 347757f32..79293f70a 100644 Binary files a/lib/batik-all-trunk.jar and b/lib/batik-all-trunk.jar differ diff --git a/src/java/org/apache/fop/complexscripts/util/CharMirror.java b/src/java/org/apache/fop/complexscripts/util/CharMirror.java index 8de2c1fab..5e905931a 100644 --- a/src/java/org/apache/fop/complexscripts/util/CharMirror.java +++ b/src/java/org/apache/fop/complexscripts/util/CharMirror.java @@ -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, diff --git a/src/java/org/apache/fop/fo/FOText.java b/src/java/org/apache/fop/fo/FOText.java index 305e2db57..9f286d888 100644 --- a/src/java/org/apache/fop/fo/FOText.java +++ b/src/java/org/apache/fop/fo/FOText.java @@ -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 CharBuffer 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) { diff --git a/src/java/org/apache/fop/fonts/GlyphMapping.java b/src/java/org/apache/fop/fonts/GlyphMapping.java index f0fadaad4..8c80eeb7c 100644 --- a/src/java/org/apache/fop/fonts/GlyphMapping.java +++ b/src/java/org/apache/fop/fonts/GlyphMapping.java @@ -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 + "]" diff --git a/src/java/org/apache/fop/fonts/TextFragment.java b/src/java/org/apache/fop/fonts/TextFragment.java index ad72db8e0..8722ecf2e 100644 --- a/src/java/org/apache/fop/fonts/TextFragment.java +++ b/src/java/org/apache/fop/fonts/TextFragment.java @@ -19,13 +19,53 @@ 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); } diff --git a/src/java/org/apache/fop/svg/NativeTextPainter.java b/src/java/org/apache/fop/svg/NativeTextPainter.java index b5c7d7882..18c140163 100644 --- a/src/java/org/apache/fop/svg/NativeTextPainter.java +++ b/src/java/org/apache/fop/svg/NativeTextPainter.java @@ -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) 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) runs) { + r.maybeReverseGlyphs(mirror); + } + } + protected abstract void preparePainting(Graphics2D g2d); protected abstract void saveGraphicsState() throws IOException; diff --git a/src/java/org/apache/fop/svg/PDFTextPainter.java b/src/java/org/apache/fop/svg/PDFTextPainter.java index c3ff744d5..c5fa9f04e 100644 --- a/src/java/org/apache/fop/svg/PDFTextPainter.java +++ b/src/java/org/apache/fop/svg/PDFTextPainter.java @@ -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); - } - } diff --git a/src/java/org/apache/fop/svg/font/ComplexGlyphVector.java b/src/java/org/apache/fop/svg/font/ComplexGlyphVector.java index 55f2eb3f2..8fa705e4c 100644 --- a/src/java/org/apache/fop/svg/font/ComplexGlyphVector.java +++ b/src/java/org/apache/fop/svg/font/ComplexGlyphVector.java @@ -20,12 +20,18 @@ 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) 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; + } + } + + } diff --git a/src/java/org/apache/fop/svg/font/FOPGVTGlyphVector.java b/src/java/org/apache/fop/svg/font/FOPGVTGlyphVector.java index 40a1e8ad3..2b2115935 100644 --- a/src/java/org/apache/fop/svg/font/FOPGVTGlyphVector.java +++ b/src/java/org/apache/fop/svg/font/FOPGVTGlyphVector.java @@ -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 }