<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"/>
<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"/>
<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>
*/
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,
import java.awt.Color;
import java.nio.CharBuffer;
+import java.text.CharacterIterator;
+import java.text.StringCharacterIterator;
import java.util.NoSuchElementException;
import java.util.Stack;
/** 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;
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) {
package org.apache.fop.fonts;
-import java.util.Collections;
import java.util.List;
import org.apache.commons.logging.Log;
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 + "]"
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);
}
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;
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;
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;
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;
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);
- }
-
}
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 {
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);
}
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;
+ }
+ }
+
+
}
class FOPGVTGlyphVector implements GVTGlyphVector {
- protected final CharacterIterator charIter;
+ protected final TextFragment text;
- private final FOPGVTFont font;
+ protected final FOPGVTFont font;
private final int fontSize;
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();
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);
this.glyphTransforms = new AffineTransform[this.glyphs.length];
}
- protected void maybeReverse(GlyphMapping mapping) {
- }
-
private static class SVGTextFragment implements TextFragment {
private final CharacterIterator charIter;
private String language;
+ private int level = -1;
+
SVGTextFragment(CharacterIterator charIter) {
this.charIter = charIter;
if (charIter instanceof AttributedCharacterIterator) {
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()) {
}
}
+ public int getBidiLevel() {
+ return level;
+ }
+
public char charAt(int index) {
return charIter.setIndex(index - charIter.getBeginIndex());
}
return frc;
}
+ public void setGlyphCode(int glyphIndex, int glyphCode) {
+ glyphs[glyphIndex] = glyphCode;
+ }
+
public int getGlyphCode(int glyphIndex) {
return glyphs[glyphIndex];
}
return endGlyphIndex - startGlyphIndex + 1;
}
+ public boolean isReversed() {
+ return false;
+ }
+
+ public void maybeReverse(boolean mirror) {
+ }
+
public void draw(Graphics2D graphics2d, AttributedCharacterIterator aci) {
// NOP
}