blob: 34f57e42d6ea941b6d816d836936381cab4ebdf6 [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.Color;
import java.awt.Dimension;
import java.awt.Paint;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.geom.AffineTransform;
import java.io.IOException;
import java.util.Map;
import org.w3c.dom.Document;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.xmlgraphics.image.loader.ImageException;
import org.apache.xmlgraphics.image.loader.ImageInfo;
import org.apache.xmlgraphics.image.loader.ImageProcessingHints;
import org.apache.xmlgraphics.image.loader.ImageSessionContext;
import org.apache.xmlgraphics.ps.PSGenerator;
import org.apache.xmlgraphics.ps.PSResource;
import org.apache.fop.fonts.Font;
import org.apache.fop.fonts.FontTriplet;
import org.apache.fop.fonts.LazyFont;
import org.apache.fop.fonts.MultiByteFont;
import org.apache.fop.fonts.SingleByteFont;
import org.apache.fop.fonts.Typeface;
import org.apache.fop.render.RenderingContext;
import org.apache.fop.render.intermediate.AbstractIFPainter;
import org.apache.fop.render.intermediate.BorderPainter;
import org.apache.fop.render.intermediate.GraphicsPainter;
import org.apache.fop.render.intermediate.IFException;
import org.apache.fop.render.intermediate.IFState;
import org.apache.fop.traits.BorderProps;
import org.apache.fop.traits.RuleStyle;
import org.apache.fop.util.CharUtilities;
import org.apache.fop.util.HexEncoder;
/**
* IFPainter implementation that produces PostScript.
*/
public class PSPainter extends AbstractIFPainter<PSDocumentHandler> {
/** logging instance */
private static Log log = LogFactory.getLog(PSPainter.class);
private final GraphicsPainter graphicsPainter;
private BorderPainter borderPainter;
private boolean inTextMode;
/**
* Default constructor.
* @param documentHandler the parent document handler
*/
public PSPainter(PSDocumentHandler documentHandler) {
this(documentHandler, IFState.create());
}
protected PSPainter(PSDocumentHandler documentHandler, IFState state) {
super(documentHandler);
this.graphicsPainter = new PSGraphicsPainter(getGenerator());
this.borderPainter = new BorderPainter(graphicsPainter);
this.state = state;
}
private PSGenerator getGenerator() {
return getDocumentHandler().getGenerator();
}
/** {@inheritDoc} */
public void startViewport(AffineTransform transform, Dimension size, Rectangle clipRect)
throws IFException {
try {
PSGenerator generator = getGenerator();
saveGraphicsState();
generator.concatMatrix(toPoints(transform));
} catch (IOException ioe) {
throw new IFException("I/O error in startViewport()", ioe);
}
if (clipRect != null) {
clipRect(clipRect);
}
}
/** {@inheritDoc} */
public void endViewport() throws IFException {
try {
restoreGraphicsState();
} catch (IOException ioe) {
throw new IFException("I/O error in endViewport()", ioe);
}
}
/** {@inheritDoc} */
public void startGroup(AffineTransform transform, String layer) throws IFException {
try {
PSGenerator generator = getGenerator();
saveGraphicsState();
generator.concatMatrix(toPoints(transform));
} catch (IOException ioe) {
throw new IFException("I/O error in startGroup()", ioe);
}
}
/** {@inheritDoc} */
public void endGroup() throws IFException {
try {
restoreGraphicsState();
} catch (IOException ioe) {
throw new IFException("I/O error in endGroup()", ioe);
}
}
/** {@inheritDoc} */
protected Map createDefaultImageProcessingHints(ImageSessionContext sessionContext) {
Map hints = super.createDefaultImageProcessingHints(sessionContext);
//PostScript doesn't support alpha channels
hints.put(ImageProcessingHints.TRANSPARENCY_INTENT,
ImageProcessingHints.TRANSPARENCY_INTENT_IGNORE);
//TODO We might want to support image masks in the future.
return hints;
}
/** {@inheritDoc} */
protected RenderingContext createRenderingContext() {
PSRenderingContext psContext = new PSRenderingContext(
getUserAgent(), getGenerator(), getFontInfo());
return psContext;
}
/** {@inheritDoc} */
protected void drawImageUsingImageHandler(ImageInfo info, Rectangle rect)
throws ImageException, IOException {
if (!getDocumentHandler().getPSUtil().isOptimizeResources()
|| PSImageUtils.isImageInlined(info,
(PSRenderingContext)createRenderingContext())) {
super.drawImageUsingImageHandler(info, rect);
} else {
if (log.isDebugEnabled()) {
log.debug("Image " + info + " is embedded as a form later");
}
//Don't load image at this time, just put a form placeholder in the stream
PSResource form = getDocumentHandler().getFormForImage(info.getOriginalURI());
PSImageUtils.drawForm(form, info, rect, getGenerator());
}
}
/** {@inheritDoc} */
public void drawImage(String uri, Rectangle rect) throws IFException {
try {
endTextObject();
} catch (IOException ioe) {
throw new IFException("I/O error in drawImage()", ioe);
}
drawImageUsingURI(uri, rect);
}
/** {@inheritDoc} */
public void drawImage(Document doc, Rectangle rect) throws IFException {
try {
endTextObject();
} catch (IOException ioe) {
throw new IFException("I/O error in drawImage()", ioe);
}
drawImageUsingDocument(doc, rect);
}
/** {@inheritDoc} */
public void clipRect(Rectangle rect) throws IFException {
try {
PSGenerator generator = getGenerator();
endTextObject();
generator.defineRect(rect.x / 1000.0, rect.y / 1000.0,
rect.width / 1000.0, rect.height / 1000.0);
generator.writeln(generator.mapCommand("clip") + " " + generator.mapCommand("newpath"));
} catch (IOException ioe) {
throw new IFException("I/O error in clipRect()", ioe);
}
}
/** {@inheritDoc} */
public void clipBackground(Rectangle rect,
BorderProps bpsBefore, BorderProps bpsAfter,
BorderProps bpsStart, BorderProps bpsEnd) throws IFException {
try {
borderPainter.clipBackground(rect,
bpsBefore, bpsAfter, bpsStart, bpsEnd);
} catch (IOException ioe) {
throw new IFException("I/O error while clipping background", ioe);
}
}
/** {@inheritDoc} */
public void fillRect(Rectangle rect, Paint fill) throws IFException {
if (fill == null) {
return;
}
if (rect.width != 0 && rect.height != 0) {
try {
endTextObject();
PSGenerator generator = getGenerator();
if (fill != null) {
if (fill instanceof Color) {
generator.useColor((Color)fill);
} else {
throw new UnsupportedOperationException("Non-Color paints NYI");
}
}
generator.defineRect(rect.x / 1000.0, rect.y / 1000.0,
rect.width / 1000.0, rect.height / 1000.0);
generator.writeln(generator.mapCommand("fill"));
} catch (IOException ioe) {
throw new IFException("I/O error in fillRect()", ioe);
}
}
}
/** {@inheritDoc} */
public void drawBorderRect(Rectangle rect, BorderProps top, BorderProps bottom,
BorderProps left, BorderProps right, Color innerBackgroundColor) throws IFException {
if (top != null || bottom != null || left != null || right != null) {
try {
endTextObject();
if (getDocumentHandler().getPSUtil().getRenderingMode() == PSRenderingMode.SIZE
&& hasOnlySolidBorders(top, bottom, left, right)) {
super.drawBorderRect(rect, top, bottom, left, right, innerBackgroundColor);
} else {
this.borderPainter.drawBorders(rect, top, bottom, left, right, innerBackgroundColor);
}
} catch (IOException ioe) {
throw new IFException("I/O error in drawBorderRect()", ioe);
}
}
}
/** {@inheritDoc} */
public void drawLine(Point start, Point end, int width, Color color, RuleStyle style)
throws IFException {
try {
endTextObject();
this.graphicsPainter.drawLine(start, end, width, color, style);
} catch (IOException ioe) {
throw new IFException("I/O error in drawLine()", ioe);
}
}
private Typeface getTypeface(String fontName) {
if (fontName == null) {
throw new NullPointerException("fontName must not be null");
}
Typeface tf = getFontInfo().getFonts().get(fontName);
if (tf instanceof LazyFont) {
tf = ((LazyFont)tf).getRealFont();
}
return tf;
}
/**
* Saves the graphics state of the rendering engine.
* @throws IOException if an I/O error occurs
*/
protected void saveGraphicsState() throws IOException {
endTextObject();
getGenerator().saveGraphicsState();
}
/**
* Restores the last graphics state of the rendering engine.
* @throws IOException if an I/O error occurs
*/
protected void restoreGraphicsState() throws IOException {
endTextObject();
getGenerator().restoreGraphicsState();
}
/**
* Indicates the beginning of a text object.
* @throws IOException if an I/O error occurs
*/
protected void beginTextObject() throws IOException {
if (!inTextMode) {
PSGenerator generator = getGenerator();
generator.saveGraphicsState();
generator.writeln("BT");
inTextMode = true;
}
}
/**
* Indicates the end of a text object.
* @throws IOException if an I/O error occurs
*/
protected void endTextObject() throws IOException {
if (inTextMode) {
inTextMode = false;
PSGenerator generator = getGenerator();
generator.writeln("ET");
generator.restoreGraphicsState();
}
}
private String formatMptAsPt(PSGenerator gen, int value) {
return gen.formatDouble(value / 1000.0);
}
/* Disabled: performance experiment (incomplete)
private static final String ZEROS = "0.00";
private String formatMptAsPt1(int value) {
String s = Integer.toString(value);
int len = s.length();
StringBuffer sb = new StringBuffer();
if (len < 4) {
sb.append(ZEROS.substring(0, 5 - len));
sb.append(s);
} else {
int dec = len - 3;
sb.append(s.substring(0, dec));
sb.append('.');
sb.append(s.substring(dec));
}
return sb.toString();
}*/
/** {@inheritDoc} */
public void drawText(int x, int y, int letterSpacing, int wordSpacing,
int[][] dp, String text) throws IFException {
try {
//Do not draw text if font-size is 0 as it creates an invalid PostScript file
if (state.getFontSize() == 0) {
return;
}
PSGenerator generator = getGenerator();
generator.useColor(state.getTextColor());
beginTextObject();
FontTriplet triplet = new FontTriplet(
state.getFontFamily(), state.getFontStyle(), state.getFontWeight());
//TODO Ignored: state.getFontVariant()
//TODO Opportunity for font caching if font state is more heavily used
String fontKey = getFontKey(triplet);
int sizeMillipoints = state.getFontSize();
// This assumes that *all* CIDFonts use a /ToUnicode mapping
Typeface tf = getTypeface(fontKey);
SingleByteFont singleByteFont = null;
if (tf instanceof SingleByteFont) {
singleByteFont = (SingleByteFont)tf;
}
Font font = getFontInfo().getFontInstance(triplet, sizeMillipoints);
PSFontResource res = getDocumentHandler().getPSResourceForFontKey(fontKey);
boolean otf = tf instanceof MultiByteFont && ((MultiByteFont)tf).isOTFFile();
useFont(fontKey, sizeMillipoints, otf);
if (dp != null && dp[0] != null) {
x += dp[0][0];
y -= dp[0][1];
}
generator.writeln("1 0 0 -1 " + formatMptAsPt(generator, x)
+ " " + formatMptAsPt(generator, y) + " Tm");
int textLen = text.length();
int start = 0;
if (singleByteFont != null) {
//Analyze string and split up in order to paint in different sub-fonts/encodings
int currentEncoding = -1;
for (int i = 0; i < textLen; i++) {
char c = text.charAt(i);
char mapped = tf.mapChar(c);
int encoding = mapped / 256;
if (currentEncoding != encoding) {
if (i > 0) {
writeText(text, start, i - start,
letterSpacing, wordSpacing, dp, font, tf, false);
}
if (encoding == 0) {
useFont(fontKey, sizeMillipoints, false);
} else {
useFont(fontKey + "_" + Integer.toString(encoding), sizeMillipoints, false);
}
currentEncoding = encoding;
start = i;
}
}
} else {
if (tf instanceof MultiByteFont && ((MultiByteFont)tf).isOTFFile()) {
//Analyze string and split up in order to paint in different sub-fonts/encodings
int curEncoding = 0;
for (int i = start; i < textLen; i++) {
char orgChar = text.charAt(i);
MultiByteFont mbFont = (MultiByteFont)tf;
mbFont.mapChar(orgChar);
int origGlyphIdx = mbFont.findGlyphIndex(orgChar);
int newGlyphIdx = mbFont.getUsedGlyphs().get(origGlyphIdx);
int encoding = newGlyphIdx / 256;
if (encoding != curEncoding) {
if (i != 0) {
writeText(text, start, i - start, letterSpacing, wordSpacing, dp, font, tf,
true);
start = i;
}
generator.useFont("/" + res.getName() + "." + encoding, sizeMillipoints / 1000f);
curEncoding = encoding;
}
}
} else {
useFont(fontKey, sizeMillipoints, false);
}
}
writeText(text, start, textLen - start, letterSpacing, wordSpacing, dp, font, tf,
tf instanceof MultiByteFont);
} catch (IOException ioe) {
throw new IFException("I/O error in drawText()", ioe);
}
}
private void writeText(String text, int start, int len,
int letterSpacing, int wordSpacing, int[][] dp,
Font font, Typeface tf, boolean multiByte) throws IOException {
PSGenerator generator = getGenerator();
int end = start + len;
int initialSize = len;
initialSize += initialSize / 2;
boolean hasLetterSpacing = (letterSpacing != 0);
boolean needTJ = false;
int lineStart = 0;
StringBuffer accText = new StringBuffer(initialSize);
StringBuffer sb = new StringBuffer(initialSize);
boolean isOTF = multiByte && ((MultiByteFont)tf).isOTFFile();
for (int i = start; i < end; i++) {
int orgChar = text.charAt(i);
int ch;
int cw;
int xGlyphAdjust = 0;
int yGlyphAdjust = 0;
if (CharUtilities.isFixedWidthSpace(orgChar)) {
//Fixed width space are rendered as spaces so copy/paste works in a reader
ch = font.mapChar(CharUtilities.SPACE);
cw = font.getCharWidth(orgChar);
xGlyphAdjust = font.getCharWidth(ch) - cw;
} else {
if ((wordSpacing != 0) && CharUtilities.isAdjustableSpace(orgChar)) {
xGlyphAdjust -= wordSpacing;
}
// surrogate pairs have to be merged in a single code point
if (CharUtilities.containsSurrogatePairAt(text, i)) {
orgChar = Character.toCodePoint((char) orgChar, text.charAt(++i));
}
ch = font.mapCodePoint(orgChar);
}
if (dp != null && i < dp.length && dp[i] != null) {
// get x advancement adjust
xGlyphAdjust -= dp[i][2] - dp[i][0];
yGlyphAdjust += dp[i][3] - dp[i][1];
}
if (dp != null && i < dp.length - 1 && dp[i + 1] != null) {
// get x placement adjust for next glyph
xGlyphAdjust -= dp[i + 1][0];
yGlyphAdjust += dp[i + 1][1];
}
if (!multiByte || isOTF) {
char codepoint = (char)(ch % 256);
if (isOTF) {
accText.append(HexEncoder.encode(codepoint, 2));
} else {
PSGenerator.escapeChar(codepoint, accText); //add character to accumulated text
}
} else {
accText.append(HexEncoder.encode(ch));
}
if (xGlyphAdjust != 0 || yGlyphAdjust != 0) {
needTJ = true;
if (sb.length() == 0) {
sb.append('['); //Need to start TJ
}
if (accText.length() > 0) {
if ((sb.length() - lineStart + accText.length()) > 200) {
sb.append(PSGenerator.LF);
lineStart = sb.length();
}
lineStart = writePostScriptString(sb, accText, multiByte, lineStart);
sb.append(' ');
accText.setLength(0); //reset accumulated text
}
if (yGlyphAdjust == 0) {
sb.append(Integer.toString(xGlyphAdjust)).append(' ');
} else {
sb.append('[');
sb.append(Integer.toString(yGlyphAdjust)).append(' ');
sb.append(Integer.toString(xGlyphAdjust)).append(']').append(' ');
}
}
}
if (needTJ) {
if (accText.length() > 0) {
if ((sb.length() - lineStart + accText.length()) > 200) {
sb.append(PSGenerator.LF);
}
writePostScriptString(sb, accText, multiByte);
}
if (hasLetterSpacing) {
sb.append("] " + formatMptAsPt(generator, letterSpacing) + " ATJ");
} else {
sb.append("] TJ");
}
} else {
writePostScriptString(sb, accText, multiByte);
if (hasLetterSpacing) {
StringBuffer spb = new StringBuffer();
spb.append(formatMptAsPt(generator, letterSpacing))
.append(" 0 ");
sb.insert(0, spb.toString());
sb.append(" " + generator.mapCommand("ashow"));
} else {
sb.append(" " + generator.mapCommand("show"));
}
}
generator.writeln(sb.toString());
}
private void writePostScriptString(StringBuffer buffer, StringBuffer string,
boolean multiByte) {
writePostScriptString(buffer, string, multiByte, 0);
}
private int writePostScriptString(StringBuffer buffer, StringBuffer string, boolean multiByte,
int lineStart) {
buffer.append(multiByte ? '<' : '(');
int l = string.length();
int index = 0;
int maxCol = 200;
buffer.append(string.substring(index, Math.min(index + maxCol, l)));
index += maxCol;
while (index < l) {
if (!multiByte) {
buffer.append('\\');
}
buffer.append(PSGenerator.LF);
lineStart = buffer.length();
buffer.append(string.substring(index, Math.min(index + maxCol, l)));
index += maxCol;
}
buffer.append(multiByte ? '>' : ')');
return lineStart;
}
private void useFont(String key, int size, boolean otf) throws IOException {
PSFontResource res = getDocumentHandler().getPSResourceForFontKey(key);
PSGenerator generator = getGenerator();
String name = "/" + res.getName();
if (otf) {
name += ".0";
}
generator.useFont(name, size / 1000f);
res.notifyResourceUsageOnPage(generator.getResourceTracker());
}
}