blob: da010d21a91b91e567c224902a6e47c31a16abf3 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* $Id$ */
package org.apache.fop.render.intermediate;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.geom.AffineTransform;
import java.awt.geom.Rectangle2D;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Stack;
import javax.xml.transform.stream.StreamResult;
import org.w3c.dom.Document;
import org.xml.sax.SAXException;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.batik.parser.AWTTransformProducer;
import org.apache.xmlgraphics.xmp.Metadata;
import org.apache.xmlgraphics.xmp.schemas.DublinCoreAdapter;
import org.apache.xmlgraphics.xmp.schemas.DublinCoreSchema;
import org.apache.xmlgraphics.xmp.schemas.XMPBasicAdapter;
import org.apache.xmlgraphics.xmp.schemas.XMPBasicSchema;
import org.apache.fop.Version;
import org.apache.fop.accessibility.StructureTreeElement;
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.AreaTreeObject;
import org.apache.fop.area.Block;
import org.apache.fop.area.BlockViewport;
import org.apache.fop.area.BookmarkData;
import org.apache.fop.area.CTM;
import org.apache.fop.area.DestinationData;
import org.apache.fop.area.OffDocumentExtensionAttachment;
import org.apache.fop.area.OffDocumentItem;
import org.apache.fop.area.PageSequence;
import org.apache.fop.area.PageViewport;
import org.apache.fop.area.RegionViewport;
import org.apache.fop.area.Trait;
import org.apache.fop.area.inline.AbstractTextArea;
import org.apache.fop.area.inline.BasicLinkArea;
import org.apache.fop.area.inline.ForeignObject;
import org.apache.fop.area.inline.Image;
import org.apache.fop.area.inline.InlineArea;
import org.apache.fop.area.inline.InlineParent;
import org.apache.fop.area.inline.InlineViewport;
import org.apache.fop.area.inline.Leader;
import org.apache.fop.area.inline.SpaceArea;
import org.apache.fop.area.inline.TextArea;
import org.apache.fop.area.inline.WordArea;
import org.apache.fop.datatypes.URISpecification;
import org.apache.fop.fo.extensions.ExtensionAttachment;
import org.apache.fop.fo.extensions.xmp.XMPMetadata;
import org.apache.fop.fonts.Font;
import org.apache.fop.fonts.FontInfo;
import org.apache.fop.fonts.FontTriplet;
import org.apache.fop.fonts.LazyFont;
import org.apache.fop.fonts.Typeface;
import org.apache.fop.render.AbstractPathOrientedRenderer;
import org.apache.fop.render.Renderer;
import org.apache.fop.render.intermediate.extensions.AbstractAction;
import org.apache.fop.render.intermediate.extensions.ActionSet;
import org.apache.fop.render.intermediate.extensions.Bookmark;
import org.apache.fop.render.intermediate.extensions.BookmarkTree;
import org.apache.fop.render.intermediate.extensions.GoToXYAction;
import org.apache.fop.render.intermediate.extensions.Link;
import org.apache.fop.render.intermediate.extensions.NamedDestination;
import org.apache.fop.render.intermediate.extensions.URIAction;
import org.apache.fop.traits.BorderProps;
import org.apache.fop.traits.RuleStyle;
/**
* This renderer implementation is an adapter to the {@link IFPainter} interface. It is used
* to generate content using FOP's intermediate format.
*/
public class IFRenderer extends AbstractPathOrientedRenderer {
//TODO Many parts of the Renderer infrastructure are using floats (coordinates in points)
//instead of ints (in millipoints). A lot of conversion to and from is performed.
//When the new IF is established, the Renderer infrastructure should be revisited so check
//if optimizations can be done to avoid int->float->int conversions.
/** logging instance */
protected static final Log log = LogFactory.getLog(IFRenderer.class);
/** XML MIME type */
public static final String IF_MIME_TYPE = MimeConstants.MIME_FOP_IF;
private IFDocumentHandler documentHandler;
private IFPainter painter;
/** If not null, the XMLRenderer will mimic another renderer by using its font setup. */
protected Renderer mimic;
private boolean inPageSequence;
private Stack graphicContextStack = new Stack();
private Stack viewportDimensionStack = new Stack();
private IFGraphicContext graphicContext = new IFGraphicContext();
//private Stack groupStack = new Stack();
private Metadata documentMetadata;
/**
* Maps XSL-FO element IDs to their on-page XY-positions
* Must be used in conjunction with the page reference to fully specify the details
* of a "go-to" action.
*/
private Map idPositions = new java.util.HashMap();
/**
* The "go-to" actions in idGoTos that are not complete yet
*/
private 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
/** Maps unique PageViewport key to page indices (for link target handling) */
protected Map pageIndices = new java.util.HashMap();
private BookmarkTree bookmarkTree;
private List deferredDestinations = new java.util.ArrayList();
private List deferredLinks = new java.util.ArrayList();
private ActionSet actionSet = new ActionSet();
private TextUtil textUtil = new TextUtil();
private Stack<String> ids = new Stack<String>();
/**
* Main constructor
*
* @param userAgent the user agent that contains configuration details. This cannot be null.
*/
public IFRenderer(FOUserAgent userAgent) {
super(userAgent);
}
/** {@inheritDoc} */
public String getMimeType() {
return IF_MIME_TYPE;
}
/**
* Sets the {@link IFDocumentHandler} to be used by the {@link IFRenderer}.
* @param documentHandler the {@link IFDocumentHandler}
*/
public void setDocumentHandler(IFDocumentHandler documentHandler) {
this.documentHandler = documentHandler;
}
/** {@inheritDoc} */
public void setupFontInfo(FontInfo inFontInfo) throws FOPException {
if (this.documentHandler == null) {
this.documentHandler = createDefaultDocumentHandler();
}
IFUtil.setupFonts(this.documentHandler, inFontInfo);
this.fontInfo = inFontInfo;
}
private void handleIFException(IFException ife) {
if (ife.getCause() instanceof SAXException) {
throw new RuntimeException(ife.getCause());
} else {
throw new RuntimeException(ife);
}
}
private void handleIFExceptionWithIOException(IFException ife) throws IOException {
Throwable cause = ife.getCause();
if (cause instanceof IOException) {
throw (IOException) cause;
} else {
handleIFException(ife);
}
}
/** {@inheritDoc} */
public boolean supportsOutOfOrder() {
return (this.documentHandler != null
? this.documentHandler.supportsPagesOutOfOrder() : false);
}
/**
* Returns the document navigation handler if available/supported.
* @return the document navigation handler or null if not supported
*/
protected IFDocumentNavigationHandler getDocumentNavigationHandler() {
return this.documentHandler.getDocumentNavigationHandler();
}
/**
* Indicates whether document navigation features are supported by the document handler.
* @return true if document navigation features are available
*/
protected boolean hasDocumentNavigation() {
return getDocumentNavigationHandler() != null;
}
/**
* Creates a default {@link IFDocumentHandler} when none has been set.
* @return the default IFDocumentHandler
*/
protected IFDocumentHandler createDefaultDocumentHandler() {
FOUserAgent userAgent = getUserAgent();
IFSerializer serializer = new IFSerializer(new IFContext(userAgent));
if (userAgent.isAccessibilityEnabled()) {
userAgent.setStructureTreeEventHandler(serializer.getStructureTreeEventHandler());
}
return serializer;
}
/** {@inheritDoc} */
public void startRenderer(OutputStream outputStream)
throws IOException {
try {
if (outputStream != null) {
StreamResult result = new StreamResult(outputStream);
if (getUserAgent().getOutputFile() != null) {
result.setSystemId(
getUserAgent().getOutputFile().toURI().toURL().toExternalForm());
}
if (this.documentHandler == null) {
this.documentHandler = createDefaultDocumentHandler();
}
this.documentHandler.setResult(result);
}
super.startRenderer(null);
if (log.isDebugEnabled()) {
log.debug("Rendering areas via IF document handler ("
+ this.documentHandler.getClass().getName() + ")...");
}
documentHandler.startDocument();
documentHandler.startDocumentHeader();
} catch (IFException e) {
handleIFExceptionWithIOException(e);
}
}
/** {@inheritDoc} */
public void stopRenderer() throws IOException {
try {
if (this.inPageSequence) {
documentHandler.endPageSequence();
this.inPageSequence = false;
}
documentHandler.startDocumentTrailer();
//Wrap up document navigation
if (hasDocumentNavigation()) {
finishOpenGoTos();
Iterator iter = this.deferredDestinations.iterator();
while (iter.hasNext()) {
NamedDestination dest = (NamedDestination)iter.next();
iter.remove();
getDocumentNavigationHandler().renderNamedDestination(dest);
}
if (this.bookmarkTree != null) {
getDocumentNavigationHandler().renderBookmarkTree(this.bookmarkTree);
}
}
documentHandler.endDocumentTrailer();
documentHandler.endDocument();
} catch (IFException e) {
handleIFExceptionWithIOException(e);
}
pageIndices.clear();
idPositions.clear();
actionSet.clear();
super.stopRenderer();
log.debug("Rendering finished.");
}
@Override
public void setDocumentLocale(Locale locale) {
documentHandler.setDocumentLocale(locale);
}
/** {@inheritDoc} */
public void processOffDocumentItem(OffDocumentItem odi) {
if (odi instanceof DestinationData) {
// render Destinations
renderDestination((DestinationData) odi);
} else if (odi instanceof BookmarkData) {
// render Bookmark-Tree
renderBookmarkTree((BookmarkData) odi);
} else if (odi instanceof OffDocumentExtensionAttachment) {
ExtensionAttachment attachment = ((OffDocumentExtensionAttachment)odi).getAttachment();
if (XMPMetadata.CATEGORY.equals(attachment.getCategory())) {
renderXMPMetadata((XMPMetadata)attachment);
} else {
try {
this.documentHandler.handleExtensionObject(attachment);
} catch (IFException ife) {
handleIFException(ife);
}
}
}
}
private void renderDestination(DestinationData dd) {
if (!hasDocumentNavigation()) {
return;
}
String targetID = dd.getIDRef();
if (targetID == null || targetID.length() == 0) {
throw new IllegalArgumentException("DestinationData must contain a ID reference");
}
PageViewport pv = dd.getPageViewport();
if (pv != null) {
GoToXYAction action = getGoToActionForID(targetID, pv.getPageIndex());
NamedDestination namedDestination = new NamedDestination(targetID, action);
this.deferredDestinations.add(namedDestination);
} else {
//Warning already issued by AreaTreeHandler (debug level is sufficient)
log.debug("Unresolved destination item received: " + dd.getIDRef());
}
}
/**
* Renders a Bookmark-Tree object
* @param bookmarks the BookmarkData object containing all the Bookmark-Items
*/
protected void renderBookmarkTree(BookmarkData bookmarks) {
assert this.bookmarkTree == null;
if (!hasDocumentNavigation()) {
return;
}
this.bookmarkTree = new BookmarkTree();
for (int i = 0; i < bookmarks.getCount(); i++) {
BookmarkData ext = bookmarks.getSubData(i);
Bookmark b = renderBookmarkItem(ext);
bookmarkTree.addBookmark(b);
}
}
private Bookmark renderBookmarkItem(BookmarkData bookmarkItem) {
String targetID = bookmarkItem.getIDRef();
if (targetID == null || targetID.length() == 0) {
throw new IllegalArgumentException("DestinationData must contain a ID reference");
}
GoToXYAction action = null;
PageViewport pv = bookmarkItem.getPageViewport();
if (pv != null) {
action = getGoToActionForID(targetID, pv.getPageIndex());
} else {
//Warning already issued by AreaTreeHandler (debug level is sufficient)
log.debug("Bookmark with IDRef \"" + targetID + "\" has a null PageViewport.");
}
Bookmark b = new Bookmark(
bookmarkItem.getBookmarkTitle(),
bookmarkItem.showChildItems(),
action);
for (int i = 0; i < bookmarkItem.getCount(); i++) {
b.addChildBookmark(renderBookmarkItem(bookmarkItem.getSubData(i)));
}
return b;
}
private void renderXMPMetadata(XMPMetadata metadata) {
this.documentMetadata = metadata.getMetadata();
}
private GoToXYAction getGoToActionForID(String targetID, int pageIndex) {
// Already a GoToXY present for this target? If not, create.
GoToXYAction action = (GoToXYAction)actionSet.get(targetID);
//GoToXYAction action = (GoToXYAction)idGoTos.get(targetID);
if (action == null) {
/* if (pageIndex < 0) {
//pageIndex = page
} */
Point position = (Point)idPositions.get(targetID);
// can the GoTo already be fully filled in?
if (pageIndex >= 0 && position != null) {
action = new GoToXYAction(targetID, pageIndex, position, documentHandler.getContext());
} else {
// Not complete yet, can't use getPDFGoTo:
action = new GoToXYAction(targetID, pageIndex, null, documentHandler.getContext());
unfinishedGoTos.add(action);
}
action = (GoToXYAction)actionSet.put(action);
//idGoTos.put(targetID, action);
}
return action;
}
private void finishOpenGoTos() {
int count = unfinishedGoTos.size();
if (count > 0) {
Point defaultPos = new Point(0, 0); // top-o-page
while (!unfinishedGoTos.isEmpty()) {
GoToXYAction action = (GoToXYAction)unfinishedGoTos.get(0);
noteGoToPosition(action, defaultPos);
}
// dysfunctional if pageref is null
}
}
private void noteGoToPosition(GoToXYAction action, Point position) {
action.setTargetLocation(position);
try {
getDocumentNavigationHandler().addResolvedAction(action);
} catch (IFException ife) {
handleIFException(ife);
}
unfinishedGoTos.remove(action);
}
private void noteGoToPosition(GoToXYAction action, PageViewport pv, Point position) {
action.setPageIndex(pv.getPageIndex());
noteGoToPosition(action, position);
}
private void saveAbsolutePosition(String id, PageViewport pv,
int relativeIPP, int relativeBPP, AffineTransform tf) {
Point position = new Point(relativeIPP, relativeBPP);
tf.transform(position, position);
idPositions.put(id, position);
// is there already a GoTo action waiting to be completed?
GoToXYAction action = (GoToXYAction)actionSet.get(id);
if (action != null) {
noteGoToPosition(action, pv, position);
}
}
private void saveAbsolutePosition(String id, int relativeIPP, int relativeBPP) {
saveAbsolutePosition(id, this.currentPageViewport,
relativeIPP, relativeBPP, graphicContext.getTransform());
}
private void saveBlockPosIfTargetable(Block block) {
String id = getTargetableID(block);
if (hasDocumentNavigation() && 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;
}
saveAbsolutePosition(id, currentPageViewport, ipp, bpp, graphicContext.getTransform());
}
}
private void saveInlinePosIfTargetable(InlineArea inlineArea) {
String id = getTargetableID(inlineArea);
if (hasDocumentNavigation() && id != null) {
int extraMarginBefore = 5000; // millipoints
int ipp = currentIPPosition;
int bpp = currentBPPosition
+ inlineArea.getBlockProgressionOffset() - extraMarginBefore;
saveAbsolutePosition(id, ipp, bpp);
}
}
private 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;
}
}
/** {@inheritDoc} */
public void startPageSequence(PageSequence pageSequence) {
try {
if (this.inPageSequence) {
documentHandler.endPageSequence();
documentHandler.getContext().setLanguage(null);
} else {
if (this.documentMetadata == null) {
this.documentMetadata = createDefaultDocumentMetadata();
}
documentHandler.handleExtensionObject(this.documentMetadata);
documentHandler.endDocumentHeader();
this.inPageSequence = true;
}
establishForeignAttributes(pageSequence.getForeignAttributes());
documentHandler.getContext().setLanguage(pageSequence.getLocale());
documentHandler.startPageSequence(null);
resetForeignAttributes();
processExtensionAttachments(pageSequence);
} catch (IFException e) {
handleIFException(e);
}
}
private Metadata createDefaultDocumentMetadata() {
Metadata xmp = new Metadata();
DublinCoreAdapter dc = DublinCoreSchema.getAdapter(xmp);
if (getUserAgent().getTitle() != null) {
dc.setTitle(getUserAgent().getTitle());
}
if (getUserAgent().getAuthor() != null) {
dc.addCreator(getUserAgent().getAuthor());
}
if (getUserAgent().getKeywords() != null) {
dc.addSubject(getUserAgent().getKeywords());
}
XMPBasicAdapter xmpBasic = XMPBasicSchema.getAdapter(xmp);
if (getUserAgent().getProducer() != null) {
xmpBasic.setCreatorTool(getUserAgent().getProducer());
} else {
xmpBasic.setCreatorTool(Version.getVersion());
}
xmpBasic.setMetadataDate(new java.util.Date());
if (getUserAgent().getCreationDate() != null) {
xmpBasic.setCreateDate(getUserAgent().getCreationDate());
} else {
xmpBasic.setCreateDate(xmpBasic.getMetadataDate());
}
return xmp;
}
/** {@inheritDoc} */
public void preparePage(PageViewport page) {
super.preparePage(page);
}
/** {@inheritDoc} */
public void renderPage(PageViewport page) throws IOException, FOPException {
if (log.isTraceEnabled()) {
log.trace("renderPage() " + page);
}
try {
pageIndices.put(page.getKey(), page.getPageIndex());
Rectangle viewArea = page.getViewArea();
Dimension dim = new Dimension(viewArea.width, viewArea.height);
establishForeignAttributes(page.getForeignAttributes());
documentHandler.getContext().setPageIndex(page.getPageIndex());
documentHandler.getContext().setPageNumber(page.getPageNumber());
documentHandler.startPage(page.getPageIndex(), page.getPageNumberString(),
page.getSimplePageMasterName(), dim);
resetForeignAttributes();
documentHandler.startPageHeader();
//Add page attachments to page header
processExtensionAttachments(page);
documentHandler.endPageHeader();
this.painter = documentHandler.startPageContent();
super.renderPage(page);
this.painter = null;
documentHandler.endPageContent();
documentHandler.startPageTrailer();
if (hasDocumentNavigation()) {
Iterator iter = this.deferredLinks.iterator();
while (iter.hasNext()) {
Link link = (Link)iter.next();
iter.remove();
getDocumentNavigationHandler().renderLink(link);
}
}
documentHandler.endPageTrailer();
establishForeignAttributes(page.getForeignAttributes());
documentHandler.endPage();
documentHandler.getContext().setPageIndex(-1);
resetForeignAttributes();
} catch (IFException e) {
handleIFException(e);
}
}
private void processExtensionAttachments(AreaTreeObject area) throws IFException {
if (area.hasExtensionAttachments()) {
for (ExtensionAttachment attachment : area.getExtensionAttachments()) {
this.documentHandler.handleExtensionObject(attachment);
}
}
}
private void establishForeignAttributes(Map foreignAttributes) {
documentHandler.getContext().setForeignAttributes(foreignAttributes);
}
private void resetForeignAttributes() {
documentHandler.getContext().resetForeignAttributes();
}
private void establishStructureTreeElement(StructureTreeElement structureTreeElement) {
documentHandler.getContext().setStructureTreeElement(structureTreeElement);
}
private void resetStructurePointer() {
documentHandler.getContext().resetStructureTreeElement();
}
/** {@inheritDoc} */
protected void saveGraphicsState() {
graphicContextStack.push(graphicContext);
graphicContext = (IFGraphicContext)graphicContext.clone();
}
/** {@inheritDoc} */
protected void restoreGraphicsState() {
while (graphicContext.getGroupStackSize() > 0) {
IFGraphicContext.Group[] groups = graphicContext.dropGroups();
for (int i = groups.length - 1; i >= 0; i--) {
try {
groups[i].end(painter);
} catch (IFException ife) {
handleIFException(ife);
}
}
}
graphicContext = (IFGraphicContext)graphicContextStack.pop();
}
private void pushGroup(IFGraphicContext.Group group) {
graphicContext.pushGroup(group);
try {
group.start(painter);
} catch (IFException ife) {
handleIFException(ife);
}
}
/** {@inheritDoc} */
protected List breakOutOfStateStack() {
log.debug("Block.FIXED --> break out");
List breakOutList = new java.util.ArrayList();
while (!this.graphicContextStack.empty()) {
//Handle groups
IFGraphicContext.Group[] groups = graphicContext.getGroups();
for (int j = groups.length - 1; j >= 0; j--) {
try {
groups[j].end(painter);
} catch (IFException ife) {
handleIFException(ife);
}
}
breakOutList.add(0, this.graphicContext);
graphicContext = (IFGraphicContext)graphicContextStack.pop();
}
return breakOutList;
}
/** {@inheritDoc} */
protected void restoreStateStackAfterBreakOut(List breakOutList) {
log.debug("Block.FIXED --> restoring context after break-out");
for (Object aBreakOutList : breakOutList) {
graphicContextStack.push(graphicContext);
this.graphicContext = (IFGraphicContext) aBreakOutList;
//Handle groups
IFGraphicContext.Group[] groups = graphicContext.getGroups();
for (IFGraphicContext.Group group : groups) {
try {
group.start(painter);
} catch (IFException ife) {
handleIFException(ife);
}
}
}
log.debug("restored.");
}
/** {@inheritDoc} */
protected void concatenateTransformationMatrix(AffineTransform at) {
if (!at.isIdentity()) {
concatenateTransformationMatrixMpt(ptToMpt(at), false);
}
}
private void concatenateTransformationMatrixMpt(AffineTransform at, boolean force) {
if (force || !at.isIdentity()) {
if (log.isTraceEnabled()) {
log.trace("-----concatenateTransformationMatrix: " + at);
}
IFGraphicContext.Group group = new IFGraphicContext.Group(at);
pushGroup(group);
}
}
/** {@inheritDoc} */
protected void beginTextObject() {
//nop - Ignore, handled by painter internally
}
/** {@inheritDoc} */
protected void endTextObject() {
//nop - Ignore, handled by painter internally
}
/** {@inheritDoc} */
protected void renderRegionViewport(RegionViewport viewport) {
Dimension dim = new Dimension(viewport.getIPD(), viewport.getBPD());
viewportDimensionStack.push(dim);
documentHandler.getContext().setRegionType(viewport.getRegionReference().getRegionClass());
super.renderRegionViewport(viewport);
viewportDimensionStack.pop();
}
/** {@inheritDoc} */
protected void renderBlockViewport(BlockViewport bv, List children) {
//Essentially the same code as in the super class but optimized for the IF
// Handle new layer.
boolean inNewLayer = false;
if (maybeStartLayer(bv)) {
inNewLayer = true;
}
//This is the content-rect
Dimension dim = new Dimension(bv.getIPD(), bv.getBPD());
viewportDimensionStack.push(dim);
// save positions
int saveIP = currentIPPosition;
int saveBP = currentBPPosition;
CTM ctm = bv.getCTM();
int borderPaddingStart = bv.getBorderAndPaddingWidthStart();
int borderPaddingBefore = bv.getBorderAndPaddingWidthBefore();
if (bv.getPositioning() == Block.ABSOLUTE
|| bv.getPositioning() == Block.FIXED) {
//For FIXED, we need to break out of the current viewports to the
//one established by the page. We save the state stack for restoration
//after the block-container has been painted. See below.
List breakOutList = null;
if (bv.getPositioning() == Block.FIXED) {
breakOutList = breakOutOfStateStack();
}
AffineTransform positionTransform = new AffineTransform();
positionTransform.translate(bv.getXOffset(), bv.getYOffset());
//"left/"top" (bv.getX/YOffset()) specify the position of the content rectangle
positionTransform.translate(-borderPaddingStart, -borderPaddingBefore);
//Free transformation for the block-container viewport
String transf;
transf = bv.getForeignAttributeValue(FOX_TRANSFORM);
if (transf != null) {
AffineTransform freeTransform = AWTTransformProducer.createAffineTransform(transf);
positionTransform.concatenate(freeTransform);
}
saveGraphicsState();
//Viewport position
concatenateTransformationMatrixMpt(positionTransform, false);
//Background and borders
float bpwidth = (borderPaddingStart + bv.getBorderAndPaddingWidthEnd());
float bpheight = (borderPaddingBefore + bv.getBorderAndPaddingWidthAfter());
drawBackAndBorders(bv, 0, 0,
(dim.width + bpwidth) / 1000f, (dim.height + bpheight) / 1000f);
//Shift to content rectangle after border painting
AffineTransform contentRectTransform = new AffineTransform();
contentRectTransform.translate(borderPaddingStart, borderPaddingBefore);
concatenateTransformationMatrixMpt(contentRectTransform, false);
//saveGraphicsState();
//Set up coordinate system for content rectangle
AffineTransform contentTransform = ctm.toAffineTransform();
//concatenateTransformationMatrixMpt(contentTransform);
startViewport(contentTransform, bv.getClipRectangle());
currentIPPosition = 0;
currentBPPosition = 0;
renderBlocks(bv, children);
endViewport();
//restoreGraphicsState();
restoreGraphicsState();
if (breakOutList != null) {
restoreStateStackAfterBreakOut(breakOutList);
}
currentIPPosition = saveIP;
currentBPPosition = saveBP;
} else {
currentBPPosition += bv.getSpaceBefore();
//borders and background in the old coordinate system
handleBlockTraits(bv);
//Advance to start of content area
currentIPPosition += bv.getStartIndent();
CTM tempctm = new CTM(containingIPPosition, currentBPPosition);
ctm = tempctm.multiply(ctm);
//Now adjust for border/padding
currentBPPosition += borderPaddingBefore;
startVParea(ctm, bv.getClipRectangle());
currentIPPosition = 0;
currentBPPosition = 0;
renderBlocks(bv, children);
endVParea();
currentIPPosition = saveIP;
currentBPPosition = saveBP;
currentBPPosition += bv.getAllocBPD();
}
viewportDimensionStack.pop();
maybeEndLayer(bv, inNewLayer);
}
/** {@inheritDoc} */
public void renderInlineViewport(InlineViewport viewport) {
StructureTreeElement structElem
= (StructureTreeElement) viewport.getTrait(Trait.STRUCTURE_TREE_ELEMENT);
establishStructureTreeElement(structElem);
pushID(viewport);
Dimension dim = new Dimension(viewport.getIPD(), viewport.getBPD());
viewportDimensionStack.push(dim);
super.renderInlineViewport(viewport);
viewportDimensionStack.pop();
resetStructurePointer();
popID(viewport);
}
/** {@inheritDoc} */
protected void startVParea(CTM ctm, Rectangle clippingRect) {
if (log.isTraceEnabled()) {
log.trace("startVParea() ctm=" + ctm + ", clippingRect=" + clippingRect);
}
AffineTransform at = new AffineTransform(ctm.toArray());
startViewport(at, clippingRect);
if (log.isTraceEnabled()) {
log.trace("startVPArea: " + at + " --> " + graphicContext.getTransform());
}
}
private void startViewport(AffineTransform at, Rectangle clipRect) {
saveGraphicsState();
try {
IFGraphicContext.Viewport viewport = new IFGraphicContext.Viewport(
at, (Dimension)viewportDimensionStack.peek(), clipRect);
graphicContext.pushGroup(viewport);
viewport.start(painter);
} catch (IFException e) {
handleIFException(e);
}
}
/** {@inheritDoc} */
protected void endVParea() {
log.trace("endVParea()");
endViewport();
if (log.isTraceEnabled()) {
log.trace("endVPArea() --> " + graphicContext.getTransform());
}
}
private void endViewport() {
restoreGraphicsState();
}
/** {@inheritDoc} */
protected void startLayer(String layer) {
if (log.isTraceEnabled()) {
log.trace("startLayer() layer=" + layer);
}
saveGraphicsState();
pushGroup(new IFGraphicContext.Group(layer));
}
/** {@inheritDoc} */
protected void endLayer() {
if (log.isTraceEnabled()) {
log.trace("endLayer()");
}
restoreGraphicsState();
}
/** {@inheritDoc} */
protected void renderInlineArea(InlineArea inlineArea) {
saveInlinePosIfTargetable(inlineArea);
pushID(inlineArea);
super.renderInlineArea(inlineArea);
popID(inlineArea);
}
/** {@inheritDoc} */
public void renderInlineParent(InlineParent ip) {
// stuff we only need if a link must be created:
Rectangle ipRect = null;
AbstractAction action = null;
// make sure the rect is determined *before* calling super!
int ipp = currentIPPosition;
int bpp = currentBPPosition + ip.getBlockProgressionOffset();
ipRect = new Rectangle(ipp, bpp, ip.getIPD(), ip.getBPD());
AffineTransform transform = graphicContext.getTransform();
ipRect = transform.createTransformedShape(ipRect).getBounds();
// render contents
super.renderInlineParent(ip);
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) {
Integer pageIndex = (Integer)pageIndices.get(pvKey);
action = getGoToActionForID(idRef, (pageIndex != null ? pageIndex : -1));
} else {
//Warnings already issued by AreaTreeHandler
}
}
// no INTERNAL_LINK, look for EXTERNAL_LINK
if (!linkTraitFound) {
Trait.ExternalLink extLink = (Trait.ExternalLink) ip.getTrait(Trait.EXTERNAL_LINK);
if (extLink != null) {
String extDest = extLink.getDestination();
if (extDest != null && extDest.length() > 0) {
linkTraitFound = true;
action = new URIAction(extDest, extLink.newWindow());
action = actionSet.put(action);
}
}
}
// warn if link trait found but not allowed, else create link
if (linkTraitFound) {
StructureTreeElement structElem
= (StructureTreeElement) ip.getTrait(Trait.STRUCTURE_TREE_ELEMENT);
action.setStructureTreeElement(structElem);
Link link = new Link(action, ipRect);
this.deferredLinks.add(link);
} else if (ip instanceof BasicLinkArea) {
BasicLinkArea linkArea = (BasicLinkArea) ip;
String id = linkArea.getResolver().getIDRefs()[0];
action = getGoToActionForID(id, -1);
Link link = new Link(action, ipRect);
this.deferredLinks.add(link);
}
}
/** {@inheritDoc} */
protected void renderBlock(Block block) {
if (log.isTraceEnabled()) {
log.trace("renderBlock() " + block);
}
saveBlockPosIfTargetable(block);
pushID(block);
IFContext context = documentHandler.getContext();
Locale oldLocale = context.getLanguage();
context.setLanguage(block.getLocale());
String oldLocation = context.getLocation();
context.setLocation(block.getLocation());
super.renderBlock(block);
context.setLocation(oldLocation);
context.setLanguage(oldLocale);
popID(block);
}
private void pushID(Area area) {
String prodID = (String) area.getTrait(Trait.PROD_ID);
if (prodID != null) {
ids.push(prodID);
documentHandler.getContext().setID(prodID);
}
}
private void popID(Area area) {
String prodID = (String) area.getTrait(Trait.PROD_ID);
if (prodID != null) {
ids.pop();
documentHandler.getContext().setID(ids.empty() ? "" : ids.peek());
}
}
private Typeface getTypeface(String fontName) {
Typeface tf = fontInfo.getFonts().get(fontName);
if (tf instanceof LazyFont) {
tf = ((LazyFont)tf).getRealFont();
}
return tf;
}
/** {@inheritDoc} */
protected void renderText(TextArea text) {
if (log.isTraceEnabled()) {
log.trace("renderText() " + text);
}
renderInlineAreaBackAndBorders(text);
Color ct = (Color) text.getTrait(Trait.COLOR);
beginTextObject();
String fontName = getInternalFontNameForArea(text);
int size = (Integer) text.getTrait(Trait.FONT_SIZE);
StructureTreeElement structElem
= (StructureTreeElement) text.getTrait(Trait.STRUCTURE_TREE_ELEMENT);
establishStructureTreeElement(structElem);
// This assumes that *all* CIDFonts use a /ToUnicode mapping
Typeface tf = getTypeface(fontName);
FontTriplet triplet = (FontTriplet)text.getTrait(Trait.FONT);
try {
painter.setFont(triplet.getName(), triplet.getStyle(), triplet.getWeight(),
"normal", size, ct);
} catch (IFException e) {
handleIFException(e);
}
int rx = currentIPPosition + text.getBorderAndPaddingWidthStart();
int bl = currentBPPosition + text.getBlockProgressionOffset() + text.getBaselineOffset();
textUtil.flush();
textUtil.setStartPosition(rx, bl);
textUtil.setSpacing(text.getTextLetterSpaceAdjust(), text.getTextWordSpaceAdjust());
documentHandler.getContext().setHyphenated(text.isHyphenated());
super.renderText(text);
textUtil.flush();
renderTextDecoration(tf, size, text, bl, rx);
documentHandler.getContext().setHyphenated(false);
resetStructurePointer();
}
/** {@inheritDoc} */
protected void renderWord(WordArea word) {
Font font = getFontFromArea(word.getParentArea());
String s = word.getWord();
int[][] dp = word.getGlyphPositionAdjustments();
Area parentArea = word.getParentArea();
assert (parentArea instanceof AbstractTextArea);
if (dp == null) {
renderTextWithAdjustments(s, word.getLetterAdjustArray(), word.isReversed(),
font, (AbstractTextArea) parentArea);
} else if (IFUtil.isDPOnlyDX(dp)) {
renderTextWithAdjustments(s, IFUtil.convertDPToDX(dp), word.isReversed(),
font, (AbstractTextArea) parentArea);
} else {
renderTextWithAdjustments(s, dp, word.isReversed(),
font, (AbstractTextArea) parentArea);
}
textUtil.nextIsSpace = word.isNextIsSpace();
super.renderWord(word);
}
/** {@inheritDoc} */
protected void renderSpace(SpaceArea space) {
Font font = getFontFromArea(space.getParentArea());
String s = space.getSpace();
Area parentArea = space.getParentArea();
assert (parentArea instanceof AbstractTextArea);
AbstractTextArea textArea = (AbstractTextArea) parentArea;
renderTextWithAdjustments(s, (int[]) null, false, font, textArea);
/* COMBINED is always false
if (textUtil.COMBINED && space.isAdjustable()) {
//Used for justified text, for example
int tws = textArea.getTextWordSpaceAdjust()
+ 2 * textArea.getTextLetterSpaceAdjust();
if (tws != 0) {
textUtil.adjust(tws);
}
}
*/
super.renderSpace(space);
}
/**
* Does low-level rendering of text using DX only position adjustments.
* @param s text to render
* @param dx an array of widths for letter adjustment (may be null)
* @param reversed if true then text has been reversed (from logical order)
* @param font to font in use
* @param parentArea the parent text area to retrieve certain traits from
*/
private void renderTextWithAdjustments(String s,
int[] dx, boolean reversed,
Font font, AbstractTextArea parentArea) {
int l = s.length();
if (l == 0) {
return;
}
for (int i = 0; i < l; i++) {
char ch = s.charAt(i);
textUtil.addChar(ch);
int glyphAdjust = 0;
/* COMBINED is always false
if (textUtil.COMBINED && font.hasChar(ch)) {
int tls = (i < l - 1 ? parentArea.getTextLetterSpaceAdjust() : 0);
glyphAdjust += tls;
}
*/
if (dx != null && i < l) {
glyphAdjust += dx[i];
}
textUtil.adjust(glyphAdjust);
}
}
/**
* Does low-level rendering of text using generalized position adjustments.
* @param s text to render
* @param dp an array of 4-tuples, expressing [X,Y] placment
* adjustments and [X,Y] advancement adjustments, in that order (may be null)
* @param reversed if true then text has been reversed (from logical order)
* @param font to font in use
* @param parentArea the parent text area to retrieve certain traits from
*/
private void renderTextWithAdjustments(String s,
int[][] dp, boolean reversed,
Font font, AbstractTextArea parentArea) {
// assert !textUtil.COMBINED;
for (int i = 0, n = s.length(); i < n; i++) {
textUtil.addChar(s.charAt(i));
if (dp != null) {
textUtil.adjust(dp[i]);
}
}
}
private class TextUtil {
private static final int INITIAL_BUFFER_SIZE = 16;
private int[][] dp = new int[INITIAL_BUFFER_SIZE][];
private final StringBuffer text = new StringBuffer();
private int startx;
private int starty;
private int tls;
private int tws;
private boolean nextIsSpace;
void addChar(char ch) {
text.append(ch);
}
void adjust(int dx) {
if (dx != 0) {
adjust(new int[] {
dx, // xPlaAdjust
0, // yPlaAdjust
dx, // xAdvAdjust
0 // yAdvAdjust
});
}
}
void adjust(int[] pa) {
if (!IFUtil.isPAIdentity(pa)) {
int idx = text.length();
if (idx > dp.length - 1) {
int newSize = Math.max(dp.length, idx + 1) + INITIAL_BUFFER_SIZE;
int[][] newDP = new int[newSize][];
// reuse prior DP[0]...DP[dp.length-1]
System.arraycopy(dp, 0, newDP, 0, dp.length);
// switch to new DP, leaving DP[dp.length]...DP[newDP.length-1] unpopulated
dp = newDP;
}
if (dp[idx - 1] == null) {
dp[idx - 1] = new int[4];
}
IFUtil.adjustPA(dp[idx - 1], pa);
}
}
void reset() {
if (text.length() > 0) {
text.setLength(0);
for (int i = 0, n = dp.length; i < n; i++) {
dp[i] = null;
}
}
}
void setStartPosition(int x, int y) {
this.startx = x;
this.starty = y;
}
void setSpacing(int tls, int tws) {
this.tls = tls;
this.tws = tws;
}
void flush() {
if (text.length() > 0) {
try {
/* if (COMBINED) { // COMBINED is always false
painter.drawText(startx, starty, 0, 0,
trimAdjustments(dp, text.length()), text.toString());
} else { */
painter.drawText(startx, starty, tls, tws,
trimAdjustments(dp, text.length()), text.toString(), nextIsSpace);
/* } */
} catch (IFException e) {
handleIFException(e);
}
reset();
}
}
void drawText(int x, int y, int letterSpacing, int wordSpacing, int[][] dx, String text, boolean nextIsSpace)
throws IFException {
painter.drawText(startx, starty, tls, tws, dx, text, nextIsSpace);
}
/**
* Trim adjustments array <code>dp</code> to be no greater length than
* text length, and where trailing all-zero entries are removed.
* @param dp a position adjustments array (or null)
* @param textLength the length of the associated text
* @return either the original value of <code>dp</code> or a copy
* of its first N significant adjustment entries, such that N is
* no greater than text length, and the last entry has a non-zero
* adjustment.
*/
private int[][] trimAdjustments(int[][] dp, int textLength) {
if (dp != null) {
int tl = textLength;
int pl = dp.length;
int i = (tl < pl) ? tl : pl;
while (i > 0) {
int[] pa = dp [ i - 1 ];
if ((pa != null) && !IFUtil.isPAIdentity(pa)) {
break;
} else {
i--;
}
}
if (i == 0) {
dp = null;
} else if (i < pl) {
dp = IFUtil.copyDP(dp, 0, i);
}
}
return dp;
}
}
/** {@inheritDoc} */
public void renderImage(Image image, Rectangle2D pos) {
drawImage(image.getURL(), pos, image.getForeignAttributes());
}
/** {@inheritDoc} */
protected void drawImage(String uri, Rectangle2D pos, Map foreignAttributes) {
Rectangle posInt = new Rectangle(
currentIPPosition + (int)pos.getX(),
currentBPPosition + (int)pos.getY(),
(int)pos.getWidth(),
(int)pos.getHeight());
uri = URISpecification.getURL(uri);
try {
establishForeignAttributes(foreignAttributes);
painter.drawImage(uri, posInt);
resetForeignAttributes();
} catch (IFException ife) {
handleIFException(ife);
}
}
/** {@inheritDoc} */
public void renderForeignObject(ForeignObject fo, Rectangle2D pos) {
endTextObject();
Rectangle posInt = new Rectangle(
currentIPPosition + (int)pos.getX(),
currentBPPosition + (int)pos.getY(),
(int)pos.getWidth(),
(int)pos.getHeight());
Document doc = fo.getDocument();
try {
establishForeignAttributes(fo.getForeignAttributes());
painter.drawImage(doc, posInt);
resetForeignAttributes();
} catch (IFException ife) {
handleIFException(ife);
}
}
/** {@inheritDoc} */
public void renderLeader(Leader area) {
renderInlineAreaBackAndBorders(area);
int style = area.getRuleStyle();
int ruleThickness = area.getRuleThickness();
int startx = currentIPPosition + area.getBorderAndPaddingWidthStart();
int starty = currentBPPosition + area.getBlockProgressionOffset() + (ruleThickness / 2);
int endx = currentIPPosition
+ area.getBorderAndPaddingWidthStart()
+ area.getIPD();
Color col = (Color)area.getTrait(Trait.COLOR);
Point start = new Point(startx, starty);
Point end = new Point(endx, starty);
try {
painter.drawLine(start, end, ruleThickness, col, RuleStyle.valueOf(style));
} catch (IFException ife) {
handleIFException(ife);
}
super.renderLeader(area);
}
/** {@inheritDoc} */
protected void clip() {
throw new IllegalStateException("Not used");
}
/** {@inheritDoc} */
protected void clipRect(float x, float y, float width, float height) {
pushGroup(new IFGraphicContext.Group());
try {
painter.clipRect(toMillipointRectangle(x, y, width, height));
} catch (IFException ife) {
handleIFException(ife);
}
}
/** {@inheritDoc} */
protected void clipBackground(float startx, float starty,
float width, float height,
BorderProps bpsBefore, BorderProps bpsAfter,
BorderProps bpsStart, BorderProps bpsEnd) {
pushGroup(new IFGraphicContext.Group());
Rectangle rect = toMillipointRectangle(startx, starty, width, height);
try {
painter.clipBackground(rect,
bpsBefore, bpsAfter, bpsStart, bpsEnd);
} catch (IFException ife) {
handleIFException(ife);
}
}
/** {@inheritDoc} */
protected void closePath() {
throw new IllegalStateException("Not used");
}
/** {@inheritDoc} */
protected void drawBackground(float startx, float starty,
float width, float height,
Trait.Background back,
BorderProps bpsBefore, BorderProps bpsAfter,
BorderProps bpsStart, BorderProps bpsEnd) {
if (painter.isBackgroundRequired(bpsBefore, bpsAfter, bpsStart, bpsEnd)) {
super.drawBackground(startx, starty, width, height,
back, bpsBefore, bpsAfter,
bpsStart, bpsEnd);
}
}
/** {@inheritDoc} */
protected void drawBorders(float startx, float starty,
float width, float height,
BorderProps bpsBefore, BorderProps bpsAfter,
BorderProps bpsStart, BorderProps bpsEnd, int level, Color innerBackgroundColor) {
Rectangle rect = toMillipointRectangle(startx, starty, width, height);
try {
BorderProps bpsTop = bpsBefore;
BorderProps bpsBottom = bpsAfter;
BorderProps bpsLeft;
BorderProps bpsRight;
if ((level == -1) || ((level & 1) == 0)) {
bpsLeft = bpsStart;
bpsRight = bpsEnd;
} else {
bpsLeft = bpsEnd;
bpsRight = bpsStart;
}
painter.drawBorderRect(rect, bpsTop, bpsBottom, bpsLeft, bpsRight, innerBackgroundColor);
} catch (IFException ife) {
handleIFException(ife);
}
}
/** {@inheritDoc} */
protected void drawBorderLine(float x1, float y1, float x2, float y2, boolean horz,
boolean startOrBefore, int style, Color col) {
//Simplified implementation that is only used by renderTextDecoration()
//drawBorders() is overridden and uses the Painter's high-level method drawBorderRect()
updateColor(col, true);
fillRect(x1, y1, x2 - x1, y2 - y1);
}
private int toMillipoints(float coordinate) {
return Math.round(coordinate * 1000);
}
private Rectangle toMillipointRectangle(float x, float y, float width, float height) {
return new Rectangle(
toMillipoints(x),
toMillipoints(y),
toMillipoints(width),
toMillipoints(height));
}
/** {@inheritDoc} */
protected void fillRect(float x, float y, float width, float height) {
try {
painter.fillRect(
toMillipointRectangle(x, y, width, height),
this.graphicContext.getPaint());
} catch (IFException e) {
handleIFException(e);
}
}
/** {@inheritDoc} */
protected void moveTo(float x, float y) {
throw new IllegalStateException("Not used");
}
/** {@inheritDoc} */
protected void lineTo(float x, float y) {
throw new IllegalStateException("Not used");
}
/** {@inheritDoc} */
protected void updateColor(Color col, boolean fill) {
if (fill) {
this.graphicContext.setPaint(col);
} else {
this.graphicContext.setColor(col);
}
}
}