diff options
Diffstat (limited to 'src/java/org/apache/fop/svg')
12 files changed, 1028 insertions, 482 deletions
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 ? "<FEFF" : "("; String startText = useMultiByte ? "<" : "("; @@ -1524,6 +1480,7 @@ public class PDFGraphics2D extends AbstractGraphics2D { case '\\': currentStream.write("\\"); break; + default: } currentStream.write(ch); } @@ -1540,13 +1497,88 @@ public class PDFGraphics2D extends AbstractGraphics2D { } currentStream.write(endText); - currentStream.write("] TJ\n"); - currentStream.write("ET\n"); currentStream.write("Q\n"); } + /** + * Applies the given alpha values for filling and stroking. + * @param fillAlpha A value between 0 and 255 (=OPAQUE) for filling + * @param strokeAlpha A value between 0 and 255 (=OPAQUE) for stroking + */ + protected void applyAlpha(int fillAlpha, int strokeAlpha) { + if (fillAlpha != OPAQUE || strokeAlpha != OPAQUE) { + checkTransparencyAllowed(); + Map vals = new java.util.HashMap(); + if (fillAlpha != OPAQUE) { + vals.put(PDFGState.GSTATE_ALPHA_NONSTROKE, new Float(fillAlpha / 255f)); + } + if (strokeAlpha != OPAQUE) { + vals.put(PDFGState.GSTATE_ALPHA_STROKE, new Float(strokeAlpha / 255f)); + } + PDFGState gstate = pdfDoc.getFactory().makeGState( + vals, graphicsState.getGState()); + resourceContext.addGState(gstate); + currentStream.write("/" + gstate.getName() + " gs\n"); + } + } + + /** + * Updates the currently selected font. + * @param font the new font to use + */ + protected void updateCurrentFont(Font font) { + String name = font.getFontName(); + float size = (float)font.getFontSize() / 1000f; + + //Only update if necessary + if ((!name.equals(this.currentFontName)) + || (size != this.currentFontSize)) { + this.currentFontName = name; + this.currentFontSize = size; + currentStream.write("/" + name + " " + size + " Tf\n"); + } + } + + /** + * Returns a suitable internal font given an AWT Font instance. + * @param awtFont the AWT font + * @return the internal Font + */ + protected Font getInternalFontForAWTFont(java.awt.Font awtFont) { + Font fontState; + String n = awtFont.getFamily(); + if (n.equals("sanserif")) { + n = "sans-serif"; + } + float siz = awtFont.getSize2D(); + String style = awtFont.isItalic() ? "italic" : "normal"; + int weight = awtFont.isBold() ? Font.WEIGHT_BOLD : Font.WEIGHT_NORMAL; + FontTriplet triplet = fontInfo.fontLookup(n, style, weight); + fontState = fontInfo.getFontInstance(triplet, (int)(siz * 1000 + 0.5)); + return fontState; + } + + /** + * Determines whether the font with the given name is a multi-byte font. + * @param name the name of the font + * @return true if it's a multi-byte font + */ + protected boolean isMultiByteFont(String name) { + // This assumes that *all* CIDFonts use a /ToUnicode mapping + 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) { + return true; + } + } else if (f instanceof CIDFont) { + return true; + } + return false; + } + private void addKerning(StringWriter buf, Integer ch1, Integer ch2, Map kerning, String startText, String endText) { @@ -1699,16 +1731,7 @@ public class PDFGraphics2D extends AbstractGraphics2D { } } - if (c.getAlpha() != 255) { - checkTransparencyAllowed(); - Map vals = new java.util.HashMap(); - vals.put(PDFGState.GSTATE_ALPHA_NONSTROKE, - new Float(c.getAlpha() / 255f)); - PDFGState gstate = pdfDoc.getFactory().makeGState( - vals, graphicsState.getGState()); - resourceContext.addGState(gstate); - currentStream.write("/" + gstate.getName() + " gs\n"); - } + applyAlpha(c.getAlpha(), OPAQUE); c = getColor(); applyColor(c, true); diff --git a/src/java/org/apache/fop/svg/PDFSVGFlowRootElementBridge.java b/src/java/org/apache/fop/svg/PDFSVGFlowRootElementBridge.java new file mode 100644 index 000000000..ec6996389 --- /dev/null +++ b/src/java/org/apache/fop/svg/PDFSVGFlowRootElementBridge.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.bridge.svg12.SVGFlowRootElementBridge; +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 SVG 1.2 flow text, so those texts can be painted using + * PDF primitives. + */ +public class PDFSVGFlowRootElementBridge extends SVGFlowRootElementBridge { + + private PDFTextPainter textPainter; + + /** + * Main Constructor. + * @param fontInfo the font directory + */ + public PDFSVGFlowRootElementBridge(FontInfo fontInfo) { + this.textPainter = new PDFFlowTextPainter(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/PDFTextElementBridge.java b/src/java/org/apache/fop/svg/PDFTextElementBridge.java index 100711eb9..47e794dda 100644 --- a/src/java/org/apache/fop/svg/PDFTextElementBridge.java +++ b/src/java/org/apache/fop/svg/PDFTextElementBridge.java @@ -19,15 +19,13 @@ package org.apache.fop.svg; -import org.apache.batik.gvt.TextNode; -import org.apache.batik.bridge.SVGTextElementBridge; import org.apache.batik.bridge.BridgeContext; +import org.apache.batik.bridge.SVGTextElementBridge; import org.apache.batik.gvt.GraphicsNode; - +import org.apache.batik.gvt.TextNode; +import org.apache.batik.gvt.TextPainter; import org.apache.fop.fonts.FontInfo; - import org.w3c.dom.Element; -import org.w3c.dom.Node; /** * Bridge class for the <text> element. @@ -37,11 +35,12 @@ import org.w3c.dom.Node; * @author <a href="mailto:keiron@aftexsw.com">Keiron Liddle</a> */ 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 <tt>TextNode</tt>. @@ -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 <a href="mailto:keiron@aftexsw.com">Keiron Liddle</a> * @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; * <tt>KEY_USER_STYLESHEET_URI</tt> to fix the URI of a user * stylesheet, and <tt>KEY_PIXEL_TO_MM</tt> to specify the pixel to * millimeter conversion factor. + * + * <p><tt>KEY_AUTO_FONTS</tt> 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 <a href="mailto:keiron@aftexsw.com">Keiron Liddle</a> * @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 <tt>ImageTranscoder</tt>. + * Constructs a new <tt>PDFTranscoder</tt>. */ 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; + } + } |