blob: 983a184c44f8e115992165d07a0732dd2db10ec9 [file] [log] [blame]
/*
* 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.
*/
/* $Id$ */
package org.apache.fop.svg;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.Shape;
import java.awt.font.FontRenderContext;
import java.awt.geom.AffineTransform;
import java.awt.geom.Ellipse2D;
import java.awt.geom.GeneralPath;
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.FontFamilyResolver;
import org.apache.batik.bridge.GlyphLayout;
import org.apache.batik.bridge.SVGGVTFont;
import org.apache.batik.bridge.StrokingTextPainter;
import org.apache.batik.bridge.TextLayoutFactory;
import org.apache.batik.bridge.TextNode;
import org.apache.batik.bridge.TextSpanLayout;
import org.apache.batik.gvt.font.GVTGlyphVector;
import org.apache.batik.gvt.text.TextPaintInfo;
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.font.FOPGVTGlyphVector;
import org.apache.fop.svg.text.BidiAttributedCharacterIterator;
import org.apache.fop.svg.text.ComplexGlyphLayout;
import org.apache.fop.util.CharUtilities;
/**
* Abstract base class for text painters that use specialized text commands native to an output
* format to render text.
*/
public abstract class NativeTextPainter extends StrokingTextPainter {
/** the logger for this class */
protected static final Log log = LogFactory.getLog(NativeTextPainter.class);
/** the font collection */
protected final FontInfo fontInfo;
protected final FontFamilyResolver fontFamilyResolver;
protected Font font;
protected TextPaintInfo tpi;
/**
* Creates a new instance.
* @param fontInfo the font collection
*/
public NativeTextPainter(FontInfo fontInfo) {
this.fontInfo = fontInfo;
this.fontFamilyResolver = new FOPFontFamilyResolverImpl(fontInfo);
}
/**
* Indicates whether the given {@link Graphics2D} instance if compatible with this text painter
* implementation.
* @param g2d the instance to check
* @return true if the instance is compatible.
*/
protected abstract boolean isSupported(Graphics2D g2d);
/**
* Paints a single text run.
* @param textRun the text run
* @param g2d the target Graphics2D instance
* @throws IOException if an I/O error occurs while rendering the text
*/
protected final void paintTextRun(TextRun textRun, Graphics2D g2d) throws IOException {
logTextRun(textRun);
AttributedCharacterIterator runaci = textRun.getACI();
runaci.first();
tpi = (TextPaintInfo) runaci.getAttribute(PAINT_INFO);
if (tpi == null || !tpi.visible) {
return;
}
if (tpi.composite != null) {
g2d.setComposite(tpi.composite);
}
TextSpanLayout layout = textRun.getLayout();
GVTGlyphVector gv = layout.getGlyphVector();
if (!(gv.getFont() instanceof FOPGVTFont)) {
assert gv.getFont() == null || gv.getFont() instanceof SVGGVTFont;
//Draw using Java2D when no native fonts are available
textRun.getLayout().draw(g2d);
return;
} else {
GeneralPath debugShapes = log.isDebugEnabled() ? new GeneralPath() : null;
preparePainting(g2d);
saveGraphicsState();
setInitialTransform(g2d.getTransform());
clip(g2d.getClip());
beginTextObject();
writeGlyphs((FOPGVTGlyphVector) gv, debugShapes);
endTextObject();
restoreGraphicsState();
if (debugShapes != null) {
g2d.setStroke(new BasicStroke(0));
g2d.setColor(Color.LIGHT_GRAY);
g2d.draw(debugShapes);
}
}
}
protected void writeGlyphs(FOPGVTGlyphVector gv, GeneralPath debugShapes) throws IOException {
AffineTransform localTransform = new AffineTransform();
Point2D prevPos = null;
AffineTransform prevGlyphTransform = null;
font = ((FOPGVTFont) gv.getFont()).getFont();
for (int index = 0, c = gv.getNumGlyphs(); index < c; index++) {
if (!gv.isGlyphVisible(index)) {
continue;
}
Point2D glyphPos = gv.getGlyphPosition(index);
AffineTransform glyphTransform = gv.getGlyphTransform(index);
if (log.isTraceEnabled()) {
log.trace("pos " + glyphPos + ", transform " + glyphTransform);
}
if (debugShapes != null) {
Shape sh = gv.getGlyphLogicalBounds(index);
if (sh == null) {
sh = new Ellipse2D.Double(glyphPos.getX(), glyphPos.getY(), 2, 2);
}
debugShapes.append(sh, false);
}
//Exact position of the glyph
localTransform.setToIdentity();
localTransform.translate(glyphPos.getX(), glyphPos.getY());
if (glyphTransform != null) {
localTransform.concatenate(glyphTransform);
}
localTransform.scale(1, -1);
positionGlyph(prevPos, glyphPos, glyphTransform != null || prevGlyphTransform != null);
char glyph = (char) gv.getGlyphCode(index);
//Update last position
prevPos = glyphPos;
prevGlyphTransform = glyphTransform;
writeGlyph(glyph, localTransform);
}
}
@Override
protected void paintTextRuns(@SuppressWarnings("rawtypes") List textRuns, Graphics2D g2d) {
if (log.isTraceEnabled()) {
log.trace("paintTextRuns: count = " + textRuns.size());
}
if (!isSupported(g2d)) {
super.paintTextRuns(textRuns, g2d);
return;
}
for (int i = 0; i < textRuns.size(); i++) {
TextRun textRun = (TextRun) textRuns.get(i);
try {
paintTextRun(textRun, g2d);
} catch (IOException ioe) {
//No other possibility than to use a RuntimeException
throw new RuntimeException(ioe);
}
}
}
/**
* Collects all characters from an {@link AttributedCharacterIterator}.
* @param runaci the character iterator
* @return the characters
*/
protected CharSequence collectCharacters(AttributedCharacterIterator runaci) {
StringBuffer chars = new StringBuffer();
for (runaci.first(); runaci.getIndex() < runaci.getEndIndex();) {
chars.append(runaci.current());
runaci.next();
}
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 = (((Integer) nodeACI.getAttribute(WRITING_MODE)).intValue() == 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, 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;
protected abstract void restoreGraphicsState() throws IOException;
protected abstract void setInitialTransform(AffineTransform transform) throws IOException;
protected abstract void clip(Shape clip) throws IOException;
protected abstract void beginTextObject() throws IOException;
protected abstract void endTextObject() throws IOException;
protected abstract void positionGlyph(Point2D prevPos, Point2D glyphPos, boolean reposition);
protected abstract void writeGlyph(char glyph, AffineTransform transform) throws IOException;
/**
* @param runaci an attributed character iterator
* @param layout a text span layout
*/
protected final void logTextRun(TextRun textRun) {
AttributedCharacterIterator runaci = textRun.getACI();
TextSpanLayout layout = textRun.getLayout();
runaci.first();
if (log.isTraceEnabled()) {
int charCount = runaci.getEndIndex() - runaci.getBeginIndex();
log.trace("================================================");
log.trace("New text run:");
log.trace("char count: " + charCount);
log.trace("range: "
+ runaci.getBeginIndex() + " - " + runaci.getEndIndex());
log.trace("glyph count: " + layout.getGlyphCount()); //=getNumGlyphs()
}
}
/**
* @param ch a character
* @param layout a text span layout
* @param index an index
* @param visibleChar visible character flag
*/
protected final void logCharacter(char ch, TextSpanLayout layout, int index,
boolean visibleChar) {
if (log.isTraceEnabled()) {
log.trace("glyph " + index
+ " -> " + layout.getGlyphIndex(index) + " => " + ch);
if (CharUtilities.isAnySpace(ch) && ch != 32) {
log.trace("Space found: " + Integer.toHexString(ch));
} else if (ch == CharUtilities.ZERO_WIDTH_JOINER) {
log.trace("ZWJ found: " + Integer.toHexString(ch));
} else if (ch == CharUtilities.SOFT_HYPHEN) {
log.trace("Soft hyphen found: " + Integer.toHexString(ch));
}
if (!visibleChar) {
log.trace("Invisible glyph found: " + Integer.toHexString(ch));
}
}
}
@Override
protected FontFamilyResolver getFontFamilyResolver() {
return this.fontFamilyResolver;
}
private static final TextLayoutFactory COMPLEX_SCRIPT_TEXT_LAYOUT_FACTORY =
new TextLayoutFactory() {
public TextSpanLayout createTextLayout(AttributedCharacterIterator aci,
int [] charMap, Point2D offset, FontRenderContext frc) {
if (ComplexGlyphLayout.mayRequireComplexLayout(aci)) {
return new ComplexGlyphLayout(aci, charMap, offset, frc);
} else {
return new GlyphLayout(aci, charMap, offset, frc);
}
}
};
@Override
protected TextLayoutFactory getTextLayoutFactory() {
return COMPLEX_SCRIPT_TEXT_LAYOUT_FACTORY;
}
}