pvList = new ArrayList();
idLocations.put(id, pvList);
pvList.add(pv);
-
+ // signal the PageViewport that it is the first PV to contain this id:
+ pv.setFirstWithID(id);
/*
* See if this ID is in the unresolved idref list, if so resolve
* Resolvable objects tied to it.
import java.awt.geom.Rectangle2D;
import java.util.List;
import java.util.Map;
+import java.util.Set;
import java.util.Stack;
import java.util.StringTokenizer;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.fop.apps.FOUserAgent;
+import org.apache.fop.area.Trait.InternalLink;
import org.apache.fop.area.Trait.Background;
+import org.apache.fop.area.inline.InlineArea;
import org.apache.fop.area.inline.AbstractTextArea;
import org.apache.fop.area.inline.Character;
import org.apache.fop.area.inline.ForeignObject;
transformer.setErrorListener(new DefaultErrorListener(log));
SAXResult res = new SAXResult(getContentHandler(treeModel, userAgent));
-
+
transformer.transform(src, res);
}
-
+
/**
* Creates a new ContentHandler instance that you can send the area tree XML to. The parsed
* pages are added to the AreaTreeModel instance you pass in as a parameter.
* @return the ContentHandler instance to receive the SAX stream from the area tree XML
*/
public ContentHandler getContentHandler(AreaTreeModel treeModel, FOUserAgent userAgent) {
- ElementMappingRegistry elementMappingRegistry
+ ElementMappingRegistry elementMappingRegistry
= userAgent.getFactory().getElementMappingRegistry();
return new Handler(treeModel, userAgent, elementMappingRegistry);
}
-
+
private static class Handler extends DefaultHandler {
-
+
private Map makers = new java.util.HashMap();
-
+
private AreaTreeModel treeModel;
private FOUserAgent userAgent;
private ElementMappingRegistry elementMappingRegistry;
-
+
private Attributes lastAttributes;
private StringBuffer content = new StringBuffer();
private PageViewport currentPageViewport;
+ private Map pageViewportsByKey = new java.util.HashMap();
+ // set of "ID firsts" that have already been assigned to a PV:
+ private Set idFirstsAssigned = new java.util.HashSet();
+
private Stack areaStack = new Stack();
private boolean firstFlow;
private boolean pendingStartPageSequence;
-
+
private Stack delegateStack = new Stack();
private ContentHandler delegate;
private DOMImplementation domImplementation;
-
-
- public Handler(AreaTreeModel treeModel, FOUserAgent userAgent,
+
+
+ public Handler(AreaTreeModel treeModel, FOUserAgent userAgent,
ElementMappingRegistry elementMappingRegistry) {
this.treeModel = treeModel;
this.userAgent = userAgent;
makers.put("beforeFloat", new BeforeFloatMaker());
makers.put("block", new BlockMaker());
makers.put("lineArea", new LineAreaMaker());
+ makers.put("inline", new InlineMaker());
makers.put("inlineparent", new InlineParentMaker());
makers.put("inlineblockparent", new InlineBlockParentMaker());
makers.put("text", new TextMaker());
makers.put("viewport", new ViewportMaker());
makers.put("image", new ImageMaker());
makers.put("foreignObject", new ForeignObjectMaker());
+ makers.put("bookmarkTree", new BookmarkTreeMaker());
+ makers.put("bookmark", new BookmarkMaker());
}
private static Rectangle2D parseRect(String rect) {
Double.parseDouble(tokenizer.nextToken()),
Double.parseDouble(tokenizer.nextToken()));
}
-
+
private Area findAreaType(Class clazz) {
if (areaStack.size() > 0) {
int pos = areaStack.size() - 1;
}
return null;
}
-
+
private RegionViewport getCurrentRegionViewport() {
return (RegionViewport)findAreaType(RegionViewport.class);
}
private BodyRegion getCurrentBodyRegion() {
return (BodyRegion)findAreaType(BodyRegion.class);
}
-
+
private BlockParent getCurrentBlockParent() {
return (BlockParent)findAreaType(BlockParent.class);
}
-
+
private AbstractTextArea getCurrentText() {
return (AbstractTextArea)findAreaType(AbstractTextArea.class);
}
-
+
private Viewport getCurrentViewport() {
return (Viewport)findAreaType(Viewport.class);
}
-
+
/** @see org.xml.sax.helpers.DefaultHandler */
- public void startElement(String uri, String localName, String qName, Attributes attributes)
+ public void startElement(String uri, String localName, String qName, Attributes attributes)
throws SAXException {
if (delegate != null) {
delegateStack.push(qName);
handler.setResult(new DOMResult(doc));
Area parent = (Area)areaStack.peek();
((ForeignObject)parent).setDocument(doc);
-
+
//activate delegate for nested foreign document
domImplementation = null; //Not needed anymore now
this.delegate = handler;
}
if (!handled) {
if (uri == null || uri.length() == 0) {
- throw new SAXException("Unhandled element " + localName
+ throw new SAXException("Unhandled element " + localName
+ " in namespace: " + uri);
} else {
- log.warn("Unhandled element " + localName
+ log.warn("Unhandled element " + localName
+ " in namespace: " + uri);
}
}
}
}
-
+
/** @see org.xml.sax.helpers.DefaultHandler */
public void endElement(String uri, String localName, String qName) throws SAXException {
if (delegate != null) {
content.setLength(0); //Reset text buffer (see characters())
}
}
-
+
// ============== Maker classes for the area tree objects =============
-
+
private static interface Maker {
void startElement(Attributes attributes) throws SAXException;
void endElement();
}
-
+
private abstract class AbstractMaker implements Maker {
public void startElement(Attributes attributes) throws SAXException {
//nop
}
}
-
+
private class AreaTreeMaker extends AbstractMaker {
- //no overrides
+
+ public void startElement(Attributes attributes) {
+ // In case the Handler is reused:
+ idFirstsAssigned.clear();
+ }
}
-
+
private class PageSequenceMaker extends AbstractMaker {
public void startElement(Attributes attributes) {
//treeModel.startPageSequence(null); Done after title or on the first viewport
}
}
-
+
private class TitleMaker extends AbstractMaker {
public void startElement(Attributes attributes) {
treeModel.startPageSequence(line);
pendingStartPageSequence = false;
}
-
-
+
+
}
-
+
private class PageViewportMaker extends AbstractMaker {
public void startElement(Attributes attributes) {
String pageNumberString = attributes.getValue("formatted-nr");
String pageMaster = attributes.getValue("simple-page-master-name");
boolean blank = getAttributeAsBoolean(attributes, "blank", false);
- currentPageViewport = new PageViewport(viewArea,
+ currentPageViewport = new PageViewport(viewArea,
pageNumber, pageNumberString,
pageMaster, blank);
transferForeignObjects(attributes, currentPageViewport);
currentPageViewport.setKey(key);
+ pageViewportsByKey.put(key, currentPageViewport);
}
}
-
+
private class PageMaker extends AbstractMaker {
public void startElement(Attributes attributes) {
assertObjectOfClass(areaStack.pop(), RegionViewport.class);
}
}
-
+
private class RegionBeforeMaker extends AbstractMaker {
public void startElement(Attributes attributes) {
public void startElement(Attributes attributes) {
pushNewRegionReference(attributes, Constants.FO_REGION_AFTER);
}
-
+
public void endElement() {
assertObjectOfClass(areaStack.pop(), RegionReference.class);
}
public void startElement(Attributes attributes) {
pushNewRegionReference(attributes, Constants.FO_REGION_START);
}
-
+
public void endElement() {
assertObjectOfClass(areaStack.pop(), RegionReference.class);
}
public void startElement(Attributes attributes) {
pushNewRegionReference(attributes, Constants.FO_REGION_END);
}
-
+
public void endElement() {
assertObjectOfClass(areaStack.pop(), RegionReference.class);
}
private class BlockMaker extends AbstractMaker {
public void startElement(Attributes attributes) {
- boolean isViewport = getAttributeAsBoolean(attributes,
+ boolean isViewport = getAttributeAsBoolean(attributes,
"is-viewport-area", false);
Block block;
if (isViewport) {
parent.addChildArea(line);
areaStack.push(line);
}
-
+
public void endElement() {
assertObjectOfClass(areaStack.pop(), LineArea.class);
- }
+ }
+ }
+
+ // Maker for "generic" inline areas
+ private class InlineMaker extends AbstractMaker {
+
+ public void startElement(Attributes attributes) {
+ InlineArea inl = new InlineArea();
+ transferForeignObjects(attributes, inl);
+ inl.setOffset(getAttributeAsInteger(attributes, "offset", 0));
+ setAreaAttributes(attributes, inl);
+ setTraits(attributes, inl, SUBSET_COMMON);
+ setTraits(attributes, inl, SUBSET_BOX);
+ setTraits(attributes, inl, SUBSET_COLOR);
+ Area parent = (Area)areaStack.peek();
+ parent.addChildArea(inl);
+ areaStack.push(inl);
+ }
+
+ public void endElement() {
+ assertObjectOfClass(areaStack.pop(), InlineArea.class);
+ }
}
private class InlineParentMaker extends AbstractMaker {
parent.addChildArea(ip);
areaStack.push(ip);
}
-
+
public void endElement() {
assertObjectOfClass(areaStack.pop(), InlineParent.class);
- }
+ }
}
private class InlineBlockParentMaker extends AbstractMaker {
public void endElement() {
assertObjectOfClass(areaStack.pop(), InlineBlockParent.class);
- }
+ }
}
private class TextMaker extends AbstractMaker {
setTraits(attributes, text, SUBSET_FONT);
text.setBaselineOffset(getAttributeAsInteger(attributes, "baseline", 0));
text.setOffset(getAttributeAsInteger(attributes, "offset", 0));
- text.setTextLetterSpaceAdjust(getAttributeAsInteger(attributes,
+ text.setTextLetterSpaceAdjust(getAttributeAsInteger(attributes,
"tlsadjust", 0));
text.setTextWordSpaceAdjust(getAttributeAsInteger(attributes,
"twsadjust", 0));
getCurrentViewport().setContent(foreign);
areaStack.push(foreign);
}
-
+
public void endElement() {
assertObjectOfClass(areaStack.pop(), ForeignObject.class);
- }
+ }
+ }
+
+ private class BookmarkTreeMaker extends AbstractMaker {
+
+ public void startElement(Attributes attributes) {
+ BookmarkData bm = new BookmarkData();
+ areaStack.push(bm);
+ }
+
+ public void endElement() {
+ Object tos = areaStack.pop();
+ assertObjectOfClass(tos, BookmarkData.class);
+ treeModel.handleOffDocumentItem((BookmarkData) tos);
+ // as long as the bookmark tree comes after the last PageViewport in the
+ // area tree XML, we don't have to worry about resolved/unresolved. The
+ // only resolution needed is the mapping of the pvKey to the PV instance.
+ }
+ }
+
+ private class BookmarkMaker extends AbstractMaker {
+
+ public void startElement(Attributes attributes) {
+ String title = attributes.getValue("title");
+ boolean showChildren = getAttributeAsBoolean(attributes, "show-children", false);
+ String[] linkdata
+ = InternalLink.parseXMLAttribute(attributes.getValue("internal-link"));
+ PageViewport pv = (PageViewport) pageViewportsByKey.get(linkdata[0]);
+ BookmarkData bm = new BookmarkData(title, showChildren, pv, linkdata[1]);
+ Object tos = areaStack.peek();
+ if (tos instanceof BookmarkData) {
+ BookmarkData parent = (BookmarkData) tos;
+ parent.addSubData(bm);
+ }
+ areaStack.push(bm);
+ }
+
+ public void endElement() {
+ assertObjectOfClass(areaStack.pop(), BookmarkData.class);
+ }
}
// ====================================================================
-
+
private void pushNewRegionReference(Attributes attributes, int side) {
String regionName = attributes.getValue("name");
RegionViewport rv = getCurrentRegionViewport();
- RegionReference reg = new RegionReference(side,
+ RegionReference reg = new RegionReference(side,
regionName, rv);
transferForeignObjects(attributes, reg);
reg.setCTM(getAttributeAsCTM(attributes, "ctm"));
Trait.BACKGROUND, Trait.COLOR};
private static final Object[] SUBSET_FONT = new Object[] {
Trait.FONT, Trait.FONT_SIZE, Trait.BLINK,
- Trait.OVERLINE, Trait.OVERLINE_COLOR,
+ Trait.OVERLINE, Trait.OVERLINE_COLOR,
Trait.LINETHROUGH, Trait.LINETHROUGH_COLOR,
Trait.UNDERLINE, Trait.UNDERLINE_COLOR};
private static final Object[] SUBSET_BOX = new Object[] {
area.addTrait(trait, Boolean.valueOf(value));
} else if (cl == String.class) {
area.addTrait(trait, value);
+ if (trait == Trait.PROD_ID
+ && !idFirstsAssigned.contains(value)
+ && currentPageViewport != null) {
+ currentPageViewport.setFirstWithID(value);
+ idFirstsAssigned.add(value);
+ }
} else if (cl == Color.class) {
try {
area.addTrait(trait, ColorUtil.parseColorString(this.userAgent, value));
} catch (PropertyException e) {
throw new IllegalArgumentException(e.getMessage());
}
+ } else if (cl == InternalLink.class) {
+ area.addTrait(trait, new InternalLink(value));
} else if (cl == Background.class) {
Background bkg = new Background();
try {
bkg.setColor(col);
} catch (PropertyException e) {
throw new IllegalArgumentException(e.getMessage());
- }
+ }
String url = attributes.getValue("bkg-img");
if (url != null) {
bkg.setURL(url);
-
+
ImageFactory fact = userAgent.getFactory().getImageFactory();
FopImage img = fact.getImage(url, userAgent);
if (img == null) {
} else {
// load dimensions
if (!img.load(FopImage.DIMENSIONS)) {
- log.error("Cannot read background image dimensions: "
+ log.error("Cannot read background image dimensions: "
+ url);
}
}
if (repeat != null) {
bkg.setRepeat(repeat);
}
- bkg.setHoriz(getAttributeAsInteger(attributes,
+ bkg.setHoriz(getAttributeAsInteger(attributes,
"bkg-horz-offset", 0));
- bkg.setVertical(getAttributeAsInteger(attributes,
+ bkg.setVertical(getAttributeAsInteger(attributes,
"bkg-vert-offset", 0));
}
area.addTrait(trait, bkg);
String fontStyle = attributes.getValue("font-style");
int fontWeight = getAttributeAsInteger(
attributes, "font-weight", Font.NORMAL);
- area.addTrait(trait,
+ area.addTrait(trait,
FontInfo.createFontKey(fontName, fontStyle, fontWeight));
}
}
}
}
}
-
- private boolean getAttributeAsBoolean(Attributes attributes, String name,
+
+ private boolean getAttributeAsBoolean(Attributes attributes, String name,
boolean defaultValue) {
String s = attributes.getValue(name);
if (s == null) {
}
}
-
+
}
whenToProcess = END_OF_DOC;
// top level defined in Rec to show all child bookmarks
bShow = true;
-
+
for (int count = 0; count < bookmarkTree.getBookmarks().size(); count++) {
Bookmark bkmk = (Bookmark)(bookmarkTree.getBookmarks()).get(count);
addSubData(createBookmarkData(bkmk));
unresolvedIDRefs.put(idRef, this);
}
+ /**
+ * Create a new bookmark data root object.
+ * This constructor is called by the AreaTreeParser when the
+ * <bookmarkTree> element is read from the XML file
+ */
+ public BookmarkData() {
+ idRef = null;
+ whenToProcess = END_OF_DOC;
+ bShow = true;
+ }
+
+ /**
+ * Create a new bookmark data object.
+ * This constructor is called by the AreaTreeParser when a
+ * <bookmark> element is read from the XML file.
+ *
+ * @param title the bookmark's title
+ * @param showChildren whether to initially display the bookmark's children
+ * @param pv the target PageViewport
+ * @param idRef the target ID
+ */
+ public BookmarkData(String title, boolean showChildren, PageViewport pv, String idRef) {
+ bookmarkTitle = title;
+ bShow = showChildren;
+ pageRef = pv;
+ this.idRef = idRef;
+ }
+
/**
* Get the idref for this bookmark-item
*
}
/**
- * Add the child bookmark data object.
+ * Add a child bookmark data object.
* This adds a child bookmark in the bookmark hierarchy.
*
* @param sub the child bookmark data
*/
public void addSubData(BookmarkData sub) {
subData.add(sub);
- unresolvedIDRefs.put(sub.getIDRef(), sub);
- String[] ids = sub.getIDRefs();
- for (int count = 0; count < ids.length; count++) {
- unresolvedIDRefs.put(ids[count], sub);
+ if (sub.pageRef == null || sub.pageRef.equals("")) {
+ unresolvedIDRefs.put(sub.getIDRef(), sub);
+ String[] ids = sub.getIDRefs();
+ for (int count = 0; count < ids.length; count++) {
+ unresolvedIDRefs.put(ids[count], sub);
+ }
}
}
}
}
-
}
/**
- * Resolve by adding an internal link.
+ * Resolve by adding an internal link to the first PageViewport in the list.
*
* @see org.apache.fop.area.Resolvable#resolveIDRef(String, List)
*/
public void resolveIDRef(String id, List pages) {
- if (idRef.equals(id) && pages != null) {
+ resolveIDRef(id, (PageViewport)pages.get(0));
+ }
+
+ /**
+ * Resolve by adding an InternalLink trait to the area
+ *
+ * @param id the target id (should be equal to the object's idRef)
+ * @param pv the PageViewport containing the first area with the given id
+ */
+ public void resolveIDRef(String id, PageViewport pv) {
+ if (idRef.equals(id) && pv != null) {
resolved = true;
- PageViewport page = (PageViewport)pages.get(0);
- area.addTrait(Trait.INTERNAL_LINK, page.getKey());
+ Trait.InternalLink iLink = new Trait.InternalLink(pv.getKey(), idRef);
+ area.addTrait(Trait.INTERNAL_LINK, iLink);
}
}
}
import java.util.Map;
import java.util.HashMap;
import java.util.Iterator;
+import java.util.Set;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
// list of id references and the rectangle on the page
//private Map idReferences = null;
+ // set of IDs that appear first (or exclusively) on this page:
+ private Set idFirsts = new java.util.HashSet();
+
// this keeps a list of currently unresolved areas or extensions
// once an idref is resolved it is removed
// when this is empty the page can be rendered
return this.pageKey;
}
+ /**
+ * Add an "ID-first" to this page.
+ * This is typically called by the AreaTreeHandler when associating
+ * an ID with a PageViewport.
+ *
+ * @param id the id to be registered as first appearing on this page
+ */
+ public void setFirstWithID(String id) {
+ if (id != null) {
+ idFirsts.add(id);
+ }
+ }
+
+ /**
+ * Check whether a certain id first appears on this page
+ *
+ * @param id the id to be checked
+ * @return true if this page is the first where the id appears
+ */
+ public boolean isFirstWithID(String id) {
+ return idFirsts.contains(id);
+ }
+
/**
* Add an idref to this page.
* All idrefs found for child areas of this PageViewport are added
/**
* Internal link trait.
- * This is resolved and provides a link to an internal area.
+ * Contains the PageViewport key and the PROD_ID of the target area
*/
- public static final Integer INTERNAL_LINK = new Integer(1); //resolved
+ public static final Integer INTERNAL_LINK = new Integer(1);
/**
* External link. A URL link to an external resource.
static {
// Create a hashmap mapping trait code to name for external representation
//put(ID_LINK, new TraitInfo("id-link", String.class));
- put(INTERNAL_LINK, new TraitInfo("internal-link", String.class));
+ put(INTERNAL_LINK, new TraitInfo("internal-link", InternalLink.class));
put(EXTERNAL_LINK, new TraitInfo("external-link", String.class));
put(FONT, new TraitInfo("font", FontTriplet.class));
put(FONT_SIZE, new TraitInfo("font-size", Integer.class));
return null;
}*/
-
+ /**
+ * Class for internal link traits.
+ * Stores PageViewport key and producer ID
+ */
+ public static class InternalLink implements Serializable {
+
+ /** The unique key of the PageViewport. */
+ private String pvKey;
+
+ /** The PROD_ID of the link target */
+ private String idRef;
+
+ /**
+ * Create an InternalLink to the given PageViewport and target ID
+ *
+ * @param pvKey the PageViewport key
+ * @param idRef the target ID
+ */
+ public InternalLink(String pvKey, String idRef) {
+ setPVKey(pvKey);
+ setIDRef(idRef);
+ }
+
+ /**
+ * Create an InternalLink based on the given XML attribute value.
+ * This is typically called when data are read from an XML area tree.
+ *
+ * @param attrValue attribute value to be parsed by InternalLink.parseXMLAttribute
+ */
+ public InternalLink(String attrValue) {
+ String[] values = parseXMLAttribute(attrValue);
+ setPVKey(values[0]);
+ setIDRef(values[1]);
+ }
+
+ /**
+ * Sets the key of the targeted PageViewport.
+ *
+ * @param pvKey the PageViewport key
+ */
+ public void setPVKey(String pvKey) {
+ this.pvKey = pvKey;
+ }
+
+ /**
+ * Returns the key of the targeted PageViewport.
+ *
+ * @return the PageViewport key
+ */
+ public String getPVKey() {
+ return pvKey;
+ }
+
+ /**
+ * Sets the target ID.
+ *
+ * @param idRef the target ID
+ */
+ public void setIDRef(String idRef) {
+ this.idRef = idRef;
+ }
+
+ /**
+ * Returns the target ID.
+ *
+ * @return the target ID
+ */
+ public String getIDRef() {
+ return idRef;
+ }
+
+ /**
+ * Returns the attribute value for this object as
+ * used in the area tree XML.
+ *
+ * @return a string of the type "(thisPVKey,thisIDRef)"
+ */
+ public String xmlAttribute() {
+ return makeXMLAttribute(pvKey, idRef);
+ }
+
+ /**
+ * Returns the XML attribute value for the given PV key and ID ref.
+ * This value is used in the area tree XML.
+ *
+ * @param pvKey the PageViewport key of the link target
+ * @param idRef the ID of the link target
+ * @return a string of the type "(thisPVKey,thisIDRef)"
+ */
+ public static String makeXMLAttribute(String pvKey, String idRef) {
+ return "(" + (pvKey == null ? "" : pvKey) + ","
+ + (idRef == null ? "" : idRef) + ")";
+ }
+
+ /**
+ * Parses XML attribute value from the area tree into
+ * PageViewport key + IDRef strings. If the attribute value is
+ * formatted like "(s1,s2)", then s1 and s2 are considered to be
+ * the PV key and the IDRef, respectively.
+ * Otherwise, the entire string is the PV key and the IDRef is null.
+ *
+ * @param attrValue the atribute value (PV key and possibly IDRef)
+ * @return a 2-String array containing the PV key and the IDRef.
+ * Both may be null.
+ */
+ public static String[] parseXMLAttribute(String attrValue) {
+ String[] result = {null, null};
+ if (attrValue != null) {
+ int len = attrValue.length();
+ if (len >= 2 && attrValue.charAt(0) == '(' && attrValue.charAt(len - 1) == ')') {
+ String[] values = attrValue.substring(1, len - 1).split(",", 2);
+ if (values.length > 0) {
+ result[0] = values[0].trim();
+ if (values.length > 1) {
+ result[1] = values[1].trim();
+ }
+ }
+ } else {
+ // PV key only, e.g. from old area tree XML:
+ result[0] = attrValue;
+ }
+ }
+ return result;
+ }
+
+ /**
+ * Return the human-friendly string for debugging.
+ * @see java.lang.Object#toString()
+ */
+ public String toString() {
+ StringBuffer sb = new StringBuffer();
+ sb.append("pvKey=").append(pvKey);
+ sb.append(",idRef=").append(idRef);
+ return sb.toString();
+ }
+ }
+
/**
* Background trait structure.
* Used for storing back trait information which are related.
import org.apache.fop.datatypes.URISpecification;
import org.apache.fop.fo.flow.BasicLink;
import org.apache.fop.layoutmgr.LayoutManager;
+import org.apache.fop.layoutmgr.PageSequenceLayoutManager;
import org.apache.fop.area.inline.InlineArea;
import org.apache.fop.area.Trait;
import org.apache.fop.area.LinkResolver;
-import org.apache.fop.area.PageViewport;
/**
* LayoutManager for the fo:basic-link formatting object
*/
public class BasicLinkLayoutManager extends InlineLayoutManager {
private BasicLink fobj;
-
+
/**
* Create an fo:basic-link layout manager.
*
setupBasicLinkArea(parentLM, area);
return area;
}
-
- private void setupBasicLinkArea(LayoutManager parentLM,
- InlineArea area) {
- if (fobj.getExternalDestination() != null) {
- area.addTrait(Trait.EXTERNAL_LINK,
- URISpecification.getURL(fobj.getExternalDestination()));
- } else {
- String idref = fobj.getInternalDestination();
- PageViewport page = getPSLM().getFirstPVWithID(idref);
- if (page != null) {
- area.addTrait(Trait.INTERNAL_LINK, page.getKey());
- } else {
- LinkResolver res = new LinkResolver(idref, area);
- getPSLM().addUnresolvedArea(idref, res);
- }
- }
- }
-}
+ /*
+ * Detect internal or external link and add it as an area trait
+ *
+ * @param parentLM the parent LayoutManager
+ * @param area the basic-link's area
+ */
+ private void setupBasicLinkArea(LayoutManager parentLM, InlineArea area) {
+ // internal destinations take precedence:
+ String idref = fobj.getInternalDestination();
+ if (idref != null && idref.length() > 0) {
+ PageSequenceLayoutManager pslm = getPSLM();
+ // the INTERNAL_LINK trait is added by the LinkResolver
+ // if and when the link is resolved:
+ LinkResolver res = new LinkResolver(idref, area);
+ res.resolveIDRef(idref, pslm.getFirstPVWithID(idref));
+ if (!res.isResolved()) {
+ pslm.addUnresolvedArea(idref, res);
+ }
+ } else {
+ String extdest = fobj.getExternalDestination();
+ if (extdest != null) {
+ String url = URISpecification.getURL(extdest);
+ if (url.length() > 0) {
+ area.addTrait(Trait.EXTERNAL_LINK, url);
+ }
+ }
+ }
+ }
+}
import org.apache.fop.area.inline.InlineArea;\r
import org.apache.fop.fo.flow.Wrapper;\r
import org.apache.fop.layoutmgr.LayoutContext;\r
+import org.apache.fop.layoutmgr.PositionIterator;\r
+import org.apache.fop.layoutmgr.TraitSetter;\r
\r
/**\r
* This is the layout manager for the fo:wrapper formatting object.\r
\r
/** @see org.apache.fop.layoutmgr.inline.LeafNodeLayoutManager */\r
public InlineArea get(LayoutContext context) {\r
- //Create a zero-width, zero-height dummy area so this node can \r
- //participate in the ID handling. Otherwise, addId() wouldn't \r
- //be called.\r
+ // Create a zero-width, zero-height dummy area so this node can\r
+ // participate in the ID handling. Otherwise, addId() wouldn't\r
+ // be called. The area must also be added to the tree, because\r
+ // determination of the X,Y position is done in the renderer.\r
InlineArea area = new InlineArea();\r
+ String id = fobj.getId();\r
+ if (id != null && id.length() > 0) {\r
+ TraitSetter.setProducerID(area, fobj.getId());\r
+ }\r
return area;\r
}\r
- \r
+\r
+ /**\r
+ * Add the area for this layout manager.\r
+ * This adds the dummy area to the parent, *if* it has an id\r
+ * - otherwise it serves no purpose.\r
+ *\r
+ * @param posIter the position iterator\r
+ * @param context the layout context for adding the area\r
+ */\r
+ public void addAreas(PositionIterator posIter, LayoutContext context) {\r
+ String id = fobj.getId();\r
+ if (id != null && id.length() > 0) {\r
+ addId();\r
+ InlineArea area = getEffectiveArea();\r
+ parentLM.addChildArea(area);\r
+ }\r
+ while (posIter.hasNext()) {\r
+ posIter.next();\r
+ }\r
+ }\r
+\r
/** @see org.apache.fop.layoutmgr.inline.LeafNodeLayoutManager#addId() */\r
protected void addId() {\r
getPSLM().addIDToPage(fobj.getId());\r
}\r
- \r
+\r
}\r
/**
* PDFReference (object reference) for this destination
*/
- private String goToReference;
+ private String goToReference;
/**
* ID Reference for this destination
this.pageViewport = destinationData.getPageViewport();
}
+ /**
+ * create a named destination
+ *
+ * @param idRef The ID reference for this destination - will be used as the name
+ * @param goToRef A PDF reference to a /GoTo pointing to the target area
+ * @param pv The PageViewport of the target area (merely informational)
+ */
+ public PDFDestination(String idRef, String goToRef, PageViewport pv) {
+ super();
+ this.idRef = idRef;
+ this.goToReference = goToRef;
+ this.pageViewport = pv;
+ }
+
/**
* @see org.apache.fop.pdf.PDFObject#toPDFString()
*/
public String toPDFString() {
String s = getObjectID()
- + "<<"
+ + "<<\n"
+ "/Limits [(" + idRef + ") (" + idRef + ")]\n"
+ "/Names [(" + idRef + ") " + goToReference + "]"
+ "\n>>\nendobj\n";
}
PDFDestination dest = (PDFDestination)obj;
- if (dest.getIDRef() == this.getIDRef()) {
+ if (dest.getIDRef().equals(this.getIDRef())) {
return true;
}
- return true;
+ return false;
}
}
package org.apache.fop.pdf;
// Java
+import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.io.FileNotFoundException;
import java.io.IOException;
/**
* Make a Metadata object.
- * @param doc the DOM Document containing the XMP metadata.
+ * @param meta the DOM Document containing the XMP metadata.
* @param readOnly true if the metadata packet should be marked read-only
* @return the newly created Metadata object
*/
/* ============= named destinations and the name dictionary ============ */
+ /**
+ * Registers and returns newdest if it is unique. Otherwise, returns
+ * the equal destination already present in the document.
+ *
+ * @param newdest a new, as yet unregistered destination
+ * @return newdest if unique, else the already registered instance
+ */
+ protected PDFDestination getUniqueDestination(PDFDestination newdest) {
+ PDFDestination existing = getDocument().findDestination(newdest);
+ if (existing != null) {
+ return existing;
+ } else {
+ getDocument().registerObject(newdest);
+ getDocument().setHasDestinations(true);
+ return newdest;
+ }
+ }
+
/**
* Make a named destination.
*
log.warn("Unresolved destination item received: " + destinationData.getIDRef());
}
PDFDestination destination = new PDFDestination(destinationData);
+ return getUniqueDestination(destination);
+ }
- PDFDestination oldDestination = getDocument().findDestination(destination);
- if (destination == oldDestination) {
- destination = oldDestination;
- } else {
- getDocument().registerObject(destination);
- getDocument().setHasDestinations(true);
- }
-
- return destination;
+ /**
+ * Create/find a named destination object.
+ *
+ * @param idRef The ID of this destination. This will be used for the name.
+ * @param goToRef A PDF reference to the associated /GoTo
+ * @param pv The PageViewport of the destination area. Only for informational purposes.
+ *
+ * @return The new or existing named destination
+ */
+ public PDFDestination makeDestination(String idRef, String goToRef, PageViewport pv) {
+ PDFDestination destination = new PDFDestination(idRef, goToRef, pv);
+ return getUniqueDestination(destination);
}
/**
/* ========================= links ===================================== */
+ // Some of the "yoffset-only" functions in this part are obsolete and can
+ // possibly be removed or deprecated. Some are still called by PDFGraphics2D
+ // (although that could be changed, they don't need the yOffset param anyway).
+
+ /**
+ * Create a PDF link to an existing PDFAction object
+ *
+ * @param rect the hotspot position in absolute coordinates
+ * @param pdfAction the PDFAction that this link refers to
+ * @return the new PDFLink object, or null if either rect or pdfAction is null
+ */
+ public PDFLink makeLink(Rectangle2D rect, PDFAction pdfAction) {
+ if (rect == null || pdfAction == null) {
+ return null;
+ } else {
+ PDFLink link = new PDFLink(rect);
+ link.setAction(pdfAction);
+ getDocument().registerObject(link);
+ return link;
+ // does findLink make sense? I mean, how often will it happen that several
+ // links have the same target *and* the same hot rect? And findLink has to
+ // walk and compare the entire link list everytime you call it...
+ }
+ }
+
/**
* Make an internal link.
*
int linkType, float yoffset) {
//PDFLink linkObject;
- int index;
-
PDFLink link = new PDFLink(rect);
if (linkType == PDFLink.EXTERNAL) {
- // check destination
- if (destination.startsWith("http://")) {
- PDFUri uri = new PDFUri(destination);
- link.setAction(uri);
- } else if (destination.endsWith(".pdf")) { // FileSpec
- PDFGoToRemote remote = getGoToPDFAction(destination, null, -1);
- link.setAction(remote);
- } else if ((index = destination.indexOf(".pdf#page=")) > 0) {
- //String file = destination.substring(0, index + 4);
- int page = Integer.parseInt(destination.substring(index + 10));
- PDFGoToRemote remote = getGoToPDFAction(destination, null, page);
- link.setAction(remote);
- } else if ((index = destination.indexOf(".pdf#dest=")) > 0) {
- //String file = destination.substring(0, index + 4);
- String dest = destination.substring(index + 10);
- PDFGoToRemote remote = getGoToPDFAction(destination, dest, -1);
- link.setAction(remote);
- } else { // URI
- PDFUri uri = new PDFUri(destination);
- link.setAction(uri);
- }
+ link.setAction(getExternalAction(destination));
} else {
// linkType is internal
String goToReference = getGoToReference(destination, yoffset);
return link;
}
- public String getGoToReference(String destination, float yoffset) {
+ /**
+ * Create/find and return the appropriate external PDFAction according to the target
+ *
+ * @param target The external target. This may be a PDF file name
+ * (optionally with internal page number or destination) or any type of URI.
+ * @return the PDFAction thus created or found
+ */
+ public PDFAction getExternalAction(String target) {
+ int index;
+ String targetLo = target.toLowerCase();
+ // HTTP URL?
+ if (targetLo.startsWith("http://")) {
+ return new PDFUri(target);
+ // Bare PDF file name?
+ } else if (targetLo.endsWith(".pdf")) {
+ return getGoToPDFAction(target, null, -1);
+ // PDF file + page?
+ } else if ((index = targetLo.indexOf(".pdf#page=")) > 0) {
+ String filename = target.substring(0, index + 4);
+ int page = Integer.parseInt(target.substring(index + 10));
+ return getGoToPDFAction(filename, null, page);
+ // PDF file + destination?
+ } else if ((index = targetLo.indexOf(".pdf#dest=")) > 0) {
+ String filename = target.substring(0, index + 4);
+ String dest = target.substring(index + 10);
+ return getGoToPDFAction(filename, dest, -1);
+ // None of the above? Default to URI:
+ } else {
+ return new PDFUri(target);
+ }
+ }
+
+ /**
+ * Create or find a PDF GoTo with the given page reference string and Y offset,
+ * and return its PDF object reference
+ *
+ * @param pdfPageRef the PDF page reference, e.g. "23 0 R"
+ * @param yoffset the distance from the bottom of the page in points
+ * @return the GoTo's object reference
+ */
+ public String getGoToReference(String pdfPageRef, float yoffset) {
+ return getPDFGoTo(pdfPageRef, new Point2D.Float(0.0f, yoffset)).referencePDF();
+ }
+
+ /**
+ * Finds and returns a PDFGoTo to the given page and position.
+ * Creates the PDFGoTo if not found.
+ *
+ * @param pdfPageRef the PDF page reference
+ * @param position the (X,Y) position in points
+ *
+ * @return the new or existing PDFGoTo object
+ */
+ public PDFGoTo getPDFGoTo(String pdfPageRef, Point2D position) {
getDocument().getProfile().verifyActionAllowed();
- String goToReference = null;
- PDFGoTo gt = new PDFGoTo(destination);
- gt.setYPosition(yoffset);
+ PDFGoTo gt = new PDFGoTo(pdfPageRef, position);
PDFGoTo oldgt = getDocument().findGoTo(gt);
if (oldgt == null) {
getDocument().assignObjectNumber(gt);
} else {
gt = oldgt;
}
-
- goToReference = gt.referencePDF();
- return goToReference;
+ return gt;
}
/**
return remote;
}
+ /**
+ * Make an outline object and add it to the given parent
+ *
+ * @param parent the parent PDFOutline object (may be null)
+ * @param label the title for the new outline object
+ * @param actionRef the action reference string to be placed after the /A
+ * @param showSubItems whether to initially display child outline items
+ * @return the new PDF outline object
+ */
+ public PDFOutline makeOutline(PDFOutline parent, String label,
+ String actionRef, boolean showSubItems) {
+ PDFOutline pdfOutline = new PDFOutline(label, actionRef, showSubItems);
+ if (parent != null) {
+ parent.addOutline(pdfOutline);
+ }
+ getDocument().registerObject(pdfOutline);
+ return pdfOutline;
+ }
+
+ /**
+ * Make an outline object and add it to the given parent
+ *
+ * @param parent the parent PDFOutline object (may be null)
+ * @param label the title for the new outline object
+ * @param pdfAction the action that this outline item points to - must not be null!
+ * @param showSubItems whether to initially display child outline items
+ * @return the new PDFOutline object, or null if pdfAction is null
+ */
+ public PDFOutline makeOutline(PDFOutline parent, String label,
+ PDFAction pdfAction, boolean showSubItems) {
+ return pdfAction == null
+ ? null
+ : makeOutline(parent, label, pdfAction.getAction(), showSubItems);
+ }
+
+ // This one is obsolete now, at least it isn't called from anywhere inside FOP
/**
* Make an outline object and add it to the given outline
*
boolean showSubItems) {
String goToRef = getGoToReference(destination, yoffset);
- PDFOutline obj = new PDFOutline(label, goToRef, showSubItems);
-
- if (parent != null) {
- parent.addOutline(obj);
- }
- getDocument().registerObject(obj);
- return obj;
+ return makeOutline(parent, label, goToRef, showSubItems);
}
-
/* ========================= fonts ===================================== */
/**
package org.apache.fop.pdf;
+import java.awt.geom.Point2D;
+
/**
* class representing a /GoTo object.
* This can either have a Goto to a page reference and location
this.pageReference = pageReference;
}
+ /**
+ * create a /GoTo object.
+ *
+ * @param pageReference the PDF reference to the target page
+ * @param position the target area's on-page coordinates in points
+ */
+ public PDFGoTo(String pageReference, Point2D position) {
+ /* generic creation of object */
+ super();
+
+ this.pageReference = pageReference;
+ setPosition(position);
+ }
+
/**
* Sets page reference after object has been created
*
this.pageReference = pageReference;
}
+ /**
+ * Sets the target (X,Y) position
+ *
+ * @param position the target's on-page coordinates in points
+ */
+ public void setPosition(Point2D position) {
+ this.xPosition = (float) position.getX();
+ this.yPosition = (float) position.getY();
+ }
+
+ /**
+ * Sets the x Position to jump to
+ *
+ * @param xPosition x position
+ */
+ public void setXPosition(float xPosition) {
+ this.xPosition = xPosition;
+ }
+
/**
* Sets the Y position to jump to
*
destination = dest;
}
- /**
- * Sets the x Position to jump to
- *
- * @param xPosition x position
- */
- public void setXPosition(int xPosition) {
- this.xPosition = (xPosition / 1000f);
- }
-
/**
* Get the PDF reference for the GoTo action.
*
}
/**
- * Get the grapics state.
+ * Get a copy of the base transform for the page. Used to translate
+ * IPP/BPP values into X,Y positions when positioning is "fixed".
+ *
+ * @return the base transform, or null if the state stack is empty
+ */
+ public AffineTransform getBaseTransform() {
+ if (stateStack.size() == 0) {
+ return null;
+ } else {
+ Data baseData = (Data) stateStack.get(0);
+ return (AffineTransform) baseData.transform.clone();
+ }
+ }
+
+ /**
+ * Get the graphics state.
* This gets the combination of all graphic states for
* the current context.
* This is the graphic state set with the gs operator not
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URL;
+import java.awt.geom.Point2D;
import java.awt.Color;
import java.awt.color.ColorSpace;
import java.awt.color.ICC_Profile;
import org.apache.fop.apps.FOPException;
import org.apache.fop.apps.FOUserAgent;
import org.apache.fop.apps.MimeConstants;
+import org.apache.fop.area.Area;
+import org.apache.fop.area.Block;
import org.apache.fop.area.CTM;
import org.apache.fop.area.LineArea;
import org.apache.fop.area.OffDocumentExtensionAttachment;
import org.apache.fop.area.inline.TextArea;
import org.apache.fop.area.inline.Image;
import org.apache.fop.area.inline.Leader;
+import org.apache.fop.area.inline.InlineArea;
import org.apache.fop.area.inline.InlineParent;
import org.apache.fop.area.inline.WordArea;
import org.apache.fop.area.inline.SpaceArea;
import org.apache.fop.image.FopImage;
import org.apache.fop.image.ImageFactory;
import org.apache.fop.image.XMLImage;
+import org.apache.fop.pdf.PDFAction;
import org.apache.fop.pdf.PDFAMode;
import org.apache.fop.pdf.PDFAnnotList;
import org.apache.fop.pdf.PDFColor;
import org.apache.fop.pdf.PDFConformanceException;
-import org.apache.fop.pdf.PDFDestination;
import org.apache.fop.pdf.PDFDocument;
import org.apache.fop.pdf.PDFEncryptionManager;
import org.apache.fop.pdf.PDFEncryptionParams;
+import org.apache.fop.pdf.PDFFactory;
import org.apache.fop.pdf.PDFFilterList;
import org.apache.fop.pdf.PDFICCBasedColorSpace;
import org.apache.fop.pdf.PDFICCStream;
+import org.apache.fop.pdf.PDFGoTo;
import org.apache.fop.pdf.PDFInfo;
import org.apache.fop.pdf.PDFLink;
import org.apache.fop.pdf.PDFMetadata;
protected Map pages = null;
/**
- * Page references are stored using the PageViewport as the key
- * when a reference is made the PageViewport is used
- * for pdf this means we need the pdf page reference
+ * Maps unique PageViewport key to PDF page reference
*/
protected Map pageReferences = new java.util.HashMap();
- /** Page viewport references */
+ /**
+ * Maps unique PageViewport key back to PageViewport itself
+ */
protected Map pvReferences = new java.util.HashMap();
+ /**
+ * Maps XSL-FO element IDs to their on-page XY-positions
+ * Must be used in conjunction with the page reference to fully specify the PDFGoTo details
+ */
+ protected Map idPositions = new java.util.HashMap();
+
+ /**
+ * Maps XSL-FO element IDs to PDFGoTo objects targeting the corresponding areas
+ * These objects may not all be fully filled in yet
+ */
+ protected Map idGoTos = new java.util.HashMap();
+
+ /**
+ * The PDFGoTos in idGoTos that are not complete yet
+ */
+ protected List unfinishedGoTos = new java.util.ArrayList();
+ // can't use a Set because PDFGoTo.equals returns true if the target is the same,
+ // even if the object number differs
+
/**
* The output stream to write the document to
*/
* the current page to add annotations to
*/
protected PDFPage currentPage;
-
+
+ /**
+ * the current page's PDF reference string (to avoid numerous function calls)
+ */
+ protected String currentPageRef;
+
/** the (optional) encryption parameters */
protected PDFEncryptionParams encryptionParams;
pdfDoc.getRoot().addOutputIntent(outputIntent);
}
+ /**
+ * Checks if there are any unfinished PDFGoTos left in the list and resolves them
+ * to a default position on the page. Logs a warning, as this should not happen.
+ */
+ protected void finishOpenGoTos() {
+ int count = unfinishedGoTos.size();
+ if (count > 0) {
+ // TODO : page height may not be the same for all targeted pages
+ Point2D.Float defaultPos = new Point2D.Float(0f, pageHeight / 1000f); // top-o-page
+ while (!unfinishedGoTos.isEmpty()) {
+ PDFGoTo gt = (PDFGoTo) unfinishedGoTos.get(0);
+ finishIDGoTo(gt, defaultPos);
+ }
+ boolean one = count == 1;
+ String pl = one ? "" : "s";
+ String ww = one ? "was" : "were";
+ String ia = one ? "is" : "are";
+ log.warn("" + count + " link target" + pl + " could not be fully resolved and "
+ + ww + " now point to the top of the page or "
+ + ia + " dysfunctional."); // dysfunctional if pageref is null
+ }
+ }
+
/**
* @see org.apache.fop.render.Renderer#stopRenderer()
*/
public void stopRenderer() throws IOException {
+ finishOpenGoTos();
+
pdfDoc.getResources().addFonts(pdfDoc, fontInfo);
pdfDoc.outputTrailer(ostream);
currentPage = null;
currentState = null;
currentFontName = "";
+
+ idPositions.clear();
+ idGoTos.clear();
}
/**
public void processOffDocumentItem(OffDocumentItem odi) {
// render Destinations
if (odi instanceof DestinationData) {
- PDFDestination destination = pdfDoc.getFactory().makeDestination((DestinationData) odi);
- PageViewport pv = destination.getPageViewport();
- String dest = (String)pageReferences.get(pv.getKey());
- Rectangle2D bounds = pv.getViewArea();
- double h = bounds.getHeight();
- float yoffset = (float)h / 1000f;
- String gtRef = pdfDoc.getFactory().getGoToReference(dest, yoffset);
- destination.setGoToReference(gtRef);
- }
+ renderDestination((DestinationData) odi);
// render Bookmark-Tree
- else if (odi instanceof BookmarkData) {
+ } else if (odi instanceof BookmarkData) {
renderBookmarkTree((BookmarkData) odi);
} else if (odi instanceof OffDocumentExtensionAttachment) {
ExtensionAttachment attachment = ((OffDocumentExtensionAttachment)odi).getAttachment();
}
}
+ private void renderDestination(DestinationData dd) {
+ String targetID = dd.getIDRef();
+ if (targetID != null && targetID.length() > 0) {
+ PageViewport pv = dd.getPageViewport();
+ if (pv != null) {
+ String pvKey = pv.getKey();
+ PDFGoTo gt = getPDFGoToForID(targetID, pvKey);
+ // create/find and register PDFDestination object:
+ pdfDoc.getFactory().makeDestination(targetID, gt.referencePDF(), pv);
+ } else {
+ log.warn("DestinationData item with IDRef \""
+ + targetID + "\" has a null PageViewport.");
+ }
+ } else {
+ log.warn("DestinationData item with null or empty IDRef received.");
+ }
+ }
+
/**
* Renders a Bookmark-Tree object
* @param bookmarks the BookmarkData object containing all the Bookmark-Items
}
}
- private void renderBookmarkItem(BookmarkData bookmarkItem,
- PDFOutline parentBookmarkItem) {
+ private void renderBookmarkItem(BookmarkData bookmarkItem,
+ PDFOutline parentBookmarkItem) {
PDFOutline pdfOutline = null;
- PageViewport pv = bookmarkItem.getPageViewport();
- if (pv != null) {
- Rectangle2D bounds = pv.getViewArea();
- double h = bounds.getHeight();
- float yoffset = (float)h / 1000f;
- String intDest = (String)pageReferences.get(pv.getKey());
- if (parentBookmarkItem == null) {
- PDFOutline outlineRoot = pdfDoc.getOutlineRoot();
- pdfOutline = pdfDoc.getFactory().makeOutline(outlineRoot,
- bookmarkItem.getBookmarkTitle(),
- intDest, yoffset,
- bookmarkItem.showChildItems());
+
+ String targetID = bookmarkItem.getIDRef();
+ if (targetID != null && targetID.length() > 0) {
+ PageViewport pv = bookmarkItem.getPageViewport();
+ if (pv != null) {
+ String pvKey = pv.getKey();
+ PDFGoTo gt = getPDFGoToForID(targetID, pvKey);
+ // create outline object:
+ PDFOutline parent = parentBookmarkItem != null
+ ? parentBookmarkItem
+ : pdfDoc.getOutlineRoot();
+ pdfOutline = pdfDoc.getFactory().makeOutline(parent,
+ bookmarkItem.getBookmarkTitle(), gt, bookmarkItem.showChildItems());
} else {
- pdfOutline = pdfDoc.getFactory().makeOutline(parentBookmarkItem,
- bookmarkItem.getBookmarkTitle(),
- intDest, yoffset,
- bookmarkItem.showChildItems());
+ log.warn("Bookmark with IDRef \"" + targetID + "\" has a null PageViewport.");
}
} else {
- log.warn("Unresolved bookmark item received: " + bookmarkItem.getIDRef());
+ log.warn("Bookmark item with null or empty IDRef received.");
}
for (int i = 0; i < bookmarkItem.getCount(); i++) {
renderBookmarkItem(bookmarkItem.getSubData(i), pdfOutline);
}
}
-
+
private void renderXMPMetadata(XMPMetadata metadata) {
Metadata docXMP = metadata.getMetadata();
Metadata fopXMP = PDFMetadata.createXMPFromUserAgent(pdfDoc);
} else {
setupPage(page);
}
+ currentPageRef = currentPage.referencePDF();
+
Rectangle2D bounds = page.getViewArea();
double h = bounds.getHeight();
pageHeight = (int) h;
comment("------ done.");
}
+ /**
+ * Returns area's id if it is the first area in the document with that id
+ * (i.e. if the area qualifies as a link target).
+ * Otherwise, or if the area has no id, null is returned.
+ *
+ * NOTE : area must be on currentPageViewport, otherwise result may be wrong!
+ *
+ * @param area the area for which to return the id
+ */
+ protected String getTargetableID(Area area) {
+ String id = (String) area.getTrait(Trait.PROD_ID);
+ if (id == null || id.length() == 0
+ || !currentPageViewport.isFirstWithID(id)
+ || idPositions.containsKey(id)) {
+ return null;
+ } else {
+ return id;
+ }
+ }
+
+ /**
+ * Set XY position in the PDFGoTo and add it to the PDF trailer.
+ *
+ * @param gt the PDFGoTo object
+ * @param position the X,Y position to set
+ */
+ protected void finishIDGoTo(PDFGoTo gt, Point2D.Float position) {
+ gt.setPosition(position);
+ pdfDoc.addTrailerObject(gt);
+ unfinishedGoTos.remove(gt);
+ }
+
+ /**
+ * Set page reference and XY position in the PDFGoTo and add it to the PDF trailer.
+ *
+ * @param gt the PDFGoTo object
+ * @param pdfPageRef the PDF reference string of the target page object
+ * @param position the X,Y position to set
+ */
+ protected void finishIDGoTo(PDFGoTo gt, String pdfPageRef, Point2D.Float position) {
+ gt.setPageReference(pdfPageRef);
+ finishIDGoTo(gt, position);
+ }
+
+ /**
+ * Get a PDFGoTo pointing to the given id. Create one if necessary.
+ * It is possible that the PDFGoTo is not fully resolved yet. In that case
+ * it must be completed (and added to the PDF trailer) later.
+ *
+ * @param targetID the target id of the PDFGoTo
+ * @param pvKey the unique key of the target PageViewport
+ *
+ * @return the PDFGoTo that was found or created
+ */
+ protected PDFGoTo getPDFGoToForID(String targetID, String pvKey) {
+ // Already a PDFGoTo present for this target? If not, create.
+ PDFGoTo gt = (PDFGoTo) idGoTos.get(targetID);
+ if (gt == null) {
+ String pdfPageRef = (String) pageReferences.get(pvKey);
+ Point2D.Float position = (Point2D.Float) idPositions.get(targetID);
+ // can the GoTo already be fully filled in?
+ if (pdfPageRef != null && position != null) {
+ // getPDFGoTo shares PDFGoTo objects as much as possible.
+ // It also takes care of assignObjectNumber and addTrailerObject.
+ gt = pdfDoc.getFactory().getPDFGoTo(pdfPageRef, position);
+ } else {
+ // Not complete yet, can't use getPDFGoTo:
+ gt = new PDFGoTo(pdfPageRef);
+ pdfDoc.assignObjectNumber(gt);
+ // pdfDoc.addTrailerObject() will be called later, from finishIDGoTo()
+ unfinishedGoTos.add(gt);
+ }
+ idGoTos.put(targetID, gt);
+ }
+ return gt;
+ }
+
+ /**
+ * Saves id's absolute position on page for later retrieval by PDFGoTos
+ *
+ * @param id the id of the area whose position must be saved
+ * @param pdfPageRef the PDF page reference string
+ * @param relativeIPP the *relative* IP position in millipoints
+ * @param relativeBPP the *relative* BP position in millipoints
+ * @param tf the transformation to apply once the relative positions have been
+ * converted to points
+ */
+ protected void saveAbsolutePosition(String id, String pdfPageRef,
+ int relativeIPP, int relativeBPP, AffineTransform tf) {
+ Point2D.Float position = new Point2D.Float(relativeIPP / 1000f, relativeBPP / 1000f);
+ tf.transform(position, position);
+ idPositions.put(id, position);
+ // is there already a PDFGoTo waiting to be completed?
+ PDFGoTo gt = (PDFGoTo) idGoTos.get(id);
+ if (gt != null) {
+ finishIDGoTo(gt, pdfPageRef, position);
+ }
+/*
+ // The code below auto-creates a named destination for every id in the document.
+ // This should probably be controlled by a user-configurable setting, as it may
+ // make the PDF file grow noticeably.
+ // *** NOT YET WELL-TESTED ! ***
+ if (true) {
+ PDFFactory factory = pdfDoc.getFactory();
+ if (gt == null) {
+ gt = factory.getPDFGoTo(pdfPageRef, position);
+ idGoTos.put(id, gt); // so others can pick it up too
+ }
+ factory.makeDestination(id, gt.referencePDF(), currentPageViewport);
+ // Note: using currentPageViewport is only correct if the id is indeed on
+ // the current PageViewport. But even if incorrect, it won't interfere with
+ // what gets created in the PDF.
+ // For speedup, we should also create a lookup map id -> PDFDestination
+ }
+*/
+ }
+
+ /**
+ * Saves id's absolute position on page for later retrieval by PDFGoTos,
+ * using the currently valid transformation and the currently valid PDF page reference
+ *
+ * @param id the id of the area whose position must be saved
+ * @param relativeIPP the *relative* IP position in millipoints
+ * @param relativeBPP the *relative* BP position in millipoints
+ */
+ protected void saveAbsolutePosition(String id, int relativeIPP, int relativeBPP) {
+ saveAbsolutePosition(id, currentPageRef,
+ relativeIPP, relativeBPP, currentState.getTransform());
+ }
+
+ /**
+ * If the given block area is a possible link target, its id + absolute position will
+ * be saved. The saved position is only correct if this function is called at the very
+ * start of renderBlock!
+ *
+ * @param block the block area in question
+ */
+ protected void saveBlockPosIfTargetable(Block block) {
+ String id = getTargetableID(block);
+ if (id != null) {
+ // FIXME: Like elsewhere in the renderer code, absolute and relative
+ // directions are happily mixed here. This makes sure that the
+ // links point to the right location, but it is not correct.
+ int ipp = block.getXOffset();
+ int bpp = block.getYOffset() + block.getSpaceBefore();
+ int positioning = block.getPositioning();
+ if (!(positioning == Block.FIXED || positioning == Block.ABSOLUTE)) {
+ ipp += currentIPPosition;
+ bpp += currentBPPosition;
+ }
+ AffineTransform tf = positioning == Block.FIXED
+ ? currentState.getBaseTransform()
+ : currentState.getTransform();
+ saveAbsolutePosition(id, currentPageRef, ipp, bpp, tf);
+ }
+ }
+
+ /**
+ * If the given inline area is a possible link target, its id + absolute position will
+ * be saved. The saved position is only correct if this function is called at the very
+ * start of renderInlineArea!
+ *
+ * @param inlineArea the inline area in question
+ */
+ protected void saveInlinePosIfTargetable(InlineArea inlineArea) {
+ String id = getTargetableID(inlineArea);
+ if (id != null) {
+ int extraMarginBefore = 5000; // millipoints
+ int ipp = currentIPPosition;
+ int bpp = currentBPPosition + inlineArea.getOffset() - extraMarginBefore;
+ saveAbsolutePosition(id, ipp, bpp);
+ }
+ }
+
+ /**
+ * @see org.apache.fop.render.AbstractRenderer#renderBlock(Block)
+ */
+ protected void renderBlock(Block block) {
+ saveBlockPosIfTargetable(block);
+ super.renderBlock(block);
+ }
+
/**
* @see org.apache.fop.render.AbstractRenderer#renderLineArea(LineArea)
*/
closeText();
}
+ /**
+ * @see org.apache.fop.render.AbstractRenderer#renderInlineArea(InlineArea)
+ */
+ protected void renderInlineArea(InlineArea inlineArea) {
+ saveInlinePosIfTargetable(inlineArea);
+ super.renderInlineArea(inlineArea);
+ }
+
/**
* Render inline parent area.
* For pdf this handles the inline parent area traits such as
* @param ip the inline parent area
*/
public void renderInlineParent(InlineParent ip) {
- float start = currentIPPosition / 1000f;
- float top = (ip.getOffset() + currentBPPosition) / 1000f;
- float width = ip.getIPD() / 1000f;
- float height = ip.getBPD() / 1000f;
+ boolean annotsAllowed = pdfDoc.getProfile().isAnnotationAllowed();
+
+ // stuff we only need if a link must be created:
+ Rectangle2D ipRect = null;
+ PDFFactory factory = null;
+ PDFAction action = null;
+ if (annotsAllowed) {
+ // make sure the rect is determined *before* calling super!
+ int ipp = currentIPPosition;
+ int bpp = currentBPPosition + ip.getOffset();
+ ipRect = new Rectangle2D.Float(ipp / 1000f, bpp / 1000f,
+ ip.getIPD() / 1000f, ip.getBPD() / 1000f);
+ AffineTransform transform = currentState.getTransform();
+ ipRect = transform.createTransformedShape(ipRect).getBounds2D();
+
+ factory = pdfDoc.getFactory();
+ }
+
// render contents
super.renderInlineParent(ip);
- if (pdfDoc.getProfile().isAnnotationAllowed()) {
- // place the link over the top
- Object tr = ip.getTrait(Trait.INTERNAL_LINK);
- boolean internal = false;
- String dest = null;
- float yoffset = 0;
- if (tr == null) {
- dest = (String)ip.getTrait(Trait.EXTERNAL_LINK);
+ boolean linkTraitFound = false;
+
+ // try INTERNAL_LINK first
+ Trait.InternalLink intLink = (Trait.InternalLink) ip.getTrait(Trait.INTERNAL_LINK);
+ if (intLink != null) {
+ linkTraitFound = true;
+ String pvKey = intLink.getPVKey();
+ String idRef = intLink.getIDRef();
+ boolean pvKeyOK = pvKey != null && pvKey.length() > 0;
+ boolean idRefOK = idRef != null && idRef.length() > 0;
+ if (pvKeyOK && idRefOK) {
+ if (annotsAllowed) {
+ action = getPDFGoToForID(idRef, pvKey);
+ }
+ } else if (pvKeyOK) {
+ log.warn("Internal link trait with PageViewport key " + pvKey
+ + " contains no ID reference.");
+ } else if (idRefOK) {
+ log.warn("Internal link trait with ID reference " + idRef
+ + " contains no PageViewport key.");
} else {
- String pvKey = (String)tr;
- dest = (String)pageReferences.get(pvKey);
- if (dest != null) {
- PageViewport pv = (PageViewport)pvReferences.get(pvKey);
- Rectangle2D bounds = pv.getViewArea();
- double h = bounds.getHeight();
- yoffset = (float)h / 1000f;
- internal = true;
+ log.warn("Internal link trait received with neither PageViewport key"
+ + " nor ID reference.");
+ }
+ }
+
+ // no INTERNAL_LINK, look for EXTERNAL_LINK
+ if (!linkTraitFound) {
+ String extDest = (String) ip.getTrait(Trait.EXTERNAL_LINK);
+ if (extDest != null && extDest.length() > 0) {
+ linkTraitFound = true;
+ if (annotsAllowed) {
+ action = factory.getExternalAction(extDest);
}
}
- if (dest != null) {
- // add link to pdf document
- Rectangle2D rect = new Rectangle2D.Float(start, top, width, height);
- // transform rect to absolute coords
- AffineTransform transform = currentState.getTransform();
- rect = transform.createTransformedShape(rect).getBounds2D();
-
- int type = internal ? PDFLink.INTERNAL : PDFLink.EXTERNAL;
- PDFLink pdflink = pdfDoc.getFactory().makeLink(
- rect, dest, type, yoffset);
- currentPage.addAnnotation(pdflink);
+ }
+
+ // warn if link trait found but not allowed, else create link
+ if (linkTraitFound) {
+ if (!annotsAllowed) {
+ log.warn("Skipping annotation for a link due to PDF profile: "
+ + pdfDoc.getProfile());
+ } else if (action != null) {
+ PDFLink pdfLink = factory.makeLink(ipRect, action);
+ currentPage.addAnnotation(pdfLink);
}
- } else {
- log.warn("Skipping annotation for a link due to PDF profile: " + pdfDoc.getProfile());
}
}
String s = word.getWord();
escapeText(s, word.getLetterAdjustArray(),
font, (AbstractTextArea)word.getParentArea(), useMultiByte, pdf);
-
+
currentStream.add(pdf.toString());
super.renderWord(word);
}
-
+
/**
* @see org.apache.fop.render.AbstractRenderer#renderSpace(SpaceArea)
*/
if (space.isAdjustable()) {
int tws = -((TextArea) space.getParentArea()).getTextWordSpaceAdjust()
- 2 * textArea.getTextLetterSpaceAdjust();
-
+
if (tws != 0) {
pdf.append(format(tws / (font.getFontSize() / 1000f)));
pdf.append(" ");
}
}
-
+
currentStream.add(pdf.toString());
super.renderSpace(space);
}
-
+
/**
* Escapes text according to PDF rules.
* @param s Text to escape
}
}
if (letterAdjust != null && i < l - 1) {
- glyphAdjust -= letterAdjust[i + 1];
+ glyphAdjust -= letterAdjust[i + 1];
}
-
+
if (startPending) {
pdf.append(startText);
startPending = false;
}
float adjust = glyphAdjust / fontSize;
-
+
if (adjust != 0) {
pdf.append(endText).append(format(adjust)).append(' ');
startPending = true;
}
-
+
}
if (!startPending) {
pdf.append(endText);
import org.apache.fop.area.NormalFlow;
import org.apache.fop.area.OffDocumentExtensionAttachment;
import org.apache.fop.area.OffDocumentItem;
+import org.apache.fop.area.BookmarkData;
import org.apache.fop.area.PageViewport;
import org.apache.fop.area.RegionReference;
import org.apache.fop.area.RegionViewport;
import org.apache.fop.area.Span;
import org.apache.fop.area.Trait;
+import org.apache.fop.area.Trait.InternalLink;
import org.apache.fop.area.Trait.Background;
import org.apache.fop.area.inline.Container;
import org.apache.fop.area.inline.ForeignObject;
addAttribute("font-name", triplet.getName());
addAttribute("font-style", triplet.getStyle());
addAttribute("font-weight", triplet.getWeight());
+ } else if (clazz.equals(InternalLink.class)) {
+ InternalLink iLink = (InternalLink)value;
+ addAttribute(name, iLink.xmlAttribute());
} else if (clazz.equals(Background.class)) {
Background bkg = (Background)value;
//TODO Remove the following line (makes changes in the test checks necessary)
/** @see org.apache.fop.render.AbstractRenderer#processOffDocumentItem(OffDocumentItem) */
public void processOffDocumentItem(OffDocumentItem oDI) {
- if (oDI instanceof OffDocumentExtensionAttachment) {
+ if (oDI instanceof BookmarkData) {
+ renderBookmarkTree((BookmarkData) oDI);
+ } else if (oDI instanceof OffDocumentExtensionAttachment) {
ExtensionAttachment attachment = ((OffDocumentExtensionAttachment)oDI).getAttachment();
if (extensionAttachments == null) {
extensionAttachments = new java.util.ArrayList();
}
}
+ /**
+ * Renders a BookmarkTree object
+ * @param bookmarkRoot the BookmarkData object representing the top of the tree
+ */
+ protected void renderBookmarkTree(BookmarkData bookmarkRoot) {
+ if (bookmarkRoot.getWhenToProcess() == OffDocumentItem.END_OF_DOC) {
+ endPageSequence();
+ }
+ /* If this kind of handling is also necessary for other renderers, then
+ better add endPageSequence to the Renderer interface and call it
+ explicitly from model.endDocument() */
+
+ startElement("bookmarkTree");
+ for (int i = 0; i < bookmarkRoot.getCount(); i++) {
+ renderBookmarkItem(bookmarkRoot.getSubData(i));
+ }
+ endElement("bookmarkTree");
+ }
+
+ private void renderBookmarkItem(BookmarkData bm) {
+ atts.clear();
+ addAttribute("title", bm.getBookmarkTitle());
+ addAttribute("show-children", String.valueOf(bm.showChildItems()));
+ PageViewport pv = bm.getPageViewport();
+ String pvKey = pv == null ? null : pv.getKey();
+ addAttribute("internal-link",
+ InternalLink.makeXMLAttribute(pvKey, bm.getIDRef()));
+ startElement("bookmark", atts);
+ for (int i = 0; i < bm.getCount(); i++) {
+ renderBookmarkItem(bm.getSubData(i));
+ }
+ endElement("bookmark");
+ }
+
/**
* @see org.apache.fop.render.Renderer#startRenderer(OutputStream)
*/
* @see org.apache.fop.render.Renderer#stopRenderer()
*/
public void stopRenderer() throws IOException {
- if (startedSequence) {
- endElement("pageSequence");
- }
+ endPageSequence();
endElement("areaTree");
try {
handler.endDocument();
*/
public void startPageSequence(LineArea seqTitle) {
handleDocumentExtensionAttachments();
- if (startedSequence) {
- endElement("pageSequence");
- }
+ endPageSequence(); // move this before handleDocumentExtensionAttachments() ?
startedSequence = true;
startElement("pageSequence");
if (seqTitle != null) {
}
}
+ /**
+ * Tells the renderer to finish the current PageSequence
+ */
+ public void endPageSequence() {
+ if (startedSequence) {
+ endElement("pageSequence");
+ }
+ startedSequence = false;
+ }
+
/**
* @see org.apache.fop.render.AbstractRenderer#renderRegionViewport(RegionViewport)
*/
endElement("lineArea");
}
+ /**
+ * @see org.apache.fop.render.AbstractRenderer#renderInlineArea(InlineArea)
+ */
+ protected void renderInlineArea(InlineArea inlineArea) {
+ atts.clear();
+ if (inlineArea.getClass() == InlineArea.class) {
+ // Generic inline area. This is implemented to allow the 0x0 "dummy"
+ // area generated by fo:wrapper to pass its id.
+ addAreaAttributes(inlineArea);
+ addTraitAttributes(inlineArea);
+ startElement("inline", atts);
+ endElement("inline");
+ } else {
+ super.renderInlineArea(inlineArea);
+ // calls specific renderers for Text, Space, Viewport, etc. etc.
+ }
+ }
+
/**
* @see org.apache.fop.render.AbstractRenderer#renderViewport(Viewport)
*/
<changes>
<release version="FOP Trunk">
+ <action context="Code" dev="JM" type="add" fixes-bug="42067" due-to="Paul Vinkenoog">
+ Add support for exact positioning of internal PDF links.
+ </action>
<action context="Code" dev="JM" type="fix" fixes-bug="41434" due-to="Martin Kögler">
Fix PDF Genaration for non-ASCII compatible locales.
</action>