Browse Source

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
pull/28/head
Glenn Adams 9 years ago
parent
commit
8c1e3b4a89

+ 5
- 0
build.xml View File

@@ -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"/>

+ 4
- 0
findbugs-exclude.xml View File

@@ -54,6 +54,10 @@
<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>

BIN
lib/batik-all-trunk.jar View File


+ 16
- 1
src/java/org/apache/fop/complexscripts/util/CharMirror.java View 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,

+ 30
- 2
src/java/org/apache/fop/fo/FOText.java View 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) {

+ 0
- 25
src/java/org/apache/fop/fonts/GlyphMapping.java View 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 + "]"

+ 41
- 1
src/java/org/apache/fop/fonts/TextFragment.java View File

@@ -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);
}

+ 112
- 0
src/java/org/apache/fop/svg/NativeTextPainter.java View 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;

+ 0
- 11
src/java/org/apache/fop/svg/PDFTextPainter.java View 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);
}

}

+ 131
- 7
src/java/org/apache/fop/svg/font/ComplexGlyphVector.java View File

@@ -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<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;
}
}


}

+ 47
- 17
src/java/org/apache/fop/svg/font/FOPGVTGlyphVector.java View 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
}

Loading…
Cancel
Save