blob: 13ac401a7d0a03d3cda9fa3fc130c85ae027561b [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.Arrays;
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.batik.parser.AWTTransformProducer;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
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.apps.FOPException;
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.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.Leader;
import org.apache.fop.area.inline.SpaceArea;
import org.apache.fop.area.inline.TextArea;
import org.apache.fop.area.inline.Viewport;
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.render.pdf.PDFEventProducer;
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 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 = false;
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();
/**
* Main constructor
*/
public IFRenderer() {
}
/** {@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 {
if (ife.getCause() instanceof IOException) {
throw (IOException)ife.getCause();
} 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() {
IFSerializer serializer = new IFSerializer();
serializer.setContext(new IFContext(getUserAgent()));
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.");
}
/** {@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);
} else {
// Not complete yet, can't use getPDFGoTo:
action = new GoToXYAction(targetID, pageIndex, null);
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);
}
PDFEventProducer eventProducer = PDFEventProducer.Provider.get(
getUserAgent().getEventBroadcaster());
eventProducer.nonFullyResolvedLinkTargets(this, count);
// 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.getOffset() - 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(toLocale(pageSequence));
documentHandler.startPageSequence(null);
resetForeignAttributes();
processExtensionAttachments(pageSequence);
} catch (IFException e) {
handleIFException(e);
}
}
private Locale toLocale(PageSequence pageSequence) {
if (pageSequence.getLanguage() != null) {
if (pageSequence.getCountry() != null) {
return new Locale(pageSequence.getLanguage(), pageSequence.getCountry());
} else {
return new Locale(pageSequence.getLanguage());
}
}
return null;
}
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(), new Integer(page.getPageIndex()));
Rectangle viewArea = page.getViewArea();
Dimension dim = new Dimension(viewArea.width, viewArea.height);
establishForeignAttributes(page.getForeignAttributes());
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();
resetForeignAttributes();
} catch (IFException e) {
handleIFException(e);
}
}
private void processExtensionAttachments(AreaTreeObject area) throws IFException {
if (area.hasExtensionAttachments()) {
for (Iterator iter = area.getExtensionAttachments().iterator();
iter.hasNext();) {
ExtensionAttachment attachment = (ExtensionAttachment) iter.next();
this.documentHandler.handleExtensionObject(attachment);
}
}
}
private void establishForeignAttributes(Map foreignAttributes) {
documentHandler.getContext().setForeignAttributes(foreignAttributes);
}
private void resetForeignAttributes() {
documentHandler.getContext().resetForeignAttributes();
}
private void establishStructurePointer(String ptr) {
documentHandler.getContext().setStructurePointer(ptr);
}
private void resetStructurePointer() {
documentHandler.getContext().resetStructurePointer();
}
/** {@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 (int i = 0, c = breakOutList.size(); i < c; i++) {
graphicContextStack.push(graphicContext);
this.graphicContext = (IFGraphicContext)breakOutList.get(i);
//Handle groups
IFGraphicContext.Group[] groups = graphicContext.getGroups();
for (int j = 0, jc = groups.length; j < jc; j++) {
try {
groups[j].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);
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
//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);
//Clipping
Rectangle clipRect = null;
if (bv.getClip()) {
clipRect = new Rectangle(0, 0, dim.width, dim.height);
//clipRect(0f, 0f, width, height);
}
//saveGraphicsState();
//Set up coordinate system for content rectangle
AffineTransform contentTransform = ctm.toAffineTransform();
//concatenateTransformationMatrixMpt(contentTransform);
startViewport(contentTransform, clipRect);
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;
Rectangle2D clippingRect = null;
if (bv.getClip()) {
clippingRect = new Rectangle(currentIPPosition, currentBPPosition,
bv.getIPD(), bv.getBPD());
}
startVParea(ctm, clippingRect);
currentIPPosition = 0;
currentBPPosition = 0;
renderBlocks(bv, children);
endVParea();
currentIPPosition = saveIP;
currentBPPosition = saveBP;
currentBPPosition += bv.getAllocBPD();
}
viewportDimensionStack.pop();
}
/** {@inheritDoc} */
public void renderViewport(Viewport viewport) {
String ptr = (String) viewport.getTrait(Trait.PTR);
establishStructurePointer(ptr);
Dimension dim = new Dimension(viewport.getIPD(), viewport.getBPD());
viewportDimensionStack.push(dim);
super.renderViewport(viewport);
viewportDimensionStack.pop();
resetStructurePointer();
}
/** {@inheritDoc} */
protected void startVParea(CTM ctm, Rectangle2D clippingRect) {
if (log.isTraceEnabled()) {
log.trace("startVParea() ctm=" + ctm + ", clippingRect=" + clippingRect);
}
AffineTransform at = new AffineTransform(ctm.toArray());
Rectangle clipRect = null;
if (clippingRect != null) {
clipRect = new Rectangle(
(int)clippingRect.getMinX() - currentIPPosition,
(int)clippingRect.getMinY() - currentBPPosition,
(int)clippingRect.getWidth(), (int)clippingRect.getHeight());
}
startViewport(at, clipRect);
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 renderInlineArea(InlineArea inlineArea) {
saveInlinePosIfTargetable(inlineArea);
super.renderInlineArea(inlineArea);
}
/** {@inheritDoc} */
public void renderInlineParent(InlineParent ip) {
// stuff we only need if a link must be created:
Rectangle ipRect = null;
AbstractAction action = null;
String ptr = (String) ip.getTrait(Trait.PTR); // used for accessibility
// make sure the rect is determined *before* calling super!
int ipp = currentIPPosition;
int bpp = currentBPPosition + ip.getOffset();
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.intValue() : -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) {
action.setStructurePointer(ptr); // used for accessibility
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);
super.renderBlock(block);
}
private Typeface getTypeface(String fontName) {
Typeface tf = (Typeface) 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)).intValue();
String ptr = (String)text.getTrait(Trait.PTR); // used for accessibility
establishStructurePointer(ptr);
// 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(), new Integer(triplet.getWeight()),
"normal", new Integer(size), ct);
} catch (IFException e) {
handleIFException(e);
}
int rx = currentIPPosition + text.getBorderAndPaddingWidthStart();
int bl = currentBPPosition + text.getOffset() + text.getBaselineOffset();
textUtil.flush();
textUtil.setStartPosition(rx, bl);
textUtil.setSpacing(text.getTextLetterSpaceAdjust(), text.getTextWordSpaceAdjust());
super.renderText(text);
textUtil.flush();
renderTextDecoration(tf, size, text, bl, rx);
resetStructurePointer();
}
/** {@inheritDoc} */
protected void renderWord(WordArea word) {
Font font = getFontFromArea(word.getParentArea());
String s = word.getWord();
renderText(s, word.getLetterAdjustArray(),
font, (AbstractTextArea)word.getParentArea());
super.renderWord(word);
}
/** {@inheritDoc} */
protected void renderSpace(SpaceArea space) {
Font font = getFontFromArea(space.getParentArea());
String s = space.getSpace();
AbstractTextArea textArea = (AbstractTextArea)space.getParentArea();
renderText(s, null, font, textArea);
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.
* @param s text to render
* @param letterAdjust an array of widths for letter adjustment (may be null)
* @param font to font in use
* @param parentArea the parent text area to retrieve certain traits from
*/
protected void renderText(String s,
int[] letterAdjust,
Font font, AbstractTextArea parentArea) {
int l = s.length();
if (l == 0) {
return;
}
if (letterAdjust != null) {
textUtil.adjust(letterAdjust[0]);
}
for (int i = 0; i < l; i++) {
char ch = s.charAt(i);
textUtil.addChar(ch);
int glyphAdjust = 0;
if (textUtil.combined && font.hasChar(ch)) {
int tls = (i < l - 1 ? parentArea.getTextLetterSpaceAdjust() : 0);
glyphAdjust += tls;
}
if (letterAdjust != null && i < l - 1) {
glyphAdjust += letterAdjust[i + 1];
}
textUtil.adjust(glyphAdjust);
}
}
private class TextUtil {
private static final int INITIAL_BUFFER_SIZE = 16;
private int[] dx = new int[INITIAL_BUFFER_SIZE];
private int lastDXPos = 0;
private final StringBuffer text = new StringBuffer();
private int startx, starty;
private int tls, tws;
private final boolean combined = false;
void addChar(char ch) {
text.append(ch);
}
void adjust(int adjust) {
if (adjust != 0) {
int idx = text.length();
if (idx > dx.length - 1) {
int newSize = Math.max(dx.length, idx + 1) + INITIAL_BUFFER_SIZE;
int[] newDX = new int[newSize];
System.arraycopy(dx, 0, newDX, 0, dx.length);
dx = newDX;
}
dx[idx] += adjust;
lastDXPos = idx;
}
}
void reset() {
if (text.length() > 0) {
text.setLength(0);
Arrays.fill(dx, 0);
lastDXPos = 0;
}
}
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 {
int[] effDX = null;
if (lastDXPos > 0) {
int size = lastDXPos + 1;
effDX = new int[size];
System.arraycopy(dx, 0, effDX, 0, size);
}
if (combined) {
painter.drawText(startx, starty, 0, 0, effDX, text.toString());
} else {
painter.drawText(startx, starty, tls, tws, effDX, text.toString());
}
} catch (IFException e) {
handleIFException(e);
}
reset();
}
}
}
/** {@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.getOffset() + (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 closePath() {
throw new IllegalStateException("Not used");
}
/** {@inheritDoc} */
protected void drawBorders(float startx, float starty,
float width, float height,
BorderProps bpsBefore, BorderProps bpsAfter,
BorderProps bpsStart, BorderProps bpsEnd) {
Rectangle rect = toMillipointRectangle(startx, starty, width, height);
try {
painter.drawBorderRect(rect, bpsBefore, bpsAfter, bpsStart, bpsEnd);
} 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);
}
}
}