From b4539a24234a1a79e64dcf8e360f67b6109283b9 Mon Sep 17 00:00:00 2001 From: Jeremias Maerki Date: Sat, 3 Nov 2007 10:59:09 +0000 Subject: [PATCH] Completely reimplemented the PDFTextPainter. All SVG text (including flow text, but excluding special cases with filters) is now painted in PDF text primitives. The whole thing compiles against and runs with Batik 1.6 but was developed against Batik Trunk (1.7). The TextBridge for SVG 1.2 text is omitted because we're still on Batik 1.6 and FOP wouldn't compile with it. The full feature set is only available with Batik 1.7, of course. With Batik 1.6, font selection may not work as expected. git-svn-id: https://svn.apache.org/repos/asf/xmlgraphics/fop/trunk@591587 13f79535-47bb-0310-9956-ffa450edef68 --- build.xml | 10 +- .../fop/render/ps/AbstractPSTranscoder.java | 13 - .../apache/fop/svg/AbstractFOPTranscoder.java | 31 +- .../svg/PDFBatikFlowTextElementBridge.java | 62 ++ .../org/apache/fop/svg/PDFBridgeContext.java | 42 +- .../PDFDocumentGraphics2DConfigurator.java | 9 +- .../apache/fop/svg/PDFFlowExtTextPainter.java | 49 ++ .../apache/fop/svg/PDFFlowTextPainter.java | 49 ++ .../org/apache/fop/svg/PDFGraphics2D.java | 193 +++--- .../fop/svg/PDFSVGFlowRootElementBridge.java | 62 ++ .../apache/fop/svg/PDFTextElementBridge.java | 76 +-- .../org/apache/fop/svg/PDFTextPainter.java | 568 ++++++++---------- src/java/org/apache/fop/svg/PDFTextUtil.java | 308 ++++++++++ .../org/apache/fop/svg/PDFTranscoder.java | 61 +- status.xml | 5 + 15 files changed, 1038 insertions(+), 500 deletions(-) create mode 100644 src/java/org/apache/fop/svg/PDFBatikFlowTextElementBridge.java create mode 100644 src/java/org/apache/fop/svg/PDFFlowExtTextPainter.java create mode 100644 src/java/org/apache/fop/svg/PDFFlowTextPainter.java create mode 100644 src/java/org/apache/fop/svg/PDFSVGFlowRootElementBridge.java create mode 100644 src/java/org/apache/fop/svg/PDFTextUtil.java diff --git a/build.xml b/build.xml index a753bfad1..d31f26e69 100644 --- a/build.xml +++ b/build.xml @@ -614,12 +614,14 @@ list of possible build targets. + + @@ -679,11 +681,9 @@ list of possible build targets. - - - - - + + + diff --git a/src/java/org/apache/fop/render/ps/AbstractPSTranscoder.java b/src/java/org/apache/fop/render/ps/AbstractPSTranscoder.java index e7ed181e4..e228b16bc 100644 --- a/src/java/org/apache/fop/render/ps/AbstractPSTranscoder.java +++ b/src/java/org/apache/fop/render/ps/AbstractPSTranscoder.java @@ -21,7 +21,6 @@ package org.apache.fop.render.ps; import java.awt.Color; - import java.io.IOException; import org.apache.avalon.framework.configuration.Configuration; @@ -29,15 +28,12 @@ import org.apache.batik.bridge.BridgeContext; import org.apache.batik.bridge.UnitProcessor; import org.apache.batik.transcoder.TranscoderException; import org.apache.batik.transcoder.TranscoderOutput; - import org.apache.batik.transcoder.image.ImageTranscoder; - import org.apache.fop.fonts.FontInfo; import org.apache.fop.fonts.FontSetup; import org.apache.fop.svg.AbstractFOPTranscoder; import org.apache.xmlgraphics.java2d.ps.AbstractPSDocumentGraphics2D; import org.apache.xmlgraphics.java2d.ps.TextHandler; - import org.w3c.dom.Document; import org.w3c.dom.svg.SVGLength; @@ -137,15 +133,6 @@ public abstract class AbstractPSTranscoder extends AbstractFOPTranscoder { } } - /** @return true if text should be stroked rather than painted using text operators */ - protected boolean isTextStroked() { - boolean stroke = false; - if (hints.containsKey(KEY_STROKE_TEXT)) { - stroke = ((Boolean)hints.get(KEY_STROKE_TEXT)).booleanValue(); - } - return stroke; - } - /** {@inheritDoc} */ protected BridgeContext createBridgeContext() { diff --git a/src/java/org/apache/fop/svg/AbstractFOPTranscoder.java b/src/java/org/apache/fop/svg/AbstractFOPTranscoder.java index 3a03db023..b7cee29ef 100644 --- a/src/java/org/apache/fop/svg/AbstractFOPTranscoder.java +++ b/src/java/org/apache/fop/svg/AbstractFOPTranscoder.java @@ -19,27 +19,25 @@ package org.apache.fop.svg; -import org.xml.sax.EntityResolver; - -import org.apache.commons.logging.impl.SimpleLog; -import org.apache.commons.logging.Log; import org.apache.batik.bridge.UserAgent; import org.apache.batik.dom.svg.SVGDOMImplementation; import org.apache.batik.dom.util.DocumentFactory; import org.apache.batik.transcoder.ErrorHandler; +import org.apache.batik.transcoder.SVGAbstractTranscoder; import org.apache.batik.transcoder.TranscoderException; import org.apache.batik.transcoder.TranscodingHints; -import org.apache.batik.transcoder.SVGAbstractTranscoder; import org.apache.batik.transcoder.image.ImageTranscoder; import org.apache.batik.transcoder.keys.BooleanKey; import org.apache.batik.util.SVGConstants; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.impl.SimpleLog; import org.w3c.dom.DOMImplementation; +import org.xml.sax.EntityResolver; /** * This is the common base class of all of FOP's transcoders. */ -public abstract class AbstractFOPTranscoder extends SVGAbstractTranscoder - { +public abstract class AbstractFOPTranscoder extends SVGAbstractTranscoder { /** * The key to specify whether to stroke text instead of using text @@ -81,6 +79,9 @@ public abstract class AbstractFOPTranscoder extends SVGAbstractTranscoder return new FOPTranscoderUserAgent(); } + /** + * @param logger + */ public void setLogger(Log logger) { this.logger = logger; } @@ -125,6 +126,22 @@ public abstract class AbstractFOPTranscoder extends SVGAbstractTranscoder return factory; } + /** + * Indicates whether text should be stroked rather than painted using text operators. Stroking + * text (also referred to as "painting as shapes") can used in situations where the quality of + * text output is not satisfying. The downside of the work-around: The generated file will + * likely become bigger and you will lose copy/paste functionality for certain output formats + * such as PDF. + * @return true if text should be stroked rather than painted using text operators + */ + protected boolean isTextStroked() { + boolean stroke = false; + if (hints.containsKey(KEY_STROKE_TEXT)) { + stroke = ((Boolean)hints.get(KEY_STROKE_TEXT)).booleanValue(); + } + return stroke; + } + // -------------------------------------------------------------------- // FOP's default error handler (for transcoders) // -------------------------------------------------------------------- diff --git a/src/java/org/apache/fop/svg/PDFBatikFlowTextElementBridge.java b/src/java/org/apache/fop/svg/PDFBatikFlowTextElementBridge.java new file mode 100644 index 000000000..748e216a7 --- /dev/null +++ b/src/java/org/apache/fop/svg/PDFBatikFlowTextElementBridge.java @@ -0,0 +1,62 @@ +/* + * 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 org.apache.batik.extension.svg.BatikFlowTextElementBridge; +import org.apache.batik.gvt.GraphicsNode; +import org.apache.batik.gvt.TextNode; +import org.apache.batik.gvt.TextPainter; +import org.apache.fop.fonts.FontInfo; + +/** + * Element Bridge for Batik's flow text extension, so those texts can be painted using + * PDF primitives. + */ +public class PDFBatikFlowTextElementBridge extends BatikFlowTextElementBridge { + + private PDFTextPainter textPainter; + + /** + * Main Constructor. + * @param fontInfo the font directory + */ + public PDFBatikFlowTextElementBridge(FontInfo fontInfo) { + this.textPainter = new PDFFlowExtTextPainter(fontInfo); + } + + /** {@inheritDoc} */ + protected GraphicsNode instantiateGraphicsNode() { + GraphicsNode node = super.instantiateGraphicsNode(); + if (node != null) { + //Set our own text painter + ((TextNode)node).setTextPainter(getTextPainter()); + } + return node; + } + + /** + * Returns the text painter used by this bridge. + * @return the text painter + */ + public TextPainter getTextPainter() { + return this.textPainter; + } + +} diff --git a/src/java/org/apache/fop/svg/PDFBridgeContext.java b/src/java/org/apache/fop/svg/PDFBridgeContext.java index 3353bc0f2..3ffad3335 100644 --- a/src/java/org/apache/fop/svg/PDFBridgeContext.java +++ b/src/java/org/apache/fop/svg/PDFBridgeContext.java @@ -20,7 +20,9 @@ package org.apache.fop.svg; import java.awt.geom.AffineTransform; +import java.lang.reflect.Constructor; +import org.apache.batik.bridge.Bridge; import org.apache.batik.bridge.BridgeContext; import org.apache.batik.bridge.DocumentLoader; import org.apache.batik.bridge.UserAgent; @@ -62,8 +64,9 @@ public class PDFBridgeContext extends BridgeContext { * @param linkTransform AffineTransform to properly place links, * may be null */ - public PDFBridgeContext(UserAgent userAgent, FontInfo fontInfo, - AffineTransform linkTransform) { + public PDFBridgeContext(UserAgent userAgent, + FontInfo fontInfo, + AffineTransform linkTransform) { super(userAgent); this.fontInfo = fontInfo; this.linkTransform = linkTransform; @@ -79,12 +82,43 @@ public class PDFBridgeContext extends BridgeContext { this(userAgent, fontInfo, null); } + private void putPDFElementBridgeConditional(String className, String testFor) { + try { + Class.forName(testFor); + //if we get here the test class is available + + Class clazz = Class.forName(className); + Constructor constructor = clazz.getConstructor(new Class[] {FontInfo.class}); + putBridge((Bridge)constructor.newInstance(new Object[] {fontInfo})); + } catch (Throwable t) { + //simply ignore (bridges instantiated over this method are optional) + } + } + /** {@inheritDoc} */ public void registerSVGBridges() { super.registerSVGBridges(); if (fontInfo != null) { - putBridge(new PDFTextElementBridge(fontInfo)); + PDFTextElementBridge textElementBridge = new PDFTextElementBridge(fontInfo); + putBridge(textElementBridge); + + //Batik flow text extension (may not always be available) + //putBridge(new PDFBatikFlowTextElementBridge(fontInfo); + putPDFElementBridgeConditional( + "org.apache.fop.svg.PDFBatikFlowTextElementBridge", + "org.apache.batik.extension.svg.BatikFlowTextElementBridge"); + + //SVG 1.2 flow text support + //putBridge(new PDFSVG12TextElementBridge(fontInfo)); //-->Batik 1.7 + putPDFElementBridgeConditional( + "org.apache.fop.svg.PDFSVG12TextElementBridge", + "org.apache.batik.bridge.svg12.SVG12TextElementBridge"); + + //putBridge(new PDFSVGFlowRootElementBridge(fontInfo)); + putPDFElementBridgeConditional( + "org.apache.fop.svg.PDFSVGFlowRootElementBridge", + "org.apache.batik.bridge.svg12.SVGFlowRootElementBridge"); } PDFAElementBridge pdfAElementBridge = new PDFAElementBridge(); @@ -99,8 +133,10 @@ public class PDFBridgeContext extends BridgeContext { } // Make sure any 'sub bridge contexts' also have our bridges. + //TODO There's no matching method in the super-class here public BridgeContext createBridgeContext() { return new PDFBridgeContext(getUserAgent(), getDocumentLoader(), fontInfo, linkTransform); } + } diff --git a/src/java/org/apache/fop/svg/PDFDocumentGraphics2DConfigurator.java b/src/java/org/apache/fop/svg/PDFDocumentGraphics2DConfigurator.java index fe4d50a4a..f57e8cc58 100644 --- a/src/java/org/apache/fop/svg/PDFDocumentGraphics2DConfigurator.java +++ b/src/java/org/apache/fop/svg/PDFDocumentGraphics2DConfigurator.java @@ -24,6 +24,7 @@ import java.util.List; import org.apache.avalon.framework.configuration.Configuration; import org.apache.avalon.framework.configuration.ConfigurationException; import org.apache.fop.apps.FOPException; +import org.apache.fop.fonts.FontCache; import org.apache.fop.fonts.FontInfo; import org.apache.fop.fonts.FontResolver; import org.apache.fop.fonts.FontSetup; @@ -53,8 +54,14 @@ public class PDFDocumentGraphics2DConfigurator { //Fonts try { FontResolver fontResolver = FontSetup.createMinimalFontResolver(); + //TODO The following could be optimized by retaining the FontCache somewhere + FontCache fontCache = FontCache.load(); + if (fontCache == null) { + fontCache = new FontCache(); + } List fontList = PrintRendererConfigurator.buildFontListFromConfiguration( - cfg, null, fontResolver, false, null); + cfg, null, fontResolver, false, fontCache); + fontCache.save(); FontInfo fontInfo = new FontInfo(); FontSetup.setup(fontInfo, fontList, fontResolver); graphics.setFontInfo(fontInfo); diff --git a/src/java/org/apache/fop/svg/PDFFlowExtTextPainter.java b/src/java/org/apache/fop/svg/PDFFlowExtTextPainter.java new file mode 100644 index 000000000..0e8f47cfe --- /dev/null +++ b/src/java/org/apache/fop/svg/PDFFlowExtTextPainter.java @@ -0,0 +1,49 @@ +/* + * 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.text.AttributedCharacterIterator; +import java.util.List; + +import org.apache.batik.extension.svg.FlowExtTextPainter; +import org.apache.batik.gvt.TextNode; +import org.apache.fop.fonts.FontInfo; + +/** + * Text Painter for Batik's flow text extension. + */ +public class PDFFlowExtTextPainter extends PDFTextPainter { + + /** + * Main constructor + * @param fontInfo the font directory + */ + public PDFFlowExtTextPainter(FontInfo fontInfo) { + super(fontInfo); + } + + /** {@inheritDoc} */ + public List getTextRuns(TextNode node, AttributedCharacterIterator aci) { + //Text runs are delegated to the normal FlowExtTextPainter, we just paint the text. + FlowExtTextPainter delegate = (FlowExtTextPainter)FlowExtTextPainter.getInstance(); + return delegate.getTextRuns(node, aci); + } + +} diff --git a/src/java/org/apache/fop/svg/PDFFlowTextPainter.java b/src/java/org/apache/fop/svg/PDFFlowTextPainter.java new file mode 100644 index 000000000..eeef40da1 --- /dev/null +++ b/src/java/org/apache/fop/svg/PDFFlowTextPainter.java @@ -0,0 +1,49 @@ +/* + * 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.text.AttributedCharacterIterator; +import java.util.List; + +import org.apache.batik.gvt.TextNode; +import org.apache.batik.gvt.flow.FlowTextPainter; +import org.apache.fop.fonts.FontInfo; + +/** + * Text Painter for SVG 1.2 (flow) text. + */ +public class PDFFlowTextPainter extends PDFTextPainter { + + /** + * Main constructor + * @param fontInfo the font directory + */ + public PDFFlowTextPainter(FontInfo fontInfo) { + super(fontInfo); + } + + /** {@inheritDoc} */ + public List getTextRuns(TextNode node, AttributedCharacterIterator aci) { + //Text runs are delegated to the normal FlowTextPainter, we just paint the text. + FlowTextPainter delegate = (FlowTextPainter)FlowTextPainter.getInstance(); + return delegate.getTextRuns(node, aci); + } + +} diff --git a/src/java/org/apache/fop/svg/PDFGraphics2D.java b/src/java/org/apache/fop/svg/PDFGraphics2D.java index dc93c3371..2436e1a10 100644 --- a/src/java/org/apache/fop/svg/PDFGraphics2D.java +++ b/src/java/org/apache/fop/svg/PDFGraphics2D.java @@ -106,6 +106,9 @@ public class PDFGraphics2D extends AbstractGraphics2D { /** The number of decimal places. */ private static final int DEC = 8; + + /** Convenience constant for full opacity */ + static final int OPAQUE = 255; /** * the PDF Document being created @@ -619,7 +622,7 @@ public class PDFGraphics2D extends AbstractGraphics2D { Shape imclip = getClip(); writeClip(imclip); currentStream.write("" + width + " 0 0 " + (-height) + " " + x - + " " + (y + height) + " cm\n" + "/Im" + + " " + (y + height) + " cm\n" + imageInfo.getName() + " Do\nQ\n"); return true; } @@ -704,16 +707,7 @@ public class PDFGraphics2D extends AbstractGraphics2D { } } - if (c.getAlpha() != 255) { - checkTransparencyAllowed(); - Map vals = new java.util.HashMap(); - vals.put(PDFGState.GSTATE_ALPHA_STROKE, - new Float(c.getAlpha() / 255f)); - PDFGState gstate = pdfDoc.getFactory().makeGState( - vals, graphicsState.getGState()); - resourceContext.addGState(gstate); - currentStream.write("/" + gstate.getName() + " gs\n"); - } + applyAlpha(OPAQUE, c.getAlpha()); c = getColor(); applyColor(c, false); @@ -1054,12 +1048,12 @@ public class PDFGraphics2D extends AbstractGraphics2D { private boolean createPattern(PatternPaint pp, boolean fill) { preparePainting(); - FontInfo fontInfo = new FontInfo(); - FontSetup.setup(fontInfo, null, null); + FontInfo specialFontInfo = new FontInfo(); + FontSetup.setup(specialFontInfo, null, null); PDFResources res = pdfDoc.getFactory().makeResources(); PDFResourceContext context = new PDFResourceContext(res); - PDFGraphics2D pattGraphic = new PDFGraphics2D(textAsShapes, fontInfo, + PDFGraphics2D pattGraphic = new PDFGraphics2D(textAsShapes, specialFontInfo, pdfDoc, context, pageRef, "", 0); pattGraphic.setGraphicContext(new GraphicContext()); @@ -1125,7 +1119,7 @@ public class PDFGraphics2D extends AbstractGraphics2D { /** @todo see if pdfDoc and res can be linked here, (currently res <> PDFDocument's resources) so addFonts() can be moved to PDFDocument class */ - res.addFonts(pdfDoc, fontInfo); + res.addFonts(pdfDoc, specialFontInfo); PDFPattern myPat = pdfDoc.getFactory().makePattern( resourceContext, 1, res, 1, 1, bbox, @@ -1156,11 +1150,13 @@ public class PDFGraphics2D extends AbstractGraphics2D { Shape clip = getClip(); Rectangle2D usrClipBounds, usrBounds; usrBounds = shape.getBounds2D(); - usrClipBounds = clip.getBounds2D(); - if (!usrClipBounds.intersects(usrBounds)) { - return true; + if (clip != null) { + usrClipBounds = clip.getBounds2D(); + if (!usrClipBounds.intersects(usrBounds)) { + return true; + } + Rectangle2D.intersect(usrBounds, usrClipBounds, usrBounds); } - Rectangle2D.intersect(usrBounds, usrClipBounds, usrBounds); double usrX = usrBounds.getX(); double usrY = usrBounds.getY(); double usrW = usrBounds.getWidth(); @@ -1169,11 +1165,15 @@ public class PDFGraphics2D extends AbstractGraphics2D { Rectangle devShapeBounds, devClipBounds, devBounds; AffineTransform at = getTransform(); devShapeBounds = at.createTransformedShape(shape).getBounds(); - devClipBounds = at.createTransformedShape(clip).getBounds(); - if (!devClipBounds.intersects(devShapeBounds)) { - return true; + if (clip != null) { + devClipBounds = at.createTransformedShape(clip).getBounds(); + if (!devClipBounds.intersects(devShapeBounds)) { + return true; + } + devBounds = devShapeBounds.intersection(devClipBounds); + } else { + devBounds = devShapeBounds; } - devBounds = devShapeBounds.intersection(devClipBounds); int devX = devBounds.x; int devY = devBounds.y; int devW = devBounds.width; @@ -1416,69 +1416,25 @@ public class PDFGraphics2D extends AbstractGraphics2D { if (ovFontState == null) { java.awt.Font gFont = getFont(); fontTransform = gFont.getTransform(); - String n = gFont.getFamily(); - if (n.equals("sanserif")) { - n = "sans-serif"; - } - float siz = gFont.getSize2D(); - String style = gFont.isItalic() ? "italic" : "normal"; - int weight = gFont.isBold() ? Font.WEIGHT_BOLD : Font.WEIGHT_NORMAL; - FontTriplet triplet = fontInfo.fontLookup(n, style, weight); - fontState = fontInfo.getFontInstance(triplet, (int)(siz * 1000 + 0.5)); + fontState = getInternalFontForAWTFont(gFont); } else { fontState = fontInfo.getFontInstance( ovFontState.getFontTriplet(), ovFontState.getFontSize()); ovFontState = null; } - String name; - float size; - name = fontState.getFontName(); - size = (float)fontState.getFontSize() / 1000f; - - if ((!name.equals(this.currentFontName)) - || (size != this.currentFontSize)) { - this.currentFontName = name; - this.currentFontSize = size; - currentStream.write("/" + name + " " + size + " Tf\n"); - - } + updateCurrentFont(fontState); currentStream.write("q\n"); Color c = getColor(); applyColor(c, true); applyPaint(getPaint(), true); - int salpha = c.getAlpha(); + applyAlpha(c.getAlpha(), OPAQUE); - if (salpha != 255) { - checkTransparencyAllowed(); - Map vals = new java.util.HashMap(); - vals.put(PDFGState.GSTATE_ALPHA_NONSTROKE, new Float(salpha / 255f)); - PDFGState gstate = pdfDoc.getFactory().makeGState( - vals, graphicsState.getGState()); - resourceContext.addGState(gstate); - currentStream.write("/" + gstate.getName() + " gs\n"); - } - - Map kerning = null; - boolean kerningAvailable = false; + Map kerning = fontState.getKerning(); + boolean kerningAvailable = (kerning != null && !kerning.isEmpty()); - kerning = fontState.getKerning(); - if (kerning != null && !kerning.isEmpty()) { - kerningAvailable = true; - } - - // This assumes that *all* CIDFonts use a /ToUnicode mapping - boolean useMultiByte = false; - org.apache.fop.fonts.Typeface f = - (org.apache.fop.fonts.Typeface)fontInfo.getFonts().get(name); - if (f instanceof LazyFont) { - if (((LazyFont) f).getRealFont() instanceof CIDFont) { - useMultiByte = true; - } - } else if (f instanceof CIDFont) { - useMultiByte = true; - } + boolean useMultiByte = isMultiByteFont(currentFontName); // String startText = useMultiByte ? " element. @@ -37,11 +35,12 @@ import org.w3c.dom.Node; * @author Keiron Liddle */ public class PDFTextElementBridge extends SVGTextElementBridge { + private PDFTextPainter pdfTextPainter; /** * Constructs a new bridge for the <text> element. - * @param fi the font infomration + * @param fi the font information */ public PDFTextElementBridge(FontInfo fi) { pdfTextPainter = new PDFTextPainter(fi); @@ -56,71 +55,20 @@ public class PDFTextElementBridge extends SVGTextElementBridge { */ public GraphicsNode createGraphicsNode(BridgeContext ctx, Element e) { GraphicsNode node = super.createGraphicsNode(ctx, e); - if (node != null && isSimple(ctx, e, node)) { + if (node != null) { + //Set our own text painter ((TextNode)node).setTextPainter(getTextPainter()); } return node; } - private PDFTextPainter getTextPainter() { - return pdfTextPainter; - } - /** - * Check if text element contains simple text. - * This checks the children of the text element to determine - * if the text is simple. The text is simple if it can be rendered - * with basic text drawing algorithms. This means there are no - * alternate characters, the font is known and there are no effects - * applied to the text. - * - * @param ctx the bridge context - * @param element the svg text element - * @param node the graphics node - * @return true if this text is simple of false if it cannot be - * easily rendered using normal drawString on the PDFGraphics2D + * Returns the TextPainter instance used by this bridge. + * @return the text painter */ - private boolean isSimple(BridgeContext ctx, Element element, GraphicsNode node) { - /* I cannot find any reference that 36pt is the maximum font size in PDF. Tests show - * no such restriction (jeremias, 28.5.2007) - * - // Font size, in user space units. - float fs = TextUtilities.convertFontSize(element).floatValue(); - // PDF cannot display fonts over 36pt - if (fs > 36) { - return false; - } - */ - - Element nodeElement; - for (Node n = element.getFirstChild(); - n != null; - n = n.getNextSibling()) { - - switch (n.getNodeType()) { - case Node.ELEMENT_NODE: - - nodeElement = (Element)n; - - if (n.getLocalName().equals(SVG_TSPAN_TAG) - || n.getLocalName().equals(SVG_ALT_GLYPH_TAG)) { - return false; - } else if (n.getLocalName().equals(SVG_TEXT_PATH_TAG)) { - return false; - } else if (n.getLocalName().equals(SVG_TREF_TAG)) { - return false; - } - break; - case Node.TEXT_NODE: - case Node.CDATA_SECTION_NODE: - } - } - - /*if (CSSUtilities.convertFilter(element, node, ctx) != null) { - return false; - }*/ - - return true; + public TextPainter getTextPainter() { + return pdfTextPainter; } + } diff --git a/src/java/org/apache/fop/svg/PDFTextPainter.java b/src/java/org/apache/fop/svg/PDFTextPainter.java index f05d5ae4f..81fc107d4 100644 --- a/src/java/org/apache/fop/svg/PDFTextPainter.java +++ b/src/java/org/apache/fop/svg/PDFTextPainter.java @@ -19,32 +19,34 @@ package org.apache.fop.svg; +import java.awt.BasicStroke; +import java.awt.Color; import java.awt.Graphics2D; -import java.awt.geom.Point2D; -import java.awt.geom.Rectangle2D; -/* java.awt.Font is not imported to avoid confusion with - org.apache.fop.fonts.Font */ -import java.text.AttributedCharacterIterator; -import java.awt.font.TextAttribute; -import java.awt.Shape; import java.awt.Paint; +import java.awt.Shape; import java.awt.Stroke; -import java.awt.Color; -import java.util.List; +import java.awt.font.TextAttribute; +import java.awt.geom.AffineTransform; +import java.awt.geom.Ellipse2D; +import java.awt.geom.GeneralPath; +import java.awt.geom.Point2D; +import java.lang.reflect.Method; +import java.text.AttributedCharacterIterator; import java.util.Iterator; +import java.util.List; -import org.apache.batik.gvt.text.Mark; -import org.apache.batik.gvt.TextPainter; -import org.apache.batik.gvt.TextNode; -import org.apache.batik.gvt.text.GVTAttributedCharacterIterator; -import org.apache.batik.gvt.text.TextPaintInfo; -import org.apache.batik.gvt.font.GVTFontFamily; import org.apache.batik.bridge.SVGFontFamily; +import org.apache.batik.gvt.font.GVTFont; +import org.apache.batik.gvt.font.GVTFontFamily; +import org.apache.batik.gvt.font.GVTGlyphVector; import org.apache.batik.gvt.renderer.StrokingTextPainter; - +import org.apache.batik.gvt.text.GVTAttributedCharacterIterator; +import org.apache.batik.gvt.text.TextPaintInfo; +import org.apache.batik.gvt.text.TextSpanLayout; import org.apache.fop.fonts.Font; import org.apache.fop.fonts.FontInfo; import org.apache.fop.fonts.FontTriplet; +import org.apache.fop.util.CharUtilities; /** * Renders the attributed character iterator of a TextNode. @@ -54,108 +56,221 @@ import org.apache.fop.fonts.FontTriplet; * drawString. If the text is complex or the cannot be translated * into a simple drawString the StrokingTextPainter is used instead. * - * (todo) handle underline, overline and strikethrough - * (todo) use drawString(AttributedCharacterIterator iterator...) for some - * - * @author Keiron Liddle * @version $Id$ */ -public class PDFTextPainter implements TextPainter { - private FontInfo fontInfo; +public class PDFTextPainter extends StrokingTextPainter { - /** - * Use the stroking text painter to get the bounds and shape. - * Also used as a fallback to draw the string with strokes. - */ - protected static final TextPainter PROXY_PAINTER = - StrokingTextPainter.getInstance(); + private static final boolean DEBUG = true; + + private boolean strokeText = false; + private FontInfo fontInfo; /** * Create a new PDF text painter with the given font information. - * @param fi the fint info + * @param fi the font info */ public PDFTextPainter(FontInfo fi) { fontInfo = fi; } - /** - * Paints the specified attributed character iterator using the - * specified Graphics2D and context and font context. - * @param node the TextNode to paint - * @param g2d the Graphics2D to use - */ - public void paint(TextNode node, Graphics2D g2d) { - String txt = node.getText(); - Point2D loc = node.getLocation(); - - AttributedCharacterIterator aci = - node.getAttributedCharacterIterator(); - // reset position to start of char iterator - if (aci.getBeginIndex() == aci.getEndIndex()) { - return; - } - char ch = aci.first(); - if (ch == AttributedCharacterIterator.DONE) { - return; + /** {@inheritDoc} */ + protected void paintTextRuns(List textRuns, Graphics2D g2d) { + if (DEBUG) { + System.out.println("paintTextRuns: count = " + textRuns.size()); + //fontInfo.dumpAllTripletsToSystemOut(); } - TextNode.Anchor anchor; - anchor = (TextNode.Anchor) aci.getAttribute( - GVTAttributedCharacterIterator.TextAttribute.ANCHOR_TYPE); - - List gvtFonts; - gvtFonts = (List) aci.getAttribute( - GVTAttributedCharacterIterator.TextAttribute.GVT_FONT_FAMILIES); - - TextPaintInfo tpi = (TextPaintInfo) aci.getAttribute( - GVTAttributedCharacterIterator.TextAttribute.PAINT_INFO); - - if (tpi == null) { - return; - } - - Paint forg = tpi.fillPaint; - Paint strokePaint = tpi.strokePaint; - Float size = (Float) aci.getAttribute(TextAttribute.SIZE); - if (size == null) { + if (!(g2d instanceof PDFGraphics2D) || strokeText) { + super.paintTextRuns(textRuns, g2d); return; } - Stroke stroke = tpi.strokeStroke; - /* - Float xpos = (Float) aci.getAttribute( - GVTAttributedCharacterIterator.TextAttribute.X); - Float ypos = (Float) aci.getAttribute( - GVTAttributedCharacterIterator.TextAttribute.Y); - */ - - Float posture = (Float) aci.getAttribute(TextAttribute.POSTURE); - Float taWeight = (Float) aci.getAttribute(TextAttribute.WEIGHT); - - boolean useStrokePainter = false; + PDFGraphics2D pdf = (PDFGraphics2D)g2d; + PDFTextUtil textUtil = new PDFTextUtil(pdf); + for (int i = 0; i < textRuns.size(); i++) { + TextRun textRun = (TextRun)textRuns.get(i); + AttributedCharacterIterator runaci = textRun.getACI(); + runaci.first(); + + TextPaintInfo tpi = (TextPaintInfo)runaci.getAttribute(PAINT_INFO); + if (tpi == null || !tpi.visible) { + continue; + } + if ((tpi != null) && (tpi.composite != null)) { + g2d.setComposite(tpi.composite); + } + + //------------------------------------ + TextSpanLayout layout = textRun.getLayout(); + if (DEBUG) { + int charCount = runaci.getEndIndex() - runaci.getBeginIndex(); + System.out.println("================================================"); + System.out.println("New text run:"); + System.out.println("char count: " + charCount); + System.out.println("range: " + + runaci.getBeginIndex() + " - " + runaci.getEndIndex()); + System.out.println("glyph count: " + layout.getGlyphCount()); //=getNumGlyphs() + } + //Gather all characters of the run + StringBuffer chars = new StringBuffer(); + for (runaci.first(); runaci.getIndex() < runaci.getEndIndex();) { + chars.append(runaci.current()); + runaci.next(); + } + runaci.first(); + if (DEBUG) { + System.out.println("Text: " + chars); + pdf.currentStream.write("%Text: " + chars + "\n"); + } + + GeneralPath debugShapes = null; + if (DEBUG) { + debugShapes = new GeneralPath(); + } + + Font[] fonts = findFonts(runaci); + if (fonts == null || fonts.length == 0) { + //Draw using Java2D + textRun.getLayout().draw(g2d); + continue; + } + + textUtil.saveGraphicsState(); + textUtil.concatMatrixCurrentTransform(); + Shape imclip = g2d.getClip(); + pdf.writeClip(imclip); + + applyColorAndPaint(tpi, pdf); + + textUtil.beginTextObject(); + textUtil.setFonts(fonts); + textUtil.setTextRenderingMode(tpi.fillPaint != null, tpi.strokePaint != null, false); + + AffineTransform localTransform = new AffineTransform(); + Point2D prevPos = null; + double prevVisibleCharWidth = 0.0; + GVTGlyphVector gv = layout.getGlyphVector(); + 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)); + if (DEBUG) { + System.out.println("glyph " + index + + " -> " + layout.getGlyphIndex(index) + " => " + ch); + if (CharUtilities.isAnySpace(ch) && ch != 32) { + System.out.println("Space found: " + Integer.toHexString(ch)); + } + if (ch == CharUtilities.ZERO_WIDTH_JOINER) { + System.out.println("ZWJ found: " + Integer.toHexString(ch)); + } + if (ch == CharUtilities.SOFT_HYPHEN) { + System.out.println("Soft hyphen found: " + Integer.toHexString(ch)); + } + if (!visibleChar) { + System.out.println("Invisible glyph found: " + Integer.toHexString(ch)); + } + } + if (!visibleChar) { + continue; + } + Point2D p = gv.getGlyphPosition(index); + + AffineTransform glyphTransform = gv.getGlyphTransform(index); + //TODO Glyph transforms could be refined so not every char has to be painted + //with its own TJ command (stretch/squeeze case could be optimized) + if (DEBUG) { + System.out.println("pos " + p + ", transform " + glyphTransform); + Shape sh; + sh = gv.getGlyphLogicalBounds(index); + if (sh == null) { + sh = new Ellipse2D.Double(p.getX(), p.getY(), 2, 2); + } + debugShapes.append(sh, false); + } - if (forg instanceof Color) { - Color col = (Color) forg; - if (col.getAlpha() != 255) { - useStrokePainter = true; + //Exact position of the glyph + localTransform.setToIdentity(); + localTransform.translate(p.getX(), p.getY()); + if (glyphTransform != null) { + localTransform.concatenate(glyphTransform); + } + localTransform.scale(1, -1); + + boolean yPosChanged = (prevPos == null + || prevPos.getY() != p.getY() + || glyphTransform != null); + if (yPosChanged) { + if (index > 0) { + textUtil.writeTJ(); + textUtil.writeTextMatrix(localTransform); + } + } else { + double xdiff = p.getX() - prevPos.getX(); + //Width of previous character + Font font = textUtil.getCurrentFont(); + double cw = prevVisibleCharWidth; + double effxdiff = (1000 * xdiff) - cw; + if (effxdiff != 0) { + double adjust = (-effxdiff / font.getFontSize()); + textUtil.adjustGlyphTJ(adjust * 1000); + } + if (DEBUG) { + System.out.println("==> x diff: " + xdiff + ", " + effxdiff + + ", charWidth: " + cw); + } + } + Font f = textUtil.selectFontForChar(ch); + if (f != textUtil.getCurrentFont()) { + textUtil.writeTJ(); + textUtil.setCurrentFont(f); + textUtil.writeTf(f); + textUtil.writeTextMatrix(localTransform); + } + char paintChar = (CharUtilities.isAnySpace(ch) ? ' ' : ch); + textUtil.writeTJChar(paintChar); + + //Update last position + prevPos = p; + prevVisibleCharWidth = textUtil.getCurrentFont().getCharWidth(chars.charAt(index)); + } + textUtil.writeTJ(); + textUtil.endTextObject(); + textUtil.restoreGraphicsState(); + if (DEBUG) { + g2d.setStroke(new BasicStroke(0)); + g2d.setColor(Color.LIGHT_GRAY); + g2d.draw(debugShapes); } - g2d.setColor(col); } - g2d.setPaint(forg); - g2d.setStroke(stroke); + } - if (strokePaint != null) { - // need to draw using AttributedCharacterIterator - useStrokePainter = true; + private void applyColorAndPaint(TextPaintInfo tpi, PDFGraphics2D pdf) { + Paint fillPaint = tpi.fillPaint; + Paint strokePaint = tpi.strokePaint; + Stroke stroke = tpi.strokeStroke; + int fillAlpha = PDFGraphics2D.OPAQUE; + if (fillPaint instanceof Color) { + Color col = (Color)fillPaint; + pdf.applyColor(col, true); + fillAlpha = col.getAlpha(); } - - if (hasUnsupportedAttributes(aci)) { - useStrokePainter = true; + if (strokePaint instanceof Color) { + Color col = (Color)strokePaint; + pdf.applyColor(col, false); } - - // text contains unsupported information - if (useStrokePainter) { - PROXY_PAINTER.paint(node, g2d); - return; + pdf.applyPaint(fillPaint, true); + pdf.applyStroke(stroke); + if (strokePaint != null) { + pdf.applyPaint(strokePaint, false); } + pdf.applyAlpha(fillAlpha, PDFGraphics2D.OPAQUE); + } + + private Font[] findFonts(AttributedCharacterIterator aci) { + List fonts = new java.util.ArrayList(); + List gvtFonts = (List) aci.getAttribute( + GVTAttributedCharacterIterator.TextAttribute.GVT_FONT_FAMILIES); + Float posture = (Float) aci.getAttribute(TextAttribute.POSTURE); + Float taWeight = (Float) aci.getAttribute(TextAttribute.WEIGHT); + Float fontSize = (Float) aci.getAttribute(TextAttribute.SIZE); String style = ((posture != null) && (posture.floatValue() > 0.0)) ? "italic" : "normal"; @@ -163,236 +278,65 @@ public class PDFTextPainter implements TextPainter { && (taWeight.floatValue() > 1.0)) ? Font.WEIGHT_BOLD : Font.WEIGHT_NORMAL; - Font fontState = null; - FontInfo fi = fontInfo; - boolean found = false; String fontFamily = null; + + //GVT_FONT can sometimes be different from the fonts in GVT_FONT_FAMILIES + //or GVT_FONT_FAMILIES can even be empty and only GVT_FONT is set + /* The following code section is not available until Batik 1.7 is released. */ + GVTFont gvtFont = (GVTFont)aci.getAttribute( + GVTAttributedCharacterIterator.TextAttribute.GVT_FONT); + if (gvtFont != null) { + try { + Method method = gvtFont.getClass().getMethod("getFamilyName", null); + String gvtFontFamily = (String)method.invoke(gvtFont, null); + //TODO Uncomment the following line when Batik 1.7 is shipped with FOP + //String gvtFontFamily = gvtFont.getFamilyName(); //Not available in Batik 1.6 + if (DEBUG) { + System.out.print(gvtFontFamily + ", "); + } + if (fontInfo.hasFont(gvtFontFamily, style, weight)) { + FontTriplet triplet = fontInfo.fontLookup(gvtFontFamily, style, + weight); + int fsize = (int)(fontSize.floatValue() * 1000); + fonts.add(fontInfo.getFontInstance(triplet, fsize)); + } + } catch (Exception e) { + //Most likely NoSuchMethodError here when using Batik 1.6 + //Just skip this section in this case + } + } + if (gvtFonts != null) { Iterator i = gvtFonts.iterator(); while (i.hasNext()) { GVTFontFamily fam = (GVTFontFamily) i.next(); if (fam instanceof SVGFontFamily) { - PROXY_PAINTER.paint(node, g2d); - return; + return null; //Let Batik paint this text! } fontFamily = fam.getFamilyName(); - if (fi.hasFont(fontFamily, style, weight)) { + if (DEBUG) { + System.out.print(fontFamily + ", "); + } + if (fontInfo.hasFont(fontFamily, style, weight)) { FontTriplet triplet = fontInfo.fontLookup(fontFamily, style, weight); - int fsize = (int)(size.floatValue() * 1000); - fontState = fontInfo.getFontInstance(triplet, fsize); - found = true; - break; + int fsize = (int)(fontSize.floatValue() * 1000); + fonts.add(fontInfo.getFontInstance(triplet, fsize)); } } } - if (!found) { + if (fonts.size() == 0) { FontTriplet triplet = fontInfo.fontLookup("any", style, Font.WEIGHT_NORMAL); - int fsize = (int)(size.floatValue() * 1000); - fontState = fontInfo.getFontInstance(triplet, fsize); - } else { - if (g2d instanceof PDFGraphics2D) { - ((PDFGraphics2D) g2d).setOverrideFontState(fontState); - } - } - int fStyle = java.awt.Font.PLAIN; - if (weight == Font.WEIGHT_BOLD) { - if (style.equals("italic")) { - fStyle = java.awt.Font.BOLD | java.awt.Font.ITALIC; - } else { - fStyle = java.awt.Font.BOLD; + int fsize = (int)(fontSize.floatValue() * 1000); + fonts.add(fontInfo.getFontInstance(triplet, fsize)); + if (DEBUG) { + System.out.print("fallback to 'any' font"); } - } else { - if (style.equals("italic")) { - fStyle = java.awt.Font.ITALIC; - } else { - fStyle = java.awt.Font.PLAIN; - } - } - java.awt.Font font = new java.awt.Font(fontFamily, fStyle, - (int)(fontState.getFontSize() / 1000)); - - g2d.setFont(font); - - float advance = getStringWidth(txt, fontState); - float tx = 0; - if (anchor != null) { - switch (anchor.getType()) { - case TextNode.Anchor.ANCHOR_MIDDLE: - tx = -advance / 2; - break; - case TextNode.Anchor.ANCHOR_END: - tx = -advance; - } - } - g2d.drawString(txt, (float)(loc.getX() + tx), (float)(loc.getY())); - } - - private boolean hasUnsupportedAttributes(AttributedCharacterIterator aci) { - boolean hasunsupported = false; - Object letSpace = aci.getAttribute( - GVTAttributedCharacterIterator.TextAttribute.LETTER_SPACING); - if (letSpace != null) { - hasunsupported = true; - } - - Object wordSpace = aci.getAttribute( - GVTAttributedCharacterIterator.TextAttribute.WORD_SPACING); - if (wordSpace != null) { - hasunsupported = true; - } - - AttributedCharacterIterator.Attribute key; - key = GVTAttributedCharacterIterator.TextAttribute.WRITING_MODE; - Object writeMod = aci.getAttribute(key); - if (!GVTAttributedCharacterIterator.TextAttribute.WRITING_MODE_LTR.equals( - writeMod)) { - hasunsupported = true; } - - Object vertOr = aci.getAttribute( - GVTAttributedCharacterIterator.TextAttribute.VERTICAL_ORIENTATION); - if (GVTAttributedCharacterIterator.TextAttribute.ORIENTATION_ANGLE.equals( - vertOr)) { - hasunsupported = true; + if (DEBUG) { + System.out.println(); } - return hasunsupported; + return (Font[])fonts.toArray(new Font[fonts.size()]); } - - private float getStringWidth(String str, Font fontState) { - float wordWidth = 0; - float whitespaceWidth = fontState.getWidth(fontState.mapChar(' ')); - - for (int i = 0; i < str.length(); i++) { - float charWidth; - char c = str.charAt(i); - if (!((c == ' ') || (c == '\n') || (c == '\r') || (c == '\t'))) { - charWidth = fontState.getWidth(fontState.mapChar(c)); - if (charWidth <= 0) { - charWidth = whitespaceWidth; - } - } else { - charWidth = whitespaceWidth; - } - wordWidth += charWidth; - } - return wordWidth / 1000f; - } - - /** - * Get the outline shape of the text characters. - * This uses the StrokingTextPainter to get the outline - * shape since in theory it should be the same. - * - * @param node the text node - * @return the outline shape of the text characters - */ - public Shape getOutline(TextNode node) { - return PROXY_PAINTER.getOutline(node); - } - - /** - * Get the bounds. - * This uses the StrokingTextPainter to get the bounds - * since in theory it should be the same. - * - * @param node the text node - * @return the bounds of the text - */ - public Rectangle2D getBounds2D(TextNode node) { - return PROXY_PAINTER.getBounds2D(node); - } - - /** - * Get the geometry bounds. - * This uses the StrokingTextPainter to get the bounds - * since in theory it should be the same. - * @param node the text node - * @return the bounds of the text - */ - public Rectangle2D getGeometryBounds(TextNode node) { - return PROXY_PAINTER.getGeometryBounds(node); - } - - // Methods that have no purpose for PDF - - /** - * Get the mark. - * This does nothing since the output is pdf and not interactive. - * @param node the text node - * @param pos the position - * @param all select all - * @return null - */ - public Mark getMark(TextNode node, int pos, boolean all) { - return null; - } - - /** - * Select at. - * This does nothing since the output is pdf and not interactive. - * @param x the x position - * @param y the y position - * @param node the text node - * @return null - */ - public Mark selectAt(double x, double y, TextNode node) { - return null; - } - - /** - * Select to. - * This does nothing since the output is pdf and not interactive. - * @param x the x position - * @param y the y position - * @param beginMark the start mark - * @return null - */ - public Mark selectTo(double x, double y, Mark beginMark) { - return null; - } - - /** - * Selec first. - * This does nothing since the output is pdf and not interactive. - * @param node the text node - * @return null - */ - public Mark selectFirst(TextNode node) { - return null; - } - - /** - * Select last. - * This does nothing since the output is pdf and not interactive. - * @param node the text node - * @return null - */ - public Mark selectLast(TextNode node) { - return null; - } - - /** - * Get selected. - * This does nothing since the output is pdf and not interactive. - * @param start the start mark - * @param finish the finish mark - * @return null - */ - public int[] getSelected(Mark start, Mark finish) { - return null; - } - - /** - * Get the highlighted shape. - * This does nothing since the output is pdf and not interactive. - * @param beginMark the start mark - * @param endMark the end mark - * @return null - */ - public Shape getHighlightShape(Mark beginMark, Mark endMark) { - return null; - } - -} - + +} \ No newline at end of file diff --git a/src/java/org/apache/fop/svg/PDFTextUtil.java b/src/java/org/apache/fop/svg/PDFTextUtil.java new file mode 100644 index 000000000..0fb552026 --- /dev/null +++ b/src/java/org/apache/fop/svg/PDFTextUtil.java @@ -0,0 +1,308 @@ +/* + * 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.geom.AffineTransform; + +import org.apache.fop.fonts.Font; +import org.apache.fop.pdf.PDFNumber; +import org.apache.fop.pdf.PDFText; + +/** + * Utility class for generating PDF text objects. + */ +public class PDFTextUtil { + + /** The number of decimal places. */ + private static final int DEC = 8; + + /** PDF text rendering mode: Fill text */ + public static final int TR_FILL = 0; + /** PDF text rendering mode: Stroke text */ + public static final int TR_STROKE = 1; + /** PDF text rendering mode: Fill, then stroke text */ + public static final int TR_FILL_STROKE = 2; + /** PDF text rendering mode: Neither fill nor stroke text (invisible) */ + public static final int TR_INVISIBLE = 3; + /** PDF text rendering mode: Fill text and add to path for clipping */ + public static final int TR_FILL_CLIP = 4; + /** PDF text rendering mode: Stroke text and add to path for clipping */ + public static final int TR_STROKE_CLIP = 5; + /** PDF text rendering mode: Fill, then stroke text and add to path for clipping */ + public static final int TR_FILL_STROKE_CLIP = 6; + /** PDF text rendering mode: Add text to path for clipping */ + public static final int TR_CLIP = 7; + + + private PDFGraphics2D g2d; + private boolean inTextObject = false; + private Font[] fonts; + private Font font; + private String startText; + private String endText; + private boolean useMultiByte; + private StringBuffer bufTJ; + private int textRenderingMode = 0; + + /** + * Main constructor. + * @param g2d the PDFGraphics2D instance to work with + */ + public PDFTextUtil(PDFGraphics2D g2d) { + this.g2d = g2d; + } + + private void writeAffineTransform(AffineTransform at, StringBuffer sb) { + double[] lt = new double[6]; + at.getMatrix(lt); + sb.append(PDFNumber.doubleOut(lt[0], DEC)).append(" "); + sb.append(PDFNumber.doubleOut(lt[1], DEC)).append(" "); + sb.append(PDFNumber.doubleOut(lt[2], DEC)).append(" "); + sb.append(PDFNumber.doubleOut(lt[3], DEC)).append(" "); + sb.append(PDFNumber.doubleOut(lt[4], DEC)).append(" "); + sb.append(PDFNumber.doubleOut(lt[5], DEC)); + } + + private void writeChar(char ch, StringBuffer sb) { + if (!useMultiByte) { + if (ch > 127) { + sb.append("\\").append(Integer.toOctalString((int)ch)); + } else { + switch (ch) { + case '(': + case ')': + case '\\': + sb.append("\\"); + break; + default: + } + sb.append(ch); + } + } else { + sb.append(PDFText.toUnicodeHex(ch)); + } + } + + private void checkInTextObject() { + if (!inTextObject) { + throw new IllegalStateException("Not in text object"); + } + } + + /** + * Called when a new text object should be started. Be sure to call setFont() before + * issuing any text painting commands. + */ + public void beginTextObject() { + if (inTextObject) { + throw new IllegalStateException("Already in text object"); + } + g2d.currentStream.write("BT\n"); + this.inTextObject = true; + } + + /** + * Called when a text object should be ended. + */ + public void endTextObject() { + checkInTextObject(); + g2d.currentStream.write("ET\n"); + this.inTextObject = false; + initValues(); + } + + private void initValues() { + this.font = null; + this.textRenderingMode = TR_FILL; + } + + /** + * Creates a "q" command, pushing a copy of the entire graphics state onto the stack. + */ + public void saveGraphicsState() { + g2d.currentStream.write("q\n"); + } + + /** + * Creates a "Q" command, restoring the entire graphics state to its former value by popping + * it from the stack. + */ + public void restoreGraphicsState() { + g2d.currentStream.write("Q\n"); + } + + /** + * Creates a "cm" command using the current transformation as the matrix. + */ + public void concatMatrixCurrentTransform() { + StringBuffer sb = new StringBuffer(); + if (!g2d.getTransform().isIdentity()) { + writeAffineTransform(g2d.getTransform(), sb); + sb.append(" cm\n"); + } + g2d.currentStream.write(sb.toString()); + } + + /** + * Sets the current fonts for the text object. For every character, the suitable font will + * be selected. + * @param fonts the new fonts + */ + public void setFonts(Font[] fonts) { + this.fonts = fonts; + } + + /** + * Sets the current font for the text object. + * @param font the new font + */ + public void setFont(Font font) { + setFonts(new Font[] {font}); + } + + /** + * Returns the current font in use. + * @return the current font or null if no font is currently active. + */ + public Font getCurrentFont() { + return this.font; + } + + /** + * Sets the current font. + * @param f the new font to use + */ + public void setCurrentFont(Font f) { + this.font = f; + } + + /** + * Writes a "Tf" command, setting a new current font. + * @param f the font to select + */ + public void writeTf(Font f) { + checkInTextObject(); + String fontName = f.getFontName(); + float fontSize = (float)f.getFontSize() / 1000f; + g2d.currentStream.write("/" + fontName + " " + PDFNumber.doubleOut(fontSize) + " Tf\n"); + + this.useMultiByte = g2d.isMultiByteFont(fontName); + this.startText = useMultiByte ? "<" : "("; + this.endText = useMultiByte ? ">" : ")"; + } + + /** + * Sets the text rendering mode. + * @param mode the rendering mode (value 0 to 7, see PDF Spec, constants: TR_*) + */ + public void setTextRenderingMode(int mode) { + if (mode < 0 || mode > 7) { + throw new IllegalArgumentException( + "Illegal value for text rendering mode. Expected: 0-7"); + } + if (mode != this.textRenderingMode) { + this.textRenderingMode = mode; + g2d.currentStream.write(this.textRenderingMode + " Tr\n"); + } + } + + /** + * Sets the text rendering mode. + * @param fill true if the text should be filled + * @param stroke true if the text should be stroked + * @param addToClip true if the path should be added for clipping + */ + public void setTextRenderingMode(boolean fill, boolean stroke, boolean addToClip) { + int mode; + if (fill) { + mode = (stroke ? 2 : 0); + } else { + mode = (stroke ? 1 : 3); + } + if (addToClip) { + mode += 4; + } + setTextRenderingMode(mode); + } + + /** + * Writes a "Tm" command, setting a new text transformation matrix. + * @param localTransform the new text transformation matrix + */ + public void writeTextMatrix(AffineTransform localTransform) { + StringBuffer sb = new StringBuffer(); + writeAffineTransform(localTransform, sb); + sb.append(" Tm\n"); + g2d.currentStream.write(sb.toString()); + } + + /** + * Selects a font from the font list suitable to display the given character. + * @param ch the character + * @return the recommended Font to use + */ + 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 + } + + /** + * Writes a char to the "TJ-Buffer". + * @param ch the unmapped character + */ + public void writeTJChar(char ch) { + if (bufTJ == null) { + bufTJ = new StringBuffer(); + } + if (bufTJ.length() == 0) { + bufTJ.append("[").append(startText); + } + char mappedChar = font.mapChar(ch); + writeChar(mappedChar, bufTJ); + } + + /** + * Writes a glyph adjust value to the "TJ-Buffer". + * @param adjust the glyph adjust value in thousands of text unit space. + */ + public void adjustGlyphTJ(double adjust) { + bufTJ.append(endText).append(" "); + bufTJ.append(PDFNumber.doubleOut(adjust, DEC - 4)); + bufTJ.append(" "); + bufTJ.append(startText); + } + + /** + * Writes a "TJ" command, writing out the accumulated buffer with the characters and glyph + * positioning values. The buffer is reset afterwards. + */ + public void writeTJ() { + if (bufTJ != null && bufTJ.length() > 0) { + bufTJ.append(endText).append("] TJ\n"); + g2d.currentStream.write(bufTJ.toString()); + bufTJ.setLength(0); + } + } + +} diff --git a/src/java/org/apache/fop/svg/PDFTranscoder.java b/src/java/org/apache/fop/svg/PDFTranscoder.java index e213379cb..f0d40b574 100644 --- a/src/java/org/apache/fop/svg/PDFTranscoder.java +++ b/src/java/org/apache/fop/svg/PDFTranscoder.java @@ -25,6 +25,7 @@ import java.io.IOException; import org.apache.avalon.framework.configuration.Configurable; import org.apache.avalon.framework.configuration.Configuration; import org.apache.avalon.framework.configuration.ConfigurationException; +import org.apache.avalon.framework.configuration.DefaultConfiguration; import org.apache.batik.bridge.BridgeContext; import org.apache.batik.bridge.UnitProcessor; import org.apache.batik.bridge.UserAgent; @@ -33,10 +34,10 @@ import org.apache.batik.transcoder.TranscoderException; import org.apache.batik.transcoder.TranscoderOutput; import org.apache.batik.transcoder.TranscodingHints; import org.apache.batik.transcoder.image.ImageTranscoder; +import org.apache.batik.transcoder.keys.BooleanKey; import org.apache.batik.transcoder.keys.FloatKey; import org.apache.fop.Version; import org.apache.fop.fonts.FontInfo; -import org.apache.fop.fonts.FontSetup; import org.w3c.dom.Document; import org.w3c.dom.svg.SVGLength; @@ -63,6 +64,12 @@ import org.w3c.dom.svg.SVGLength; * KEY_USER_STYLESHEET_URI to fix the URI of a user * stylesheet, and KEY_PIXEL_TO_MM to specify the pixel to * millimeter conversion factor. + * + *

KEY_AUTO_FONTS to disable the auto-detection of fonts installed in the system. + * The PDF Transcoder cannot use AWT's font subsystem and that's why the fonts have to be + * configured differently. By default, font auto-detection is enabled to match the behaviour + * of the other transcoders, but this may be associated with a price in the form of a small + * performance penalty. If font auto-detection is not desired, it can be disable using this key. * * @author Keiron Liddle * @version $Id$ @@ -76,13 +83,20 @@ public class PDFTranscoder extends AbstractFOPTranscoder */ public static final TranscodingHints.Key KEY_DEVICE_RESOLUTION = new FloatKey(); + /** + * The key is used to specify whether the available fonts should be automatically + * detected. The alternative is to configure the transcoder manually using a configuration + * file. + */ + public static final TranscodingHints.Key KEY_AUTO_FONTS = new BooleanKey(); + private Configuration cfg = null; /** Graphics2D instance that is used to paint to */ protected PDFDocumentGraphics2D graphics = null; /** - * Constructs a new ImageTranscoder. + * Constructs a new PDFTranscoder. */ public PDFTranscoder() { super(); @@ -102,9 +116,7 @@ public class PDFTranscoder extends AbstractFOPTranscoder }; } - /** - * {@inheritDoc} - */ + /** {@inheritDoc} */ public void configure(Configuration cfg) throws ConfigurationException { this.cfg = cfg; } @@ -121,16 +133,35 @@ public class PDFTranscoder extends AbstractFOPTranscoder TranscoderOutput output) throws TranscoderException { - graphics = new PDFDocumentGraphics2D(); + graphics = new PDFDocumentGraphics2D(isTextStroked()); graphics.getPDFDocument().getInfo().setProducer("Apache FOP Version " + Version.getVersion() + ": PDF Transcoder for Batik"); try { - if (this.cfg != null) { + Configuration effCfg = this.cfg; + if (effCfg == null) { + //By default, enable font auto-detection if no cfg is given + boolean autoFonts = true; + if (hints.containsKey(KEY_AUTO_FONTS)) { + autoFonts = ((Boolean)hints.get(KEY_AUTO_FONTS)).booleanValue(); + } + if (autoFonts) { + DefaultConfiguration c = new DefaultConfiguration("pdf-transcoder"); + DefaultConfiguration fonts = new DefaultConfiguration("fonts"); + c.addChild(fonts); + DefaultConfiguration autodetect = new DefaultConfiguration("auto-detect"); + fonts.addChild(autodetect); + effCfg = c; + } + } + + if (effCfg != null) { PDFDocumentGraphics2DConfigurator configurator = new PDFDocumentGraphics2DConfigurator(); - configurator.configure(graphics, this.cfg); + configurator.configure(graphics, effCfg); + } else { + graphics.setupDefaultFontInfo(); } } catch (Exception e) { throw new TranscoderException( @@ -190,8 +221,18 @@ public class PDFTranscoder extends AbstractFOPTranscoder /** {@inheritDoc} */ protected BridgeContext createBridgeContext() { - BridgeContext ctx = new PDFBridgeContext(userAgent, graphics.getFontInfo()); - return ctx; + //For compatibility with Batik 1.6 + return createBridgeContext("1.x"); } + /** {@inheritDoc} */ + public BridgeContext createBridgeContext(String version) { + FontInfo fontInfo = graphics.getFontInfo(); + if (isTextStroked()) { + fontInfo = null; + } + BridgeContext ctx = new PDFBridgeContext(userAgent, fontInfo); + return ctx; + } + } diff --git a/status.xml b/status.xml index 8d9cedd75..56e72406d 100644 --- a/status.xml +++ b/status.xml @@ -28,6 +28,11 @@ + + PDF Transcoder (SVG) text painting has been completely rewritten. + Except for some special cases (with filters for example), all text + (including flow text) is now painted using PDF text operators. + Added support for ids on empty fo:inlines. -- 2.39.5