blob: 6c009607bd4ad734d5069e332f5a1c69724f28fa [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.render.ps;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.Paint;
import java.awt.Shape;
import java.awt.Stroke;
import java.awt.geom.AffineTransform;
import java.awt.geom.Ellipse2D;
import java.awt.geom.GeneralPath;
import java.awt.geom.PathIterator;
import java.awt.geom.Point2D;
import java.io.IOException;
import java.text.AttributedCharacterIterator;
import java.util.Iterator;
import java.util.List;
import org.apache.batik.gvt.font.GVTGlyphVector;
import org.apache.batik.gvt.text.TextPaintInfo;
import org.apache.batik.gvt.text.TextSpanLayout;
import org.apache.xmlgraphics.java2d.ps.PSGraphics2D;
import org.apache.xmlgraphics.ps.PSGenerator;
import org.apache.xmlgraphics.ps.PSResource;
import org.apache.fop.fonts.Font;
import org.apache.fop.fonts.FontInfo;
import org.apache.fop.svg.NativeTextPainter;
import org.apache.fop.util.CharUtilities;
/**
* Renders the attributed character iterator of a text node.
* This class draws the text directly using PostScript text operators so
* the text is not drawn using shapes which makes the PS files larger.
* <p>
* The text runs are split into smaller text runs that can be bundles in single
* calls of the xshow, yshow or xyshow operators. For outline text, the charpath
* operator is used.
*/
public class PSTextPainter extends NativeTextPainter {
private static final boolean DEBUG = false;
private FontResourceCache fontResources;
private static final AffineTransform IDENTITY_TRANSFORM = new AffineTransform();
/**
* Create a new PS text painter with the given font information.
* @param fontInfo the font collection
*/
public PSTextPainter(FontInfo fontInfo) {
super(fontInfo);
this.fontResources = new FontResourceCache(fontInfo);
}
/** {@inheritDoc} */
protected boolean isSupported(Graphics2D g2d) {
return g2d instanceof PSGraphics2D;
}
/** {@inheritDoc} */
protected void paintTextRun(TextRun textRun, Graphics2D g2d) throws IOException {
AttributedCharacterIterator runaci = textRun.getACI();
runaci.first();
TextPaintInfo tpi = (TextPaintInfo)runaci.getAttribute(PAINT_INFO);
if (tpi == null || !tpi.visible) {
return;
}
if ((tpi != null) && (tpi.composite != null)) {
g2d.setComposite(tpi.composite);
}
//------------------------------------
TextSpanLayout layout = textRun.getLayout();
logTextRun(runaci, layout);
CharSequence chars = collectCharacters(runaci);
runaci.first(); //Reset ACI
final PSGraphics2D ps = (PSGraphics2D)g2d;
final PSGenerator gen = ps.getPSGenerator();
ps.preparePainting();
if (DEBUG) {
log.debug("Text: " + chars);
gen.commentln("%Text: " + chars);
}
GeneralPath debugShapes = null;
if (DEBUG) {
debugShapes = new GeneralPath();
}
TextUtil textUtil = new TextUtil(gen);
textUtil.setupFonts(runaci);
if (!textUtil.hasFonts()) {
//Draw using Java2D when no native fonts are available
textRun.getLayout().draw(g2d);
return;
}
gen.saveGraphicsState();
gen.concatMatrix(g2d.getTransform());
Shape imclip = g2d.getClip();
clip(ps, imclip);
gen.writeln("BT"); //beginTextObject()
AffineTransform localTransform = new AffineTransform();
Point2D prevPos = null;
GVTGlyphVector gv = layout.getGlyphVector();
PSTextRun psRun = new PSTextRun(); //Used to split a text run into smaller runs
for (int index = 0, c = gv.getNumGlyphs(); index < c; index++) {
char ch = chars.charAt(index);
boolean visibleChar = gv.isGlyphVisible(index)
|| (CharUtilities.isAnySpace(ch) && !CharUtilities.isZeroWidthSpace(ch));
logCharacter(ch, layout, index, visibleChar);
if (!visibleChar) {
continue;
}
Point2D glyphPos = gv.getGlyphPosition(index);
AffineTransform glyphTransform = gv.getGlyphTransform(index);
if (log.isTraceEnabled()) {
log.trace("pos " + glyphPos + ", transform " + glyphTransform);
}
if (DEBUG) {
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);
boolean flushCurrentRun = false;
//Try to optimize by combining characters using the same font and on the same line.
if (glyphTransform != null) {
//Happens for text-on-a-path
flushCurrentRun = true;
}
if (psRun.getRunLength() >= 128) {
//Don't let a run get too long
flushCurrentRun = true;
}
//Note the position of the glyph relative to the previous one
Point2D relPos;
if (prevPos == null) {
relPos = new Point2D.Double(0, 0);
} else {
relPos = new Point2D.Double(
glyphPos.getX() - prevPos.getX(),
glyphPos.getY() - prevPos.getY());
}
if (psRun.vertChanges == 0
&& psRun.getHorizRunLength() > 2
&& relPos.getY() != 0) {
//new line
flushCurrentRun = true;
}
//Select the actual character to paint
char paintChar = (CharUtilities.isAnySpace(ch) ? ' ' : ch);
//Select (sub)font for character
Font f = textUtil.selectFontForChar(paintChar);
char mapped = f.mapChar(ch);
boolean fontChanging = textUtil.isFontChanging(f, mapped);
if (fontChanging) {
flushCurrentRun = true;
}
if (flushCurrentRun) {
//Paint the current run and reset for the next run
psRun.paint(ps, textUtil, tpi);
psRun.reset();
}
//Track current run
psRun.addCharacter(paintChar, relPos);
psRun.noteStartingTransformation(localTransform);
//Change font if necessary
if (fontChanging) {
textUtil.setCurrentFont(f, mapped);
}
//Update last position
prevPos = glyphPos;
}
psRun.paint(ps, textUtil, tpi);
gen.writeln("ET"); //endTextObject()
gen.restoreGraphicsState();
if (DEBUG) {
//Paint debug shapes
g2d.setStroke(new BasicStroke(0));
g2d.setColor(Color.LIGHT_GRAY);
g2d.draw(debugShapes);
}
}
private void applyColor(Paint paint, final PSGenerator gen) throws IOException {
if (paint == null) {
return;
} else if (paint instanceof Color) {
Color col = (Color)paint;
gen.useColor(col);
} else {
log.warn("Paint not supported: " + paint.toString());
}
}
private PSResource getResourceForFont(Font f, String postfix) {
String key = (postfix != null ? f.getFontName() + '_' + postfix : f.getFontName());
return this.fontResources.getPSResourceForFontKey(key);
}
private void clip(PSGraphics2D ps, Shape shape) throws IOException {
if (shape == null) {
return;
}
ps.getPSGenerator().writeln("newpath");
PathIterator iter = shape.getPathIterator(IDENTITY_TRANSFORM);
ps.processPathIterator(iter);
ps.getPSGenerator().writeln("clip");
}
private class TextUtil {
private PSGenerator gen;
private Font[] fonts;
private Font currentFont;
private int currentEncoding = -1;
public TextUtil(PSGenerator gen) {
this.gen = gen;
}
public Font selectFontForChar(char ch) {
for (int i = 0, c = fonts.length; i < c; i++) {
if (fonts[i].hasChar(ch)) {
return fonts[i];
}
}
return fonts[0]; //TODO Maybe fall back to painting with shapes
}
public void writeTextMatrix(AffineTransform transform) throws IOException {
double[] matrix = new double[6];
transform.getMatrix(matrix);
gen.writeln(gen.formatDouble5(matrix[0]) + " "
+ gen.formatDouble5(matrix[1]) + " "
+ gen.formatDouble5(matrix[2]) + " "
+ gen.formatDouble5(matrix[3]) + " "
+ gen.formatDouble5(matrix[4]) + " "
+ gen.formatDouble5(matrix[5]) + " Tm");
}
public boolean isFontChanging(Font f, char mapped) {
if (f != getCurrentFont()) {
int encoding = mapped / 256;
if (encoding != getCurrentFontEncoding()) {
return true; //Font is changing
}
}
return false; //Font is the same
}
public void selectFont(Font f, char mapped) throws IOException {
int encoding = mapped / 256;
String postfix = (encoding == 0 ? null : Integer.toString(encoding));
PSResource res = getResourceForFont(f, postfix);
gen.useFont("/" + res.getName(), f.getFontSize() / 1000f);
gen.getResourceTracker().notifyResourceUsageOnPage(res);
}
public Font getCurrentFont() {
return this.currentFont;
}
public int getCurrentFontEncoding() {
return this.currentEncoding;
}
public void setCurrentFont(Font font, int encoding) {
this.currentFont = font;
this.currentEncoding = encoding;
}
public void setCurrentFont(Font font, char mapped) {
int encoding = mapped / 256;
setCurrentFont(font, encoding);
}
public void setupFonts(AttributedCharacterIterator runaci) {
this.fonts = findFonts(runaci);
}
public boolean hasFonts() {
return (fonts != null) && (fonts.length > 0);
}
}
private class PSTextRun {
private AffineTransform textTransform;
private List relativePositions = new java.util.LinkedList();
private StringBuffer currentChars = new StringBuffer();
private int horizChanges = 0;
private int vertChanges = 0;
public void reset() {
textTransform = null;
currentChars.setLength(0);
horizChanges = 0;
vertChanges = 0;
relativePositions.clear();
}
public int getHorizRunLength() {
if (this.vertChanges == 0
&& getRunLength() > 0) {
return getRunLength();
}
return 0;
}
public void addCharacter(char paintChar, Point2D relPos) {
addRelativePosition(relPos);
currentChars.append(paintChar);
}
private void addRelativePosition(Point2D relPos) {
if (getRunLength() > 0) {
if (relPos.getX() != 0) {
horizChanges++;
}
if (relPos.getY() != 0) {
vertChanges++;
}
}
relativePositions.add(relPos);
}
public void noteStartingTransformation(AffineTransform transform) {
if (textTransform == null) {
this.textTransform = new AffineTransform(transform);
}
}
public int getRunLength() {
return currentChars.length();
}
private boolean isXShow() {
return vertChanges == 0;
}
private boolean isYShow() {
return horizChanges == 0;
}
public void paint(PSGraphics2D g2d, TextUtil textUtil, TextPaintInfo tpi)
throws IOException {
if (getRunLength() > 0) {
if (log.isDebugEnabled()) {
log.debug("Text run: " + currentChars);
}
textUtil.writeTextMatrix(this.textTransform);
if (isXShow()) {
log.debug("Horizontal text: xshow");
paintXYShow(g2d, textUtil, tpi.fillPaint, true, false);
} else if (isYShow()) {
log.debug("Vertical text: yshow");
paintXYShow(g2d, textUtil, tpi.fillPaint, false, true);
} else {
log.debug("Arbitrary text: xyshow");
paintXYShow(g2d, textUtil, tpi.fillPaint, true, true);
}
boolean stroke = (tpi.strokePaint != null) && (tpi.strokeStroke != null);
if (stroke) {
log.debug("Stroked glyph outlines");
paintStrokedGlyphs(g2d, textUtil, tpi.strokePaint, tpi.strokeStroke);
}
}
}
private void paintXYShow(PSGraphics2D g2d, TextUtil textUtil, Paint paint,
boolean x, boolean y) throws IOException {
PSGenerator gen = textUtil.gen;
char firstChar = this.currentChars.charAt(0);
//Font only has to be setup up before the first character
Font f = textUtil.selectFontForChar(firstChar);
char mapped = f.mapChar(firstChar);
textUtil.selectFont(f, mapped);
textUtil.setCurrentFont(f, mapped);
applyColor(paint, gen);
StringBuffer sb = new StringBuffer();
sb.append('(');
for (int i = 0, c = this.currentChars.length(); i < c; i++) {
char ch = this.currentChars.charAt(i);
mapped = f.mapChar(ch);
PSGenerator.escapeChar(mapped, sb);
}
sb.append(')');
if (x || y) {
sb.append("\n[");
int idx = 0;
Iterator iter = this.relativePositions.iterator();
while (iter.hasNext()) {
Point2D pt = (Point2D)iter.next();
if (idx > 0) {
if (x) {
sb.append(format(gen, pt.getX()));
}
if (y) {
if (x) {
sb.append(' ');
}
sb.append(format(gen, -pt.getY()));
}
if (idx % 8 == 0) {
sb.append('\n');
} else {
sb.append(' ');
}
}
idx++;
}
if (x) {
sb.append('0');
}
if (y) {
if (x) {
sb.append(' ');
}
sb.append('0');
}
sb.append(']');
}
sb.append(' ');
if (x) {
sb.append('x');
}
if (y) {
sb.append('y');
}
sb.append("show"); // --> xshow, yshow or xyshow
gen.writeln(sb.toString());
}
private String format(PSGenerator gen, double coord) {
if (Math.abs(coord) < 0.00001) {
return "0";
} else {
return gen.formatDouble5(coord);
}
}
private void paintStrokedGlyphs(PSGraphics2D g2d, TextUtil textUtil,
Paint strokePaint, Stroke stroke) throws IOException {
PSGenerator gen = textUtil.gen;
applyColor(strokePaint, gen);
PSGraphics2D.applyStroke(stroke, gen);
Font f = null;
Iterator iter = this.relativePositions.iterator();
iter.next();
Point2D pos = new Point2D.Double(0, 0);
gen.writeln("0 0 M");
for (int i = 0, c = this.currentChars.length(); i < c; i++) {
char ch = this.currentChars.charAt(0);
if (i == 0) {
//Font only has to be setup up before the first character
f = textUtil.selectFontForChar(ch);
}
char mapped = f.mapChar(ch);
if (i == 0) {
textUtil.selectFont(f, mapped);
textUtil.setCurrentFont(f, mapped);
}
mapped = f.mapChar(this.currentChars.charAt(i));
//add glyph outlines to current path
char codepoint = (char)(mapped % 256);
gen.write("(" + codepoint + ")");
gen.writeln(" false charpath");
if (iter.hasNext()) {
//Position for the next character
Point2D pt = (Point2D)iter.next();
pos.setLocation(pos.getX() + pt.getX(), pos.getY() - pt.getY());
gen.writeln(gen.formatDouble5(pos.getX()) + " "
+ gen.formatDouble5(pos.getY()) + " M");
}
}
gen.writeln("stroke"); //paints all accumulated glyph outlines
}
}
}