0001: /*
0002: * Licensed to the Apache Software Foundation (ASF) under one or more
0003: * contributor license agreements. See the NOTICE file distributed with
0004: * this work for additional information regarding copyright ownership.
0005: * The ASF licenses this file to You under the Apache License, Version 2.0
0006: * (the "License"); you may not use this file except in compliance with
0007: * the License. You may obtain a copy of the License at
0008: *
0009: * http://www.apache.org/licenses/LICENSE-2.0
0010: *
0011: * Unless required by applicable law or agreed to in writing, software
0012: * distributed under the License is distributed on an "AS IS" BASIS,
0013: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
0014: * See the License for the specific language governing permissions and
0015: * limitations under the License.
0016: */
0017:
0018: /* $Id: PDFRenderer.java 542237 2007-05-28 14:31:24Z jeremias $ */
0019:
0020: package org.apache.fop.render.pdf;
0021:
0022: // Java
0023: import java.io.IOException;
0024: import java.io.InputStream;
0025: import java.io.OutputStream;
0026: import java.net.URL;
0027: import java.awt.geom.Point2D;
0028: import java.awt.Color;
0029: import java.awt.color.ColorSpace;
0030: import java.awt.color.ICC_Profile;
0031: import java.awt.geom.Rectangle2D;
0032: import java.awt.geom.AffineTransform;
0033: import java.util.Iterator;
0034: import java.util.Map;
0035: import java.util.List;
0036:
0037: import javax.xml.transform.Source;
0038: import javax.xml.transform.stream.StreamSource;
0039:
0040: // XML
0041: import org.w3c.dom.Document;
0042:
0043: // Avalon
0044: import org.apache.commons.io.IOUtils;
0045:
0046: // FOP
0047: import org.apache.fop.apps.FOPException;
0048: import org.apache.fop.apps.FOUserAgent;
0049: import org.apache.fop.apps.MimeConstants;
0050: import org.apache.fop.area.Area;
0051: import org.apache.fop.area.Block;
0052: import org.apache.fop.area.CTM;
0053: import org.apache.fop.area.LineArea;
0054: import org.apache.fop.area.OffDocumentExtensionAttachment;
0055: import org.apache.fop.area.PageViewport;
0056: import org.apache.fop.area.RegionViewport;
0057: import org.apache.fop.area.Trait;
0058: import org.apache.fop.area.OffDocumentItem;
0059: import org.apache.fop.area.BookmarkData;
0060: import org.apache.fop.area.inline.AbstractTextArea;
0061: import org.apache.fop.area.inline.TextArea;
0062: import org.apache.fop.area.inline.Image;
0063: import org.apache.fop.area.inline.Leader;
0064: import org.apache.fop.area.inline.InlineArea;
0065: import org.apache.fop.area.inline.InlineParent;
0066: import org.apache.fop.area.inline.WordArea;
0067: import org.apache.fop.area.inline.SpaceArea;
0068: import org.apache.fop.fonts.Typeface;
0069: import org.apache.fop.fonts.Font;
0070: import org.apache.fop.image.FopImage;
0071: import org.apache.fop.image.ImageFactory;
0072: import org.apache.fop.image.XMLImage;
0073: import org.apache.fop.pdf.PDFAction;
0074: import org.apache.fop.pdf.PDFAMode;
0075: import org.apache.fop.pdf.PDFAnnotList;
0076: import org.apache.fop.pdf.PDFColor;
0077: import org.apache.fop.pdf.PDFConformanceException;
0078: import org.apache.fop.pdf.PDFDocument;
0079: import org.apache.fop.pdf.PDFEncryptionManager;
0080: import org.apache.fop.pdf.PDFEncryptionParams;
0081: import org.apache.fop.pdf.PDFFactory;
0082: import org.apache.fop.pdf.PDFFilterList;
0083: import org.apache.fop.pdf.PDFGoTo;
0084: import org.apache.fop.pdf.PDFICCBasedColorSpace;
0085: import org.apache.fop.pdf.PDFICCStream;
0086: import org.apache.fop.pdf.PDFInfo;
0087: import org.apache.fop.pdf.PDFLink;
0088: import org.apache.fop.pdf.PDFMetadata;
0089: import org.apache.fop.pdf.PDFNumber;
0090: import org.apache.fop.pdf.PDFOutline;
0091: import org.apache.fop.pdf.PDFOutputIntent;
0092: import org.apache.fop.pdf.PDFPage;
0093: import org.apache.fop.pdf.PDFResourceContext;
0094: import org.apache.fop.pdf.PDFResources;
0095: import org.apache.fop.pdf.PDFState;
0096: import org.apache.fop.pdf.PDFStream;
0097: import org.apache.fop.pdf.PDFText;
0098: import org.apache.fop.pdf.PDFXMode;
0099: import org.apache.fop.pdf.PDFXObject;
0100: import org.apache.fop.render.AbstractPathOrientedRenderer;
0101: import org.apache.fop.render.Graphics2DAdapter;
0102: import org.apache.fop.render.RendererContext;
0103: import org.apache.fop.util.CharUtilities;
0104: import org.apache.fop.util.ColorProfileUtil;
0105: import org.apache.fop.fo.Constants;
0106: import org.apache.fop.fo.extensions.ExtensionAttachment;
0107: import org.apache.fop.fo.extensions.xmp.XMPMetadata;
0108: import org.apache.xmlgraphics.xmp.Metadata;
0109: import org.apache.xmlgraphics.xmp.schemas.XMPBasicAdapter;
0110: import org.apache.xmlgraphics.xmp.schemas.XMPBasicSchema;
0111:
0112: import org.apache.fop.area.DestinationData;
0113:
0114: /**
0115: * Renderer that renders areas to PDF.
0116: */
0117: public class PDFRenderer extends AbstractPathOrientedRenderer {
0118:
0119: /**
0120: * The mime type for pdf
0121: */
0122: public static final String MIME_TYPE = MimeConstants.MIME_PDF;
0123:
0124: /** Normal PDF resolution (72dpi) */
0125: public static final int NORMAL_PDF_RESOLUTION = 72;
0126:
0127: /** PDF encryption parameter: all parameters as object, datatype: PDFEncryptionParams */
0128: public static final String ENCRYPTION_PARAMS = "encryption-params";
0129: /** PDF encryption parameter: user password, datatype: String */
0130: public static final String USER_PASSWORD = "user-password";
0131: /** PDF encryption parameter: owner password, datatype: String */
0132: public static final String OWNER_PASSWORD = "owner-password";
0133: /** PDF encryption parameter: Forbids printing, datatype: Boolean or "true"/"false" */
0134: public static final String NO_PRINT = "noprint";
0135: /** PDF encryption parameter: Forbids copying content, datatype: Boolean or "true"/"false" */
0136: public static final String NO_COPY_CONTENT = "nocopy";
0137: /** PDF encryption parameter: Forbids editing content, datatype: Boolean or "true"/"false" */
0138: public static final String NO_EDIT_CONTENT = "noedit";
0139: /** PDF encryption parameter: Forbids annotations, datatype: Boolean or "true"/"false" */
0140: public static final String NO_ANNOTATIONS = "noannotations";
0141: /** Rendering Options key for the PDF/A mode. */
0142: public static final String PDF_A_MODE = "pdf-a-mode";
0143: /** Rendering Options key for the PDF/X mode. */
0144: public static final String PDF_X_MODE = "pdf-x-mode";
0145: /** Rendering Options key for the ICC profile for the output intent. */
0146: public static final String KEY_OUTPUT_PROFILE = "output-profile";
0147:
0148: /** Controls whether comments are written to the PDF stream. */
0149: protected static final boolean WRITE_COMMENTS = true;
0150:
0151: /**
0152: * the PDF Document being created
0153: */
0154: protected PDFDocument pdfDoc;
0155:
0156: /** the PDF/A mode (Default: disabled) */
0157: protected PDFAMode pdfAMode = PDFAMode.DISABLED;
0158:
0159: /** the PDF/X mode (Default: disabled) */
0160: protected PDFXMode pdfXMode = PDFXMode.DISABLED;
0161:
0162: /**
0163: * Map of pages using the PageViewport as the key
0164: * this is used for prepared pages that cannot be immediately
0165: * rendered
0166: */
0167: protected Map pages = null;
0168:
0169: /**
0170: * Maps unique PageViewport key to PDF page reference
0171: */
0172: protected Map pageReferences = new java.util.HashMap();
0173:
0174: /**
0175: * Maps unique PageViewport key back to PageViewport itself
0176: */
0177: protected Map pvReferences = new java.util.HashMap();
0178:
0179: /**
0180: * Maps XSL-FO element IDs to their on-page XY-positions
0181: * Must be used in conjunction with the page reference to fully specify the PDFGoTo details
0182: */
0183: protected Map idPositions = new java.util.HashMap();
0184:
0185: /**
0186: * Maps XSL-FO element IDs to PDFGoTo objects targeting the corresponding areas
0187: * These objects may not all be fully filled in yet
0188: */
0189: protected Map idGoTos = new java.util.HashMap();
0190:
0191: /**
0192: * The PDFGoTos in idGoTos that are not complete yet
0193: */
0194: protected List unfinishedGoTos = new java.util.ArrayList();
0195: // can't use a Set because PDFGoTo.equals returns true if the target is the same,
0196: // even if the object number differs
0197:
0198: /**
0199: * The output stream to write the document to
0200: */
0201: protected OutputStream ostream;
0202:
0203: /**
0204: * the /Resources object of the PDF document being created
0205: */
0206: protected PDFResources pdfResources;
0207:
0208: /**
0209: * the current stream to add PDF commands to
0210: */
0211: protected PDFStream currentStream;
0212:
0213: /**
0214: * the current annotation list to add annotations to
0215: */
0216: protected PDFResourceContext currentContext = null;
0217:
0218: /**
0219: * the current page to add annotations to
0220: */
0221: protected PDFPage currentPage;
0222:
0223: /**
0224: * the current page's PDF reference string (to avoid numerous function calls)
0225: */
0226: protected String currentPageRef;
0227:
0228: /** the (optional) encryption parameters */
0229: protected PDFEncryptionParams encryptionParams;
0230:
0231: /** the ICC stream used as output profile by this document for PDF/A and PDF/X functionality. */
0232: protected PDFICCStream outputProfile;
0233: /** the ICC stream for the sRGB color space. */
0234: //protected PDFICCStream sRGBProfile;
0235: /** the default sRGB color space. */
0236: protected PDFICCBasedColorSpace sRGBColorSpace;
0237:
0238: /** Optional URI to an output profile to be used. */
0239: protected String outputProfileURI;
0240:
0241: /** drawing state */
0242: protected PDFState currentState = null;
0243:
0244: /** Name of currently selected font */
0245: protected String currentFontName = "";
0246: /** Size of currently selected font */
0247: protected int currentFontSize = 0;
0248: /** page height */
0249: protected int pageHeight;
0250:
0251: /** Registry of PDF filters */
0252: protected Map filterMap;
0253:
0254: /**
0255: * true if a BT command has been written.
0256: */
0257: protected boolean inTextMode = false;
0258:
0259: /**
0260: * create the PDF renderer
0261: */
0262: public PDFRenderer() {
0263: }
0264:
0265: private boolean booleanValueOf(Object obj) {
0266: if (obj instanceof Boolean) {
0267: return ((Boolean) obj).booleanValue();
0268: } else if (obj instanceof String) {
0269: return Boolean.valueOf((String) obj).booleanValue();
0270: } else {
0271: throw new IllegalArgumentException(
0272: "Boolean or \"true\" or \"false\" expected.");
0273: }
0274: }
0275:
0276: /**
0277: * @see org.apache.fop.render.Renderer#setUserAgent(FOUserAgent)
0278: */
0279: public void setUserAgent(FOUserAgent agent) {
0280: super .setUserAgent(agent);
0281: PDFEncryptionParams params = (PDFEncryptionParams) agent
0282: .getRendererOptions().get(ENCRYPTION_PARAMS);
0283: if (params != null) {
0284: this .encryptionParams = params; //overwrite if available
0285: }
0286: String pwd;
0287: pwd = (String) agent.getRendererOptions().get(USER_PASSWORD);
0288: if (pwd != null) {
0289: if (encryptionParams == null) {
0290: this .encryptionParams = new PDFEncryptionParams();
0291: }
0292: this .encryptionParams.setUserPassword(pwd);
0293: }
0294: pwd = (String) agent.getRendererOptions().get(OWNER_PASSWORD);
0295: if (pwd != null) {
0296: if (encryptionParams == null) {
0297: this .encryptionParams = new PDFEncryptionParams();
0298: }
0299: this .encryptionParams.setOwnerPassword(pwd);
0300: }
0301: Object setting;
0302: setting = agent.getRendererOptions().get(NO_PRINT);
0303: if (setting != null) {
0304: if (encryptionParams == null) {
0305: this .encryptionParams = new PDFEncryptionParams();
0306: }
0307: this .encryptionParams
0308: .setAllowPrint(!booleanValueOf(setting));
0309: }
0310: setting = agent.getRendererOptions().get(NO_COPY_CONTENT);
0311: if (setting != null) {
0312: if (encryptionParams == null) {
0313: this .encryptionParams = new PDFEncryptionParams();
0314: }
0315: this .encryptionParams
0316: .setAllowCopyContent(!booleanValueOf(setting));
0317: }
0318: setting = agent.getRendererOptions().get(NO_EDIT_CONTENT);
0319: if (setting != null) {
0320: if (encryptionParams == null) {
0321: this .encryptionParams = new PDFEncryptionParams();
0322: }
0323: this .encryptionParams
0324: .setAllowEditContent(!booleanValueOf(setting));
0325: }
0326: setting = agent.getRendererOptions().get(NO_ANNOTATIONS);
0327: if (setting != null) {
0328: if (encryptionParams == null) {
0329: this .encryptionParams = new PDFEncryptionParams();
0330: }
0331: this .encryptionParams
0332: .setAllowEditAnnotations(!booleanValueOf(setting));
0333: }
0334: String s = (String) agent.getRendererOptions().get(PDF_A_MODE);
0335: if (s != null) {
0336: this .pdfAMode = PDFAMode.valueOf(s);
0337: }
0338: s = (String) agent.getRendererOptions().get(PDF_X_MODE);
0339: if (s != null) {
0340: this .pdfXMode = PDFXMode.valueOf(s);
0341: }
0342: s = (String) agent.getRendererOptions().get(KEY_OUTPUT_PROFILE);
0343: if (s != null) {
0344: this .outputProfileURI = s;
0345: }
0346: }
0347:
0348: /**
0349: * @see org.apache.fop.render.Renderer#startRenderer(OutputStream)
0350: */
0351: public void startRenderer(OutputStream stream) throws IOException {
0352: if (userAgent == null) {
0353: throw new IllegalStateException(
0354: "UserAgent must be set before starting the renderer");
0355: }
0356: ostream = stream;
0357: this .pdfDoc = new PDFDocument(
0358: userAgent.getProducer() != null ? userAgent
0359: .getProducer() : "");
0360: this .pdfDoc.getProfile().setPDFAMode(this .pdfAMode);
0361: this .pdfDoc.getProfile().setPDFXMode(this .pdfXMode);
0362: this .pdfDoc.getInfo().setCreator(userAgent.getCreator());
0363: this .pdfDoc.getInfo().setCreationDate(
0364: userAgent.getCreationDate());
0365: this .pdfDoc.getInfo().setAuthor(userAgent.getAuthor());
0366: this .pdfDoc.getInfo().setTitle(userAgent.getTitle());
0367: this .pdfDoc.getInfo().setKeywords(userAgent.getKeywords());
0368: this .pdfDoc.setFilterMap(filterMap);
0369: this .pdfDoc.outputHeader(stream);
0370:
0371: //Setup encryption if necessary
0372: PDFEncryptionManager.setupPDFEncryption(encryptionParams,
0373: this .pdfDoc);
0374:
0375: addsRGBColorSpace();
0376: if (this .outputProfileURI != null) {
0377: addDefaultOutputProfile();
0378: }
0379: if (pdfXMode != PDFXMode.DISABLED) {
0380: log.debug(pdfXMode + " is active.");
0381: log
0382: .warn("Note: "
0383: + pdfXMode
0384: + " support is work-in-progress and not fully implemented, yet!");
0385: addPDFXOutputIntent();
0386: }
0387: if (pdfAMode.isPDFA1LevelB()) {
0388: log
0389: .debug("PDF/A is active. Conformance Level: "
0390: + pdfAMode);
0391: addPDFA1OutputIntent();
0392: }
0393:
0394: }
0395:
0396: private void addsRGBColorSpace() throws IOException {
0397: if (this .sRGBColorSpace != null) {
0398: return;
0399: }
0400: ICC_Profile profile;
0401: PDFICCStream sRGBProfile = pdfDoc.getFactory()
0402: .makePDFICCStream();
0403: InputStream in = PDFDocument.class
0404: .getResourceAsStream("sRGB Color Space Profile.icm");
0405: if (in != null) {
0406: try {
0407: profile = ICC_Profile.getInstance(in);
0408: } finally {
0409: IOUtils.closeQuietly(in);
0410: }
0411: } else {
0412: //Fallback: Use the sRGB profile from the JRE (about 140KB)
0413: profile = ICC_Profile.getInstance(ColorSpace.CS_sRGB);
0414: }
0415: sRGBProfile.setColorSpace(profile, null);
0416:
0417: //Map sRGB as default RGB profile for DeviceRGB
0418: this .sRGBColorSpace = pdfDoc
0419: .getFactory()
0420: .makeICCBasedColorSpace(null, "DefaultRGB", sRGBProfile);
0421: }
0422:
0423: private void addDefaultOutputProfile() throws IOException {
0424: if (this .outputProfile != null) {
0425: return;
0426: }
0427: ICC_Profile profile;
0428: InputStream in = null;
0429: if (this .outputProfileURI != null) {
0430: this .outputProfile = pdfDoc.getFactory().makePDFICCStream();
0431: Source src = userAgent.resolveURI(this .outputProfileURI);
0432: if (src == null) {
0433: throw new IOException("Output profile not found: "
0434: + this .outputProfileURI);
0435: }
0436: if (src instanceof StreamSource) {
0437: in = ((StreamSource) src).getInputStream();
0438: } else {
0439: in = new URL(src.getSystemId()).openStream();
0440: }
0441: try {
0442: profile = ICC_Profile.getInstance(in);
0443: } finally {
0444: IOUtils.closeQuietly(in);
0445: }
0446: this .outputProfile.setColorSpace(profile, null);
0447: } else {
0448: //Fall back to sRGB profile
0449: outputProfile = sRGBColorSpace.getICCStream();
0450: }
0451: }
0452:
0453: /**
0454: * Adds an OutputIntent to the PDF as mandated by PDF/A-1 when uncalibrated color spaces
0455: * are used (which is true if we use DeviceRGB to represent sRGB colors).
0456: * @throws IOException in case of an I/O problem
0457: */
0458: private void addPDFA1OutputIntent() throws IOException {
0459: addDefaultOutputProfile();
0460:
0461: String desc = ColorProfileUtil
0462: .getICCProfileDescription(this .outputProfile
0463: .getICCProfile());
0464: PDFOutputIntent outputIntent = pdfDoc.getFactory()
0465: .makeOutputIntent();
0466: outputIntent.setSubtype(PDFOutputIntent.GTS_PDFA1);
0467: outputIntent.setDestOutputProfile(this .outputProfile);
0468: outputIntent.setOutputConditionIdentifier(desc);
0469: outputIntent.setInfo(outputIntent
0470: .getOutputConditionIdentifier());
0471: pdfDoc.getRoot().addOutputIntent(outputIntent);
0472: }
0473:
0474: /**
0475: * Adds an OutputIntent to the PDF as mandated by PDF/X when uncalibrated color spaces
0476: * are used (which is true if we use DeviceRGB to represent sRGB colors).
0477: * @throws IOException in case of an I/O problem
0478: */
0479: private void addPDFXOutputIntent() throws IOException {
0480: addDefaultOutputProfile();
0481:
0482: String desc = ColorProfileUtil
0483: .getICCProfileDescription(this .outputProfile
0484: .getICCProfile());
0485: int deviceClass = this .outputProfile.getICCProfile()
0486: .getProfileClass();
0487: if (deviceClass != ICC_Profile.CLASS_OUTPUT) {
0488: throw new PDFConformanceException(
0489: pdfDoc.getProfile().getPDFXMode()
0490: + " requires that"
0491: + " the DestOutputProfile be an Output Device Profile. "
0492: + desc
0493: + " does not match that requirement.");
0494: }
0495: PDFOutputIntent outputIntent = pdfDoc.getFactory()
0496: .makeOutputIntent();
0497: outputIntent.setSubtype(PDFOutputIntent.GTS_PDFX);
0498: outputIntent.setDestOutputProfile(this .outputProfile);
0499: outputIntent.setOutputConditionIdentifier(desc);
0500: outputIntent.setInfo(outputIntent
0501: .getOutputConditionIdentifier());
0502: pdfDoc.getRoot().addOutputIntent(outputIntent);
0503: }
0504:
0505: /**
0506: * Checks if there are any unfinished PDFGoTos left in the list and resolves them
0507: * to a default position on the page. Logs a warning, as this should not happen.
0508: */
0509: protected void finishOpenGoTos() {
0510: int count = unfinishedGoTos.size();
0511: if (count > 0) {
0512: // TODO : page height may not be the same for all targeted pages
0513: Point2D.Float defaultPos = new Point2D.Float(0f,
0514: pageHeight / 1000f); // top-o-page
0515: while (!unfinishedGoTos.isEmpty()) {
0516: PDFGoTo gt = (PDFGoTo) unfinishedGoTos.get(0);
0517: finishIDGoTo(gt, defaultPos);
0518: }
0519: boolean one = count == 1;
0520: String pl = one ? "" : "s";
0521: String ww = one ? "was" : "were";
0522: String ia = one ? "is" : "are";
0523: log.warn("" + count + " link target" + pl
0524: + " could not be fully resolved and " + ww
0525: + " now point to the top of the page or " + ia
0526: + " dysfunctional."); // dysfunctional if pageref is null
0527: }
0528: }
0529:
0530: /**
0531: * @see org.apache.fop.render.Renderer#stopRenderer()
0532: */
0533: public void stopRenderer() throws IOException {
0534: finishOpenGoTos();
0535:
0536: pdfDoc.getResources().addFonts(pdfDoc, fontInfo);
0537: pdfDoc.outputTrailer(ostream);
0538:
0539: this .pdfDoc = null;
0540: ostream = null;
0541:
0542: pages = null;
0543:
0544: pageReferences.clear();
0545: pvReferences.clear();
0546: pdfResources = null;
0547: currentStream = null;
0548: currentContext = null;
0549: currentPage = null;
0550: currentState = null;
0551: currentFontName = "";
0552:
0553: idPositions.clear();
0554: idGoTos.clear();
0555: }
0556:
0557: /**
0558: * @see org.apache.fop.render.Renderer#supportsOutOfOrder()
0559: */
0560: public boolean supportsOutOfOrder() {
0561: //return false;
0562: return true;
0563: }
0564:
0565: /**
0566: * @see org.apache.fop.render.Renderer#processOffDocumentItem(OffDocumentItem)
0567: */
0568: public void processOffDocumentItem(OffDocumentItem odi) {
0569: if (odi instanceof DestinationData) {
0570: // render Destinations
0571: renderDestination((DestinationData) odi);
0572: } else if (odi instanceof BookmarkData) {
0573: // render Bookmark-Tree
0574: renderBookmarkTree((BookmarkData) odi);
0575: } else if (odi instanceof OffDocumentExtensionAttachment) {
0576: ExtensionAttachment attachment = ((OffDocumentExtensionAttachment) odi)
0577: .getAttachment();
0578: if (XMPMetadata.CATEGORY.equals(attachment.getCategory())) {
0579: renderXMPMetadata((XMPMetadata) attachment);
0580: }
0581: }
0582: }
0583:
0584: private void renderDestination(DestinationData dd) {
0585: String targetID = dd.getIDRef();
0586: if (targetID != null && targetID.length() > 0) {
0587: PageViewport pv = dd.getPageViewport();
0588: if (pv == null) {
0589: log.warn("Unresolved destination item received: "
0590: + dd.getIDRef());
0591: }
0592: PDFGoTo gt = getPDFGoToForID(targetID, pv.getKey());
0593: pdfDoc.getFactory().makeDestination(dd.getIDRef(),
0594: gt.makeReference());
0595: } else {
0596: log
0597: .warn("DestinationData item with null or empty IDRef received.");
0598: }
0599: }
0600:
0601: /**
0602: * Renders a Bookmark-Tree object
0603: * @param bookmarks the BookmarkData object containing all the Bookmark-Items
0604: */
0605: protected void renderBookmarkTree(BookmarkData bookmarks) {
0606: for (int i = 0; i < bookmarks.getCount(); i++) {
0607: BookmarkData ext = bookmarks.getSubData(i);
0608: renderBookmarkItem(ext, null);
0609: }
0610: }
0611:
0612: private void renderBookmarkItem(BookmarkData bookmarkItem,
0613: PDFOutline parentBookmarkItem) {
0614: PDFOutline pdfOutline = null;
0615:
0616: String targetID = bookmarkItem.getIDRef();
0617: if (targetID != null && targetID.length() > 0) {
0618: PageViewport pv = bookmarkItem.getPageViewport();
0619: if (pv != null) {
0620: String pvKey = pv.getKey();
0621: PDFGoTo gt = getPDFGoToForID(targetID, pvKey);
0622: // create outline object:
0623: PDFOutline parent = parentBookmarkItem != null ? parentBookmarkItem
0624: : pdfDoc.getOutlineRoot();
0625: pdfOutline = pdfDoc.getFactory().makeOutline(parent,
0626: bookmarkItem.getBookmarkTitle(), gt,
0627: bookmarkItem.showChildItems());
0628: } else {
0629: log.warn("Bookmark with IDRef \"" + targetID
0630: + "\" has a null PageViewport.");
0631: }
0632: } else {
0633: log
0634: .warn("Bookmark item with null or empty IDRef received.");
0635: }
0636:
0637: for (int i = 0; i < bookmarkItem.getCount(); i++) {
0638: renderBookmarkItem(bookmarkItem.getSubData(i), pdfOutline);
0639: }
0640: }
0641:
0642: private void renderXMPMetadata(XMPMetadata metadata) {
0643: Metadata docXMP = metadata.getMetadata();
0644: Metadata fopXMP = PDFMetadata.createXMPFromUserAgent(pdfDoc);
0645: //Merge FOP's own metadata into the one from the XSL-FO document
0646: fopXMP.mergeInto(docXMP);
0647: XMPBasicAdapter xmpBasic = XMPBasicSchema.getAdapter(docXMP);
0648: //Metadata was changed so update metadata date
0649: xmpBasic.setMetadataDate(new java.util.Date());
0650: PDFMetadata.updateInfoFromMetadata(docXMP, pdfDoc.getInfo());
0651:
0652: PDFMetadata pdfMetadata = pdfDoc.getFactory().makeMetadata(
0653: docXMP, metadata.isReadOnly());
0654: pdfDoc.getRoot().setMetadata(pdfMetadata);
0655: }
0656:
0657: /** @see org.apache.fop.render.Renderer#getGraphics2DAdapter() */
0658: public Graphics2DAdapter getGraphics2DAdapter() {
0659: return new PDFGraphics2DAdapter(this );
0660: }
0661:
0662: /**
0663: * writes out a comment.
0664: * @param text text for the comment
0665: */
0666: protected void comment(String text) {
0667: if (WRITE_COMMENTS) {
0668: currentStream.add("% " + text + "\n");
0669: }
0670: }
0671:
0672: /** Saves the graphics state of the rendering engine. */
0673: protected void saveGraphicsState() {
0674: endTextObject();
0675: currentStream.add("q\n");
0676: }
0677:
0678: /** Restores the last graphics state of the rendering engine. */
0679: protected void restoreGraphicsState() {
0680: endTextObject();
0681: currentStream.add("Q\n");
0682: }
0683:
0684: /** Indicates the beginning of a text object. */
0685: protected void beginTextObject() {
0686: if (!inTextMode) {
0687: currentStream.add("BT\n");
0688: currentFontName = "";
0689: inTextMode = true;
0690: }
0691: }
0692:
0693: /** Indicates the end of a text object. */
0694: protected void endTextObject() {
0695: closeText();
0696: if (inTextMode) {
0697: currentStream.add("ET\n");
0698: inTextMode = false;
0699: }
0700: }
0701:
0702: /**
0703: * Start the next page sequence.
0704: * For the pdf renderer there is no concept of page sequences
0705: * but it uses the first available page sequence title to set
0706: * as the title of the pdf document.
0707: *
0708: * @param seqTitle the title of the page sequence
0709: */
0710: public void startPageSequence(LineArea seqTitle) {
0711: if (seqTitle != null) {
0712: String str = convertTitleToString(seqTitle);
0713: PDFInfo info = this .pdfDoc.getInfo();
0714: if (info.getTitle() == null) {
0715: info.setTitle(str);
0716: }
0717: }
0718: if (pdfDoc.getRoot().getMetadata() == null) {
0719: //If at this time no XMP metadata for the overall document has been set, create it
0720: //from the PDFInfo object.
0721: Metadata xmp = PDFMetadata.createXMPFromUserAgent(pdfDoc);
0722: PDFMetadata pdfMetadata = pdfDoc.getFactory().makeMetadata(
0723: xmp, true);
0724: pdfDoc.getRoot().setMetadata(pdfMetadata);
0725: }
0726: }
0727:
0728: /**
0729: * The pdf page is prepared by making the page.
0730: * The page is made in the pdf document without any contents
0731: * and then stored to add the contents later.
0732: * The page objects is stored using the area tree PageViewport
0733: * as a key.
0734: *
0735: * @param page the page to prepare
0736: */
0737: public void preparePage(PageViewport page) {
0738: setupPage(page);
0739: if (pages == null) {
0740: pages = new java.util.HashMap();
0741: }
0742: pages.put(page, currentPage);
0743: }
0744:
0745: private void setupPage(PageViewport page) {
0746: this .pdfResources = this .pdfDoc.getResources();
0747:
0748: Rectangle2D bounds = page.getViewArea();
0749: double w = bounds.getWidth();
0750: double h = bounds.getHeight();
0751: currentPage = this .pdfDoc.getFactory().makePage(
0752: this .pdfResources, (int) Math.round(w / 1000),
0753: (int) Math.round(h / 1000), page.getPageIndex());
0754: pageReferences.put(page.getKey(), currentPage.referencePDF());
0755: pvReferences.put(page.getKey(), page);
0756: }
0757:
0758: /**
0759: * This method creates a pdf stream for the current page
0760: * uses it as the contents of a new page. The page is written
0761: * immediately to the output stream.
0762: * @see org.apache.fop.render.Renderer#renderPage(PageViewport)
0763: */
0764: public void renderPage(PageViewport page) throws IOException,
0765: FOPException {
0766: if (pages != null
0767: && (currentPage = (PDFPage) pages.get(page)) != null) {
0768: //Retrieve previously prepared page (out-of-line rendering)
0769: pages.remove(page);
0770: } else {
0771: setupPage(page);
0772: }
0773: currentPageRef = currentPage.referencePDF();
0774:
0775: Rectangle2D bounds = page.getViewArea();
0776: double h = bounds.getHeight();
0777: pageHeight = (int) h;
0778:
0779: currentStream = this .pdfDoc.getFactory().makeStream(
0780: PDFFilterList.CONTENT_FILTER, false);
0781:
0782: currentState = new PDFState();
0783: // Transform the PDF's default coordinate system (0,0 at lower left) to the PDFRenderer's
0784: AffineTransform basicPageTransform = new AffineTransform(1, 0,
0785: 0, -1, 0, pageHeight / 1000f);
0786: currentState.concatenate(basicPageTransform);
0787: currentStream.add(CTMHelper.toPDFString(basicPageTransform,
0788: false)
0789: + " cm\n");
0790:
0791: currentFontName = "";
0792:
0793: super .renderPage(page);
0794:
0795: this .pdfDoc.registerObject(currentStream);
0796: currentPage.setContents(currentStream);
0797: PDFAnnotList annots = currentPage.getAnnotations();
0798: if (annots != null) {
0799: this .pdfDoc.addObject(annots);
0800: }
0801: this .pdfDoc.addObject(currentPage);
0802: this .pdfDoc.output(ostream);
0803: }
0804:
0805: /**
0806: * @see org.apache.fop.render.AbstractRenderer#startVParea(CTM, Rectangle2D)
0807: */
0808: protected void startVParea(CTM ctm, Rectangle2D clippingRect) {
0809: // Set the given CTM in the graphics state
0810: currentState.push();
0811: currentState.concatenate(new AffineTransform(CTMHelper
0812: .toPDFArray(ctm)));
0813:
0814: saveGraphicsState();
0815: if (clippingRect != null) {
0816: clipRect((float) clippingRect.getX() / 1000f,
0817: (float) clippingRect.getY() / 1000f,
0818: (float) clippingRect.getWidth() / 1000f,
0819: (float) clippingRect.getHeight() / 1000f);
0820: }
0821: // multiply with current CTM
0822: currentStream.add(CTMHelper.toPDFString(ctm) + " cm\n");
0823: }
0824:
0825: /**
0826: * @see org.apache.fop.render.AbstractRenderer#endVParea()
0827: */
0828: protected void endVParea() {
0829: restoreGraphicsState();
0830: currentState.pop();
0831: }
0832:
0833: /**
0834: * Handle the traits for a region
0835: * This is used to draw the traits for the given page region.
0836: * (See Sect. 6.4.1.2 of XSL-FO spec.)
0837: * @param region the RegionViewport whose region is to be drawn
0838: */
0839: protected void handleRegionTraits(RegionViewport region) {
0840: currentFontName = "";
0841: super .handleRegionTraits(region);
0842: }
0843:
0844: /**
0845: * Formats a float value (normally coordinates) as Strings.
0846: * @param value the value
0847: * @return the formatted value
0848: */
0849: protected static final String format(float value) {
0850: return PDFNumber.doubleOut(value);
0851: }
0852:
0853: /** @see org.apache.fop.render.AbstractPathOrientedRenderer */
0854: protected void drawBorderLine(float x1, float y1, float x2,
0855: float y2, boolean horz, boolean startOrBefore, int style,
0856: Color col) {
0857: float w = x2 - x1;
0858: float h = y2 - y1;
0859: if ((w < 0) || (h < 0)) {
0860: log
0861: .error("Negative extent received. Border won't be painted.");
0862: return;
0863: }
0864: switch (style) {
0865: case Constants.EN_DASHED:
0866: setColor(col, false, null);
0867: if (horz) {
0868: float unit = Math.abs(2 * h);
0869: int rep = (int) (w / unit);
0870: if (rep % 2 == 0) {
0871: rep++;
0872: }
0873: unit = w / rep;
0874: currentStream.add("[" + format(unit) + "] 0 d ");
0875: currentStream.add(format(h) + " w\n");
0876: float ym = y1 + (h / 2);
0877: currentStream.add(format(x1) + " " + format(ym) + " m "
0878: + format(x2) + " " + format(ym) + " l S\n");
0879: } else {
0880: float unit = Math.abs(2 * w);
0881: int rep = (int) (h / unit);
0882: if (rep % 2 == 0) {
0883: rep++;
0884: }
0885: unit = h / rep;
0886: currentStream.add("[" + format(unit) + "] 0 d ");
0887: currentStream.add(format(w) + " w\n");
0888: float xm = x1 + (w / 2);
0889: currentStream.add(format(xm) + " " + format(y1) + " m "
0890: + format(xm) + " " + format(y2) + " l S\n");
0891: }
0892: break;
0893: case Constants.EN_DOTTED:
0894: setColor(col, false, null);
0895: currentStream.add("1 J ");
0896: if (horz) {
0897: float unit = Math.abs(2 * h);
0898: int rep = (int) (w / unit);
0899: if (rep % 2 == 0) {
0900: rep++;
0901: }
0902: unit = w / rep;
0903: currentStream.add("[0 " + format(unit) + "] 0 d ");
0904: currentStream.add(format(h) + " w\n");
0905: float ym = y1 + (h / 2);
0906: currentStream.add(format(x1) + " " + format(ym) + " m "
0907: + format(x2) + " " + format(ym) + " l S\n");
0908: } else {
0909: float unit = Math.abs(2 * w);
0910: int rep = (int) (h / unit);
0911: if (rep % 2 == 0) {
0912: rep++;
0913: }
0914: unit = h / rep;
0915: currentStream.add("[0 " + format(unit) + " ] 0 d ");
0916: currentStream.add(format(w) + " w\n");
0917: float xm = x1 + (w / 2);
0918: currentStream.add(format(xm) + " " + format(y1) + " m "
0919: + format(xm) + " " + format(y2) + " l S\n");
0920: }
0921: break;
0922: case Constants.EN_DOUBLE:
0923: setColor(col, false, null);
0924: currentStream.add("[] 0 d ");
0925: if (horz) {
0926: float h3 = h / 3;
0927: currentStream.add(format(h3) + " w\n");
0928: float ym1 = y1 + (h3 / 2);
0929: float ym2 = ym1 + h3 + h3;
0930: currentStream.add(format(x1) + " " + format(ym1)
0931: + " m " + format(x2) + " " + format(ym1)
0932: + " l S\n");
0933: currentStream.add(format(x1) + " " + format(ym2)
0934: + " m " + format(x2) + " " + format(ym2)
0935: + " l S\n");
0936: } else {
0937: float w3 = w / 3;
0938: currentStream.add(format(w3) + " w\n");
0939: float xm1 = x1 + (w3 / 2);
0940: float xm2 = xm1 + w3 + w3;
0941: currentStream.add(format(xm1) + " " + format(y1)
0942: + " m " + format(xm1) + " " + format(y2)
0943: + " l S\n");
0944: currentStream.add(format(xm2) + " " + format(y1)
0945: + " m " + format(xm2) + " " + format(y2)
0946: + " l S\n");
0947: }
0948: break;
0949: case Constants.EN_GROOVE:
0950: case Constants.EN_RIDGE: {
0951: float colFactor = (style == EN_GROOVE ? 0.4f : -0.4f);
0952: currentStream.add("[] 0 d ");
0953: if (horz) {
0954: Color uppercol = lightenColor(col, -colFactor);
0955: Color lowercol = lightenColor(col, colFactor);
0956: float h3 = h / 3;
0957: currentStream.add(format(h3) + " w\n");
0958: float ym1 = y1 + (h3 / 2);
0959: setColor(uppercol, false, null);
0960: currentStream.add(format(x1) + " " + format(ym1)
0961: + " m " + format(x2) + " " + format(ym1)
0962: + " l S\n");
0963: setColor(col, false, null);
0964: currentStream.add(format(x1) + " " + format(ym1 + h3)
0965: + " m " + format(x2) + " " + format(ym1 + h3)
0966: + " l S\n");
0967: setColor(lowercol, false, null);
0968: currentStream.add(format(x1) + " "
0969: + format(ym1 + h3 + h3) + " m " + format(x2)
0970: + " " + format(ym1 + h3 + h3) + " l S\n");
0971: } else {
0972: Color leftcol = lightenColor(col, -colFactor);
0973: Color rightcol = lightenColor(col, colFactor);
0974: float w3 = w / 3;
0975: currentStream.add(format(w3) + " w\n");
0976: float xm1 = x1 + (w3 / 2);
0977: setColor(leftcol, false, null);
0978: currentStream.add(format(xm1) + " " + format(y1)
0979: + " m " + format(xm1) + " " + format(y2)
0980: + " l S\n");
0981: setColor(col, false, null);
0982: currentStream.add(format(xm1 + w3) + " " + format(y1)
0983: + " m " + format(xm1 + w3) + " " + format(y2)
0984: + " l S\n");
0985: setColor(rightcol, false, null);
0986: currentStream.add(format(xm1 + w3 + w3) + " "
0987: + format(y1) + " m " + format(xm1 + w3 + w3)
0988: + " " + format(y2) + " l S\n");
0989: }
0990: break;
0991: }
0992: case Constants.EN_INSET:
0993: case Constants.EN_OUTSET: {
0994: float colFactor = (style == EN_OUTSET ? 0.4f : -0.4f);
0995: currentStream.add("[] 0 d ");
0996: Color c = col;
0997: if (horz) {
0998: c = lightenColor(c, (startOrBefore ? 1 : -1)
0999: * colFactor);
1000: currentStream.add(format(h) + " w\n");
1001: float ym1 = y1 + (h / 2);
1002: setColor(c, false, null);
1003: currentStream.add(format(x1) + " " + format(ym1)
1004: + " m " + format(x2) + " " + format(ym1)
1005: + " l S\n");
1006: } else {
1007: c = lightenColor(c, (startOrBefore ? 1 : -1)
1008: * colFactor);
1009: currentStream.add(format(w) + " w\n");
1010: float xm1 = x1 + (w / 2);
1011: setColor(c, false, null);
1012: currentStream.add(format(xm1) + " " + format(y1)
1013: + " m " + format(xm1) + " " + format(y2)
1014: + " l S\n");
1015: }
1016: break;
1017: }
1018: case Constants.EN_HIDDEN:
1019: break;
1020: default:
1021: setColor(col, false, null);
1022: currentStream.add("[] 0 d ");
1023: if (horz) {
1024: currentStream.add(format(h) + " w\n");
1025: float ym = y1 + (h / 2);
1026: currentStream.add(format(x1) + " " + format(ym) + " m "
1027: + format(x2) + " " + format(ym) + " l S\n");
1028: } else {
1029: currentStream.add(format(w) + " w\n");
1030: float xm = x1 + (w / 2);
1031: currentStream.add(format(xm) + " " + format(y1) + " m "
1032: + format(xm) + " " + format(y2) + " l S\n");
1033: }
1034: }
1035: }
1036:
1037: /**
1038: * Sets the current line width in points.
1039: * @param width line width in points
1040: */
1041: private void updateLineWidth(float width) {
1042: if (currentState.setLineWidth(width)) {
1043: //Only write if value has changed WRT the current line width
1044: currentStream.add(format(width) + " w\n");
1045: }
1046: }
1047:
1048: /**
1049: * Clip a rectangular area.
1050: * write a clipping operation given coordinates in the current
1051: * transform.
1052: * @param x the x coordinate
1053: * @param y the y coordinate
1054: * @param width the width of the area
1055: * @param height the height of the area
1056: */
1057: protected void clipRect(float x, float y, float width, float height) {
1058: currentStream.add(format(x) + " " + format(y) + " "
1059: + format(width) + " " + format(height) + " re ");
1060: clip();
1061: }
1062:
1063: /**
1064: * Clip an area.
1065: */
1066: protected void clip() {
1067: currentStream.add("W\n");
1068: currentStream.add("n\n");
1069: }
1070:
1071: /**
1072: * Moves the current point to (x, y), omitting any connecting line segment.
1073: * @param x x coordinate
1074: * @param y y coordinate
1075: */
1076: protected void moveTo(float x, float y) {
1077: currentStream.add(format(x) + " " + format(y) + " m ");
1078: }
1079:
1080: /**
1081: * Appends a straight line segment from the current point to (x, y). The
1082: * new current point is (x, y).
1083: * @param x x coordinate
1084: * @param y y coordinate
1085: */
1086: protected void lineTo(float x, float y) {
1087: currentStream.add(format(x) + " " + format(y) + " l ");
1088: }
1089:
1090: /**
1091: * Closes the current subpath by appending a straight line segment from
1092: * the current point to the starting point of the subpath.
1093: */
1094: protected void closePath() {
1095: currentStream.add("h ");
1096: }
1097:
1098: /**
1099: * @see org.apache.fop.render.AbstractPathOrientedRenderer#fillRect(float, float, float, float)
1100: */
1101: protected void fillRect(float x, float y, float w, float h) {
1102: if (w != 0 && h != 0) {
1103: currentStream.add(format(x) + " " + format(y) + " "
1104: + format(w) + " " + format(h) + " re f\n");
1105: }
1106: }
1107:
1108: /**
1109: * Draw a line.
1110: *
1111: * @param startx the start x position
1112: * @param starty the start y position
1113: * @param endx the x end position
1114: * @param endy the y end position
1115: */
1116: private void drawLine(float startx, float starty, float endx,
1117: float endy) {
1118: currentStream
1119: .add(format(startx) + " " + format(starty) + " m ");
1120: currentStream.add(format(endx) + " " + format(endy) + " l S\n");
1121: }
1122:
1123: /**
1124: * Breaks out of the state stack to handle fixed block-containers.
1125: * @return the saved state stack to recreate later
1126: */
1127: protected List breakOutOfStateStack() {
1128: List breakOutList = new java.util.ArrayList();
1129: PDFState.Data data;
1130: while (true) {
1131: data = currentState.getData();
1132: if (currentState.pop() == null) {
1133: break;
1134: }
1135: if (breakOutList.size() == 0) {
1136: comment("------ break out!");
1137: }
1138: breakOutList.add(0, data); //Insert because of stack-popping
1139: restoreGraphicsState();
1140: }
1141: return breakOutList;
1142: }
1143:
1144: /**
1145: * Restores the state stack after a break out.
1146: * @param breakOutList the state stack to restore.
1147: */
1148: protected void restoreStateStackAfterBreakOut(List breakOutList) {
1149: CTM tempctm;
1150: comment("------ restoring context after break-out...");
1151: PDFState.Data data;
1152: Iterator i = breakOutList.iterator();
1153: double[] matrix = new double[6];
1154: while (i.hasNext()) {
1155: data = (PDFState.Data) i.next();
1156: currentState.push();
1157: saveGraphicsState();
1158: AffineTransform at = data.getTransform();
1159: if (!at.isIdentity()) {
1160: currentState.concatenate(at);
1161: at.getMatrix(matrix);
1162: tempctm = new CTM(matrix[0], matrix[1], matrix[2],
1163: matrix[3], matrix[4] * 1000, matrix[5] * 1000);
1164: currentStream.add(CTMHelper.toPDFString(tempctm)
1165: + " cm\n");
1166: }
1167: //TODO Break-out: Also restore items such as line width and color
1168: //Left out for now because all this painting stuff is very
1169: //inconsistent. Some values go over PDFState, some don't.
1170: }
1171: comment("------ done.");
1172: }
1173:
1174: /**
1175: * Returns area's id if it is the first area in the document with that id
1176: * (i.e. if the area qualifies as a link target).
1177: * Otherwise, or if the area has no id, null is returned.
1178: *
1179: * NOTE : area must be on currentPageViewport, otherwise result may be wrong!
1180: *
1181: * @param area the area for which to return the id
1182: */
1183: protected String getTargetableID(Area area) {
1184: String id = (String) area.getTrait(Trait.PROD_ID);
1185: if (id == null || id.length() == 0
1186: || !currentPageViewport.isFirstWithID(id)
1187: || idPositions.containsKey(id)) {
1188: return null;
1189: } else {
1190: return id;
1191: }
1192: }
1193:
1194: /**
1195: * Set XY position in the PDFGoTo and add it to the PDF trailer.
1196: *
1197: * @param gt the PDFGoTo object
1198: * @param position the X,Y position to set
1199: */
1200: protected void finishIDGoTo(PDFGoTo gt, Point2D.Float position) {
1201: gt.setPosition(position);
1202: pdfDoc.addTrailerObject(gt);
1203: unfinishedGoTos.remove(gt);
1204: }
1205:
1206: /**
1207: * Set page reference and XY position in the PDFGoTo and add it to the PDF trailer.
1208: *
1209: * @param gt the PDFGoTo object
1210: * @param pdfPageRef the PDF reference string of the target page object
1211: * @param position the X,Y position to set
1212: */
1213: protected void finishIDGoTo(PDFGoTo gt, String pdfPageRef,
1214: Point2D.Float position) {
1215: gt.setPageReference(pdfPageRef);
1216: finishIDGoTo(gt, position);
1217: }
1218:
1219: /**
1220: * Get a PDFGoTo pointing to the given id. Create one if necessary.
1221: * It is possible that the PDFGoTo is not fully resolved yet. In that case
1222: * it must be completed (and added to the PDF trailer) later.
1223: *
1224: * @param targetID the target id of the PDFGoTo
1225: * @param pvKey the unique key of the target PageViewport
1226: *
1227: * @return the PDFGoTo that was found or created
1228: */
1229: protected PDFGoTo getPDFGoToForID(String targetID, String pvKey) {
1230: // Already a PDFGoTo present for this target? If not, create.
1231: PDFGoTo gt = (PDFGoTo) idGoTos.get(targetID);
1232: if (gt == null) {
1233: String pdfPageRef = (String) pageReferences.get(pvKey);
1234: Point2D.Float position = (Point2D.Float) idPositions
1235: .get(targetID);
1236: // can the GoTo already be fully filled in?
1237: if (pdfPageRef != null && position != null) {
1238: // getPDFGoTo shares PDFGoTo objects as much as possible.
1239: // It also takes care of assignObjectNumber and addTrailerObject.
1240: gt = pdfDoc.getFactory().getPDFGoTo(pdfPageRef,
1241: position);
1242: } else {
1243: // Not complete yet, can't use getPDFGoTo:
1244: gt = new PDFGoTo(pdfPageRef);
1245: pdfDoc.assignObjectNumber(gt);
1246: // pdfDoc.addTrailerObject() will be called later, from finishIDGoTo()
1247: unfinishedGoTos.add(gt);
1248: }
1249: idGoTos.put(targetID, gt);
1250: }
1251: return gt;
1252: }
1253:
1254: /**
1255: * Saves id's absolute position on page for later retrieval by PDFGoTos
1256: *
1257: * @param id the id of the area whose position must be saved
1258: * @param pdfPageRef the PDF page reference string
1259: * @param relativeIPP the *relative* IP position in millipoints
1260: * @param relativeBPP the *relative* BP position in millipoints
1261: * @param tf the transformation to apply once the relative positions have been
1262: * converted to points
1263: */
1264: protected void saveAbsolutePosition(String id, String pdfPageRef,
1265: int relativeIPP, int relativeBPP, AffineTransform tf) {
1266: Point2D.Float position = new Point2D.Float(relativeIPP / 1000f,
1267: relativeBPP / 1000f);
1268: tf.transform(position, position);
1269: idPositions.put(id, position);
1270: // is there already a PDFGoTo waiting to be completed?
1271: PDFGoTo gt = (PDFGoTo) idGoTos.get(id);
1272: if (gt != null) {
1273: finishIDGoTo(gt, pdfPageRef, position);
1274: }
1275: /*
1276: // The code below auto-creates a named destination for every id in the document.
1277: // This should probably be controlled by a user-configurable setting, as it may
1278: // make the PDF file grow noticeably.
1279: // *** NOT YET WELL-TESTED ! ***
1280: if (true) {
1281: PDFFactory factory = pdfDoc.getFactory();
1282: if (gt == null) {
1283: gt = factory.getPDFGoTo(pdfPageRef, position);
1284: idGoTos.put(id, gt); // so others can pick it up too
1285: }
1286: factory.makeDestination(id, gt.referencePDF(), currentPageViewport);
1287: // Note: using currentPageViewport is only correct if the id is indeed on
1288: // the current PageViewport. But even if incorrect, it won't interfere with
1289: // what gets created in the PDF.
1290: // For speedup, we should also create a lookup map id -> PDFDestination
1291: }
1292: */
1293: }
1294:
1295: /**
1296: * Saves id's absolute position on page for later retrieval by PDFGoTos,
1297: * using the currently valid transformation and the currently valid PDF page reference
1298: *
1299: * @param id the id of the area whose position must be saved
1300: * @param relativeIPP the *relative* IP position in millipoints
1301: * @param relativeBPP the *relative* BP position in millipoints
1302: */
1303: protected void saveAbsolutePosition(String id, int relativeIPP,
1304: int relativeBPP) {
1305: saveAbsolutePosition(id, currentPageRef, relativeIPP,
1306: relativeBPP, currentState.getTransform());
1307: }
1308:
1309: /**
1310: * If the given block area is a possible link target, its id + absolute position will
1311: * be saved. The saved position is only correct if this function is called at the very
1312: * start of renderBlock!
1313: *
1314: * @param block the block area in question
1315: */
1316: protected void saveBlockPosIfTargetable(Block block) {
1317: String id = getTargetableID(block);
1318: if (id != null) {
1319: // FIXME: Like elsewhere in the renderer code, absolute and relative
1320: // directions are happily mixed here. This makes sure that the
1321: // links point to the right location, but it is not correct.
1322: int ipp = block.getXOffset();
1323: int bpp = block.getYOffset() + block.getSpaceBefore();
1324: int positioning = block.getPositioning();
1325: if (!(positioning == Block.FIXED || positioning == Block.ABSOLUTE)) {
1326: ipp += currentIPPosition;
1327: bpp += currentBPPosition;
1328: }
1329: AffineTransform tf = positioning == Block.FIXED ? currentState
1330: .getBaseTransform()
1331: : currentState.getTransform();
1332: saveAbsolutePosition(id, currentPageRef, ipp, bpp, tf);
1333: }
1334: }
1335:
1336: /**
1337: * If the given inline area is a possible link target, its id + absolute position will
1338: * be saved. The saved position is only correct if this function is called at the very
1339: * start of renderInlineArea!
1340: *
1341: * @param inlineArea the inline area in question
1342: */
1343: protected void saveInlinePosIfTargetable(InlineArea inlineArea) {
1344: String id = getTargetableID(inlineArea);
1345: if (id != null) {
1346: int extraMarginBefore = 5000; // millipoints
1347: int ipp = currentIPPosition;
1348: int bpp = currentBPPosition + inlineArea.getOffset()
1349: - extraMarginBefore;
1350: saveAbsolutePosition(id, ipp, bpp);
1351: }
1352: }
1353:
1354: /**
1355: * @see org.apache.fop.render.AbstractRenderer#renderBlock(Block)
1356: */
1357: protected void renderBlock(Block block) {
1358: saveBlockPosIfTargetable(block);
1359: super .renderBlock(block);
1360: }
1361:
1362: /**
1363: * @see org.apache.fop.render.AbstractRenderer#renderLineArea(LineArea)
1364: */
1365: protected void renderLineArea(LineArea line) {
1366: super .renderLineArea(line);
1367: closeText();
1368: }
1369:
1370: /**
1371: * @see org.apache.fop.render.AbstractRenderer#renderInlineArea(InlineArea)
1372: */
1373: protected void renderInlineArea(InlineArea inlineArea) {
1374: saveInlinePosIfTargetable(inlineArea);
1375: super .renderInlineArea(inlineArea);
1376: }
1377:
1378: /**
1379: * Render inline parent area.
1380: * For pdf this handles the inline parent area traits such as
1381: * links, border, background.
1382: * @param ip the inline parent area
1383: */
1384: public void renderInlineParent(InlineParent ip) {
1385:
1386: boolean annotsAllowed = pdfDoc.getProfile()
1387: .isAnnotationAllowed();
1388:
1389: // stuff we only need if a link must be created:
1390: Rectangle2D ipRect = null;
1391: PDFFactory factory = null;
1392: PDFAction action = null;
1393: if (annotsAllowed) {
1394: // make sure the rect is determined *before* calling super!
1395: int ipp = currentIPPosition;
1396: int bpp = currentBPPosition + ip.getOffset();
1397: ipRect = new Rectangle2D.Float(ipp / 1000f, bpp / 1000f, ip
1398: .getIPD() / 1000f, ip.getBPD() / 1000f);
1399: AffineTransform transform = currentState.getTransform();
1400: ipRect = transform.createTransformedShape(ipRect)
1401: .getBounds2D();
1402:
1403: factory = pdfDoc.getFactory();
1404: }
1405:
1406: // render contents
1407: super .renderInlineParent(ip);
1408:
1409: boolean linkTraitFound = false;
1410:
1411: // try INTERNAL_LINK first
1412: Trait.InternalLink intLink = (Trait.InternalLink) ip
1413: .getTrait(Trait.INTERNAL_LINK);
1414: if (intLink != null) {
1415: linkTraitFound = true;
1416: String pvKey = intLink.getPVKey();
1417: String idRef = intLink.getIDRef();
1418: boolean pvKeyOK = pvKey != null && pvKey.length() > 0;
1419: boolean idRefOK = idRef != null && idRef.length() > 0;
1420: if (pvKeyOK && idRefOK) {
1421: if (annotsAllowed) {
1422: action = getPDFGoToForID(idRef, pvKey);
1423: }
1424: } else if (pvKeyOK) {
1425: log.warn("Internal link trait with PageViewport key "
1426: + pvKey + " contains no ID reference.");
1427: } else if (idRefOK) {
1428: log.warn("Internal link trait with ID reference "
1429: + idRef + " contains no PageViewport key.");
1430: } else {
1431: log
1432: .warn("Internal link trait received with neither PageViewport key"
1433: + " nor ID reference.");
1434: }
1435: }
1436:
1437: // no INTERNAL_LINK, look for EXTERNAL_LINK
1438: if (!linkTraitFound) {
1439: String extDest = (String) ip.getTrait(Trait.EXTERNAL_LINK);
1440: if (extDest != null && extDest.length() > 0) {
1441: linkTraitFound = true;
1442: if (annotsAllowed) {
1443: action = factory.getExternalAction(extDest);
1444: }
1445: }
1446: }
1447:
1448: // warn if link trait found but not allowed, else create link
1449: if (linkTraitFound) {
1450: if (!annotsAllowed) {
1451: log
1452: .warn("Skipping annotation for a link due to PDF profile: "
1453: + pdfDoc.getProfile());
1454: } else if (action != null) {
1455: PDFLink pdfLink = factory.makeLink(ipRect, action);
1456: currentPage.addAnnotation(pdfLink);
1457: }
1458: }
1459: }
1460:
1461: /**
1462: * @see org.apache.fop.render.AbstractRenderer#renderText(TextArea)
1463: */
1464: public void renderText(TextArea text) {
1465: renderInlineAreaBackAndBorders(text);
1466: beginTextObject();
1467: StringBuffer pdf = new StringBuffer();
1468:
1469: String fontName = getInternalFontNameForArea(text);
1470: int size = ((Integer) text.getTrait(Trait.FONT_SIZE))
1471: .intValue();
1472:
1473: // This assumes that *all* CIDFonts use a /ToUnicode mapping
1474: Typeface tf = (Typeface) fontInfo.getFonts().get(fontName);
1475: boolean useMultiByte = tf.isMultiByte();
1476:
1477: updateFont(fontName, size, pdf);
1478: Color ct = (Color) text.getTrait(Trait.COLOR);
1479: updateColor(ct, true, pdf);
1480:
1481: // word.getOffset() = only height of text itself
1482: // currentBlockIPPosition: 0 for beginning of line; nonzero
1483: // where previous line area failed to take up entire allocated space
1484: int rx = currentIPPosition
1485: + text.getBorderAndPaddingWidthStart();
1486: int bl = currentBPPosition + text.getOffset()
1487: + text.getBaselineOffset();
1488:
1489: pdf.append("1 0 0 -1 " + format(rx / 1000f) + " "
1490: + format(bl / 1000f) + " Tm "
1491: /*+ format(text.getTextLetterSpaceAdjust() / 1000f) + " Tc\n"*/
1492: /*+ format(text.getTextWordSpaceAdjust() / 1000f) + " Tw ["*/);
1493:
1494: pdf.append("[");
1495: currentStream.add(pdf.toString());
1496:
1497: super .renderText(text);
1498:
1499: currentStream.add("] TJ\n");
1500:
1501: renderTextDecoration(tf, size, text, bl, rx);
1502: }
1503:
1504: /**
1505: * @see org.apache.fop.render.AbstractRenderer#renderWord(WordArea)
1506: */
1507: public void renderWord(WordArea word) {
1508: Font font = getFontFromArea(word.getParentArea());
1509: Typeface tf = (Typeface) fontInfo.getFonts().get(
1510: font.getFontName());
1511: boolean useMultiByte = tf.isMultiByte();
1512:
1513: StringBuffer pdf = new StringBuffer();
1514:
1515: String s = word.getWord();
1516: escapeText(s, word.getLetterAdjustArray(), font,
1517: (AbstractTextArea) word.getParentArea(), useMultiByte,
1518: pdf);
1519:
1520: currentStream.add(pdf.toString());
1521:
1522: super .renderWord(word);
1523: }
1524:
1525: /**
1526: * @see org.apache.fop.render.AbstractRenderer#renderSpace(SpaceArea)
1527: */
1528: public void renderSpace(SpaceArea space) {
1529: Font font = getFontFromArea(space.getParentArea());
1530: Typeface tf = (Typeface) fontInfo.getFonts().get(
1531: font.getFontName());
1532: boolean useMultiByte = tf.isMultiByte();
1533:
1534: String s = space.getSpace();
1535:
1536: StringBuffer pdf = new StringBuffer();
1537:
1538: AbstractTextArea textArea = (AbstractTextArea) space
1539: .getParentArea();
1540: escapeText(s, null, font, textArea, useMultiByte, pdf);
1541:
1542: if (space.isAdjustable()) {
1543: int tws = -((TextArea) space.getParentArea())
1544: .getTextWordSpaceAdjust()
1545: - 2 * textArea.getTextLetterSpaceAdjust();
1546:
1547: if (tws != 0) {
1548: pdf.append(format(tws / (font.getFontSize() / 1000f)));
1549: pdf.append(" ");
1550: }
1551: }
1552:
1553: currentStream.add(pdf.toString());
1554:
1555: super .renderSpace(space);
1556: }
1557:
1558: /**
1559: * Escapes text according to PDF rules.
1560: * @param s Text to escape
1561: * @param letterAdjust an array of widths for letter adjustment (may be null)
1562: * @param fs Font state
1563: * @param parentArea the parent text area to retrieve certain traits from
1564: * @param useMultiByte Indicates the use of multi byte convention
1565: * @param pdf target buffer for the escaped text
1566: */
1567: public void escapeText(String s, int[] letterAdjust, Font fs,
1568: AbstractTextArea parentArea, boolean useMultiByte,
1569: StringBuffer pdf) {
1570: String startText = useMultiByte ? "<" : "(";
1571: String endText = useMultiByte ? "> " : ") ";
1572:
1573: /*
1574: boolean kerningAvailable = false;
1575: Map kerning = fs.getKerning();
1576: if (kerning != null && !kerning.isEmpty()) {
1577: //kerningAvailable = true;
1578: //TODO Reenable me when the layout engine supports kerning, too
1579: log.warn("Kerning support is disabled until it is supported by the layout engine!");
1580: }
1581: */
1582:
1583: int l = s.length();
1584:
1585: float fontSize = fs.getFontSize() / 1000f;
1586: boolean startPending = true;
1587: for (int i = 0; i < l; i++) {
1588: char orgChar = s.charAt(i);
1589: char ch;
1590: float glyphAdjust = 0;
1591: if (fs.hasChar(orgChar)) {
1592: ch = fs.mapChar(orgChar);
1593: int tls = (i < l - 1 ? parentArea
1594: .getTextLetterSpaceAdjust() : 0);
1595: glyphAdjust -= tls;
1596: } else {
1597: if (CharUtilities.isFixedWidthSpace(orgChar)) {
1598: //Fixed width space are rendered as spaces so copy/paste works in a reader
1599: ch = fs.mapChar(CharUtilities.SPACE);
1600: glyphAdjust = fs.getCharWidth(ch)
1601: - fs.getCharWidth(orgChar);
1602: } else {
1603: ch = fs.mapChar(orgChar);
1604: }
1605: }
1606: if (letterAdjust != null && i < l - 1) {
1607: glyphAdjust -= letterAdjust[i + 1];
1608: }
1609:
1610: if (startPending) {
1611: pdf.append(startText);
1612: startPending = false;
1613: }
1614: if (!useMultiByte) {
1615: if (ch > 127) {
1616: pdf.append("\\");
1617: pdf.append(Integer.toOctalString((int) ch));
1618: } else {
1619: switch (ch) {
1620: case '(':
1621: case ')':
1622: case '\\':
1623: pdf.append("\\");
1624: break;
1625: default:
1626: }
1627: pdf.append(ch);
1628: }
1629: } else {
1630: pdf.append(PDFText.toUnicodeHex(ch));
1631: }
1632:
1633: float adjust = glyphAdjust / fontSize;
1634:
1635: if (adjust != 0) {
1636: pdf.append(endText).append(format(adjust)).append(' ');
1637: startPending = true;
1638: }
1639:
1640: }
1641: if (!startPending) {
1642: pdf.append(endText);
1643: }
1644: }
1645:
1646: /**
1647: * Checks to see if we have some text rendering commands open
1648: * still and writes out the TJ command to the stream if we do
1649: */
1650: protected void closeText() {
1651: /*
1652: if (textOpen) {
1653: currentStream.add("] TJ\n");
1654: textOpen = false;
1655: prevWordX = 0;
1656: prevWordY = 0;
1657: currentFontName = "";
1658: }*/
1659: }
1660:
1661: /**
1662: * Establishes a new foreground or fill color. In contrast to updateColor
1663: * this method does not check the PDFState for optimization possibilities.
1664: * @param col the color to apply
1665: * @param fill true to set the fill color, false for the foreground color
1666: * @param pdf StringBuffer to write the PDF code to, if null, the code is
1667: * written to the current stream.
1668: */
1669: protected void setColor(Color col, boolean fill, StringBuffer pdf) {
1670: PDFColor color = new PDFColor(this .pdfDoc, col);
1671:
1672: closeText();
1673:
1674: if (pdf != null) {
1675: pdf.append(color.getColorSpaceOut(fill));
1676: } else {
1677: currentStream.add(color.getColorSpaceOut(fill));
1678: }
1679: }
1680:
1681: /**
1682: * Establishes a new foreground or fill color.
1683: * @param col the color to apply (null skips this operation)
1684: * @param fill true to set the fill color, false for the foreground color
1685: * @param pdf StringBuffer to write the PDF code to, if null, the code is
1686: * written to the current stream.
1687: */
1688: private void updateColor(Color col, boolean fill, StringBuffer pdf) {
1689: if (col == null) {
1690: return;
1691: }
1692: boolean update = false;
1693: if (fill) {
1694: update = currentState.setBackColor(col);
1695: } else {
1696: update = currentState.setColor(col);
1697: }
1698:
1699: if (update) {
1700: setColor(col, fill, pdf);
1701: }
1702: }
1703:
1704: /** @see org.apache.fop.render.AbstractPathOrientedRenderer */
1705: protected void updateColor(Color col, boolean fill) {
1706: updateColor(col, fill, null);
1707: }
1708:
1709: private void updateFont(String name, int size, StringBuffer pdf) {
1710: if ((!name.equals(this .currentFontName))
1711: || (size != this .currentFontSize)) {
1712: closeText();
1713:
1714: this .currentFontName = name;
1715: this .currentFontSize = size;
1716: pdf = pdf.append("/" + name + " "
1717: + format((float) size / 1000f) + " Tf\n");
1718: }
1719: }
1720:
1721: /**
1722: * @see org.apache.fop.render.AbstractRenderer#renderImage(Image, Rectangle2D)
1723: */
1724: public void renderImage(Image image, Rectangle2D pos) {
1725: endTextObject();
1726: String url = image.getURL();
1727: putImage(url, pos);
1728: }
1729:
1730: /** @see org.apache.fop.render.AbstractPathOrientedRenderer */
1731: protected void drawImage(String url, Rectangle2D pos,
1732: Map foreignAttributes) {
1733: endTextObject();
1734: putImage(url, pos);
1735: }
1736:
1737: /**
1738: * Adds a PDF XObject (a bitmap) to the PDF that will later be referenced.
1739: * @param url URL of the bitmap
1740: * @param pos Position of the bitmap
1741: */
1742: protected void putImage(String url, Rectangle2D pos) {
1743: PDFXObject xobject = pdfDoc.getImage(url);
1744: if (xobject != null) {
1745: float w = (float) pos.getWidth() / 1000f;
1746: float h = (float) pos.getHeight() / 1000f;
1747: placeImage((float) pos.getX() / 1000f,
1748: (float) pos.getY() / 1000f, w, h, xobject
1749: .getXNumber());
1750: return;
1751: }
1752:
1753: url = ImageFactory.getURL(url);
1754: ImageFactory fact = userAgent.getFactory().getImageFactory();
1755: FopImage fopimage = fact.getImage(url, userAgent);
1756: if (fopimage == null) {
1757: return;
1758: }
1759: if (!fopimage.load(FopImage.DIMENSIONS)) {
1760: return;
1761: }
1762: String mime = fopimage.getMimeType();
1763: if ("text/xml".equals(mime)) {
1764: if (!fopimage.load(FopImage.ORIGINAL_DATA)) {
1765: return;
1766: }
1767: Document doc = ((XMLImage) fopimage).getDocument();
1768: String ns = ((XMLImage) fopimage).getNameSpace();
1769:
1770: renderDocument(doc, ns, pos, null);
1771: } else if ("image/svg+xml".equals(mime)) {
1772: if (!fopimage.load(FopImage.ORIGINAL_DATA)) {
1773: return;
1774: }
1775: Document doc = ((XMLImage) fopimage).getDocument();
1776: String ns = ((XMLImage) fopimage).getNameSpace();
1777:
1778: renderDocument(doc, ns, pos, null);
1779: } else if ("image/eps".equals(mime)) {
1780: FopPDFImage pdfimage = new FopPDFImage(fopimage, url);
1781: int xobj = pdfDoc.addImage(currentContext, pdfimage)
1782: .getXNumber();
1783: fact.releaseImage(url, userAgent);
1784:
1785: float w = (float) pos.getWidth() / 1000f;
1786: float h = (float) pos.getHeight() / 1000f;
1787: placeImage((float) pos.getX() / 1000,
1788: (float) pos.getY() / 1000, w, h, xobj);
1789: } else if ("image/jpeg".equals(mime)
1790: || "image/tiff".equals(mime)) {
1791: FopPDFImage pdfimage = new FopPDFImage(fopimage, url);
1792: int xobj = pdfDoc.addImage(currentContext, pdfimage)
1793: .getXNumber();
1794: fact.releaseImage(url, userAgent);
1795:
1796: float w = (float) pos.getWidth() / 1000f;
1797: float h = (float) pos.getHeight() / 1000f;
1798: placeImage((float) pos.getX() / 1000,
1799: (float) pos.getY() / 1000, w, h, xobj);
1800: } else {
1801: if (!fopimage.load(FopImage.BITMAP)) {
1802: return;
1803: }
1804: FopPDFImage pdfimage = new FopPDFImage(fopimage, url);
1805: int xobj = pdfDoc.addImage(currentContext, pdfimage)
1806: .getXNumber();
1807: fact.releaseImage(url, userAgent);
1808:
1809: float w = (float) pos.getWidth() / 1000f;
1810: float h = (float) pos.getHeight() / 1000f;
1811: placeImage((float) pos.getX() / 1000f,
1812: (float) pos.getY() / 1000f, w, h, xobj);
1813: }
1814:
1815: // output new data
1816: try {
1817: this .pdfDoc.output(ostream);
1818: } catch (IOException ioe) {
1819: // ioexception will be caught later
1820: }
1821: }
1822:
1823: /**
1824: * Places a previously registered image at a certain place on the page.
1825: * @param x X coordinate
1826: * @param y Y coordinate
1827: * @param w width for image
1828: * @param h height for image
1829: * @param xobj object number of the referenced image
1830: */
1831: protected void placeImage(float x, float y, float w, float h,
1832: int xobj) {
1833: saveGraphicsState();
1834: currentStream.add(format(w) + " 0 0 " + format(-h) + " "
1835: + format(currentIPPosition / 1000f + x) + " "
1836: + format(currentBPPosition / 1000f + h + y) + " cm\n"
1837: + "/Im" + xobj + " Do\n");
1838: restoreGraphicsState();
1839: }
1840:
1841: /**
1842: * @see org.apache.fop.render.PrintRenderer#createRendererContext(
1843: * int, int, int, int, java.util.Map)
1844: */
1845: protected RendererContext createRendererContext(int x, int y,
1846: int width, int height, Map foreignAttributes) {
1847: RendererContext context = super .createRendererContext(x, y,
1848: width, height, foreignAttributes);
1849: context.setProperty(PDFRendererContextConstants.PDF_DOCUMENT,
1850: pdfDoc);
1851: context.setProperty(PDFRendererContextConstants.OUTPUT_STREAM,
1852: ostream);
1853: context.setProperty(PDFRendererContextConstants.PDF_STATE,
1854: currentState);
1855: context.setProperty(PDFRendererContextConstants.PDF_PAGE,
1856: currentPage);
1857: context.setProperty(PDFRendererContextConstants.PDF_CONTEXT,
1858: currentContext == null ? currentPage : currentContext);
1859: context.setProperty(PDFRendererContextConstants.PDF_CONTEXT,
1860: currentContext);
1861: context.setProperty(PDFRendererContextConstants.PDF_STREAM,
1862: currentStream);
1863: context.setProperty(PDFRendererContextConstants.PDF_FONT_INFO,
1864: fontInfo);
1865: context.setProperty(PDFRendererContextConstants.PDF_FONT_NAME,
1866: currentFontName);
1867: context.setProperty(PDFRendererContextConstants.PDF_FONT_SIZE,
1868: new Integer(currentFontSize));
1869: return context;
1870: }
1871:
1872: /**
1873: * Render leader area.
1874: * This renders a leader area which is an area with a rule.
1875: * @param area the leader area to render
1876: */
1877: public void renderLeader(Leader area) {
1878: renderInlineAreaBackAndBorders(area);
1879:
1880: currentState.push();
1881: saveGraphicsState();
1882: int style = area.getRuleStyle();
1883: float startx = (currentIPPosition + area
1884: .getBorderAndPaddingWidthStart()) / 1000f;
1885: float starty = (currentBPPosition + area.getOffset()) / 1000f;
1886: float endx = (currentIPPosition
1887: + area.getBorderAndPaddingWidthStart() + area.getIPD()) / 1000f;
1888: float ruleThickness = area.getRuleThickness() / 1000f;
1889: Color col = (Color) area.getTrait(Trait.COLOR);
1890:
1891: switch (style) {
1892: case EN_SOLID:
1893: case EN_DASHED:
1894: case EN_DOUBLE:
1895: drawBorderLine(startx, starty, endx,
1896: starty + ruleThickness, true, true, style, col);
1897: break;
1898: case EN_DOTTED:
1899: clipRect(startx, starty, endx - startx, ruleThickness);
1900: //This displaces the dots to the right by half a dot's width
1901: //TODO There's room for improvement here
1902: currentStream.add("1 0 0 1 " + format(ruleThickness / 2)
1903: + " 0 cm\n");
1904: drawBorderLine(startx, starty, endx,
1905: starty + ruleThickness, true, true, style, col);
1906: break;
1907: case EN_GROOVE:
1908: case EN_RIDGE:
1909: float half = area.getRuleThickness() / 2000f;
1910:
1911: setColor(lightenColor(col, 0.6f), true, null);
1912: currentStream.add(format(startx) + " " + format(starty)
1913: + " m\n");
1914: currentStream.add(format(endx) + " " + format(starty)
1915: + " l\n");
1916: currentStream.add(format(endx) + " "
1917: + format(starty + 2 * half) + " l\n");
1918: currentStream.add(format(startx) + " "
1919: + format(starty + 2 * half) + " l\n");
1920: currentStream.add("h\n");
1921: currentStream.add("f\n");
1922: setColor(col, true, null);
1923: if (style == EN_GROOVE) {
1924: currentStream.add(format(startx) + " " + format(starty)
1925: + " m\n");
1926: currentStream.add(format(endx) + " " + format(starty)
1927: + " l\n");
1928: currentStream.add(format(endx) + " "
1929: + format(starty + half) + " l\n");
1930: currentStream.add(format(startx + half) + " "
1931: + format(starty + half) + " l\n");
1932: currentStream.add(format(startx) + " "
1933: + format(starty + 2 * half) + " l\n");
1934: } else {
1935: currentStream.add(format(endx) + " " + format(starty)
1936: + " m\n");
1937: currentStream.add(format(endx) + " "
1938: + format(starty + 2 * half) + " l\n");
1939: currentStream.add(format(startx) + " "
1940: + format(starty + 2 * half) + " l\n");
1941: currentStream.add(format(startx) + " "
1942: + format(starty + half) + " l\n");
1943: currentStream.add(format(endx - half) + " "
1944: + format(starty + half) + " l\n");
1945: }
1946: currentStream.add("h\n");
1947: currentStream.add("f\n");
1948: break;
1949: default:
1950: throw new UnsupportedOperationException(
1951: "rule style not supported");
1952: }
1953:
1954: restoreGraphicsState();
1955: currentState.pop();
1956: beginTextObject();
1957: super .renderLeader(area);
1958: }
1959:
1960: /** @see org.apache.fop.render.AbstractRenderer */
1961: public String getMimeType() {
1962: return MIME_TYPE;
1963: }
1964:
1965: public void setAMode(PDFAMode mode) {
1966: this .pdfAMode = mode;
1967: }
1968:
1969: public void setXMode(PDFXMode mode) {
1970: this .pdfXMode = mode;
1971: }
1972:
1973: public void setOutputProfileURI(String outputProfileURI) {
1974: this .outputProfileURI = outputProfileURI;
1975: }
1976:
1977: public void setFilterMap(Map filterMap) {
1978: this.filterMap = filterMap;
1979: }
1980: }
|