/*
* This program is free software; you can redistribute it and/or modify it under the
* terms of the GNU Lesser General Public License, version 2.1 as published by the Free Software
* Foundation.
*
* You should have received a copy of the GNU Lesser General Public License along with this
* program; if not, you can obtain a copy at http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html
* or from the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the GNU Lesser General Public License for more details.
*
* Copyright (c) 2001 - 2009 Object Refinery Ltd, Pentaho Corporation and Contributors.. All rights reserved.
*/
package org.pentaho.reporting.engine.classic.core.modules.output.pageable.graphics.internal;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.RenderingHints;
import java.awt.Stroke;
import java.awt.Shape;
import java.awt.geom.AffineTransform;
import java.awt.geom.Line2D;
import java.awt.geom.Rectangle2D;
import java.awt.print.PageFormat;
import java.awt.print.Paper;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.pentaho.reporting.engine.classic.core.ElementAlignment;
import org.pentaho.reporting.engine.classic.core.LocalImageContainer;
import org.pentaho.reporting.engine.classic.core.URLImageContainer;
import org.pentaho.reporting.engine.classic.core.layout.ModelPrinter;
import org.pentaho.reporting.engine.classic.core.layout.model.BlockRenderBox;
import org.pentaho.reporting.engine.classic.core.layout.model.CanvasRenderBox;
import org.pentaho.reporting.engine.classic.core.layout.model.InlineRenderBox;
import org.pentaho.reporting.engine.classic.core.layout.model.LayoutNodeTypes;
import org.pentaho.reporting.engine.classic.core.layout.model.LogicalPageBox;
import org.pentaho.reporting.engine.classic.core.layout.model.ParagraphRenderBox;
import org.pentaho.reporting.engine.classic.core.layout.model.RenderBox;
import org.pentaho.reporting.engine.classic.core.layout.model.RenderNode;
import org.pentaho.reporting.engine.classic.core.layout.model.RenderableReplacedContentBox;
import org.pentaho.reporting.engine.classic.core.layout.model.RenderableText;
import org.pentaho.reporting.engine.classic.core.layout.output.CollectSelectedNodesStep;
import org.pentaho.reporting.engine.classic.core.layout.output.OutputProcessorFeature;
import org.pentaho.reporting.engine.classic.core.layout.output.OutputProcessorMetaData;
import org.pentaho.reporting.engine.classic.core.layout.output.RenderUtility;
import org.pentaho.reporting.engine.classic.core.layout.process.IterateStructuralProcessStep;
import org.pentaho.reporting.engine.classic.core.layout.process.RevalidateTextEllipseProcessStep;
import org.pentaho.reporting.engine.classic.core.layout.text.ExtendedBaselineInfo;
import org.pentaho.reporting.engine.classic.core.layout.text.Glyph;
import org.pentaho.reporting.engine.classic.core.layout.text.GlyphList;
import org.pentaho.reporting.engine.classic.core.modules.output.pageable.graphics.PageDrawable;
import org.pentaho.reporting.engine.classic.core.modules.output.pageable.pdf.internal.PdfLogicalPageDrawable;
import org.pentaho.reporting.engine.classic.core.style.BandStyleKeys;
import org.pentaho.reporting.engine.classic.core.style.ElementStyleKeys;
import org.pentaho.reporting.engine.classic.core.style.StyleKey;
import org.pentaho.reporting.engine.classic.core.style.StyleSheet;
import org.pentaho.reporting.engine.classic.core.style.TextStyleKeys;
import org.pentaho.reporting.engine.classic.core.util.geom.StrictBounds;
import org.pentaho.reporting.engine.classic.core.util.geom.StrictGeomUtility;
import org.pentaho.reporting.libraries.base.util.DebugLog;
import org.pentaho.reporting.libraries.base.util.WaitingImageObserver;
import org.pentaho.reporting.libraries.fonts.encoding.CodePointBuffer;
import org.pentaho.reporting.libraries.resourceloader.Resource;
import org.pentaho.reporting.libraries.resourceloader.ResourceException;
import org.pentaho.reporting.libraries.resourceloader.ResourceKey;
import org.pentaho.reporting.libraries.resourceloader.ResourceManager;
import org.pentaho.reporting.libraries.resourceloader.factory.drawable.DrawableWrapper;
/**
* The page drawable is the content provider for the Graphics2DOutputTarget. This component is responsible for rendering
* the current page to a Graphics2D object.
*
* @author Thomas Morgner
*/
public class LogicalPageDrawable extends IterateStructuralProcessStep implements PageDrawable
{
protected static class TextSpec
{
private boolean bold;
private boolean italics;
private String fontName;
private float fontSize;
private OutputProcessorMetaData metaData;
private Graphics2D graphics;
protected TextSpec(final StyleSheet layoutContext,
final OutputProcessorMetaData metaData,
final Graphics2D graphics)
{
if (graphics == null)
{
throw new NullPointerException();
}
if (metaData == null)
{
throw new NullPointerException();
}
if (layoutContext == null)
{
throw new NullPointerException();
}
this.graphics = graphics;
this.metaData = metaData;
fontName = metaData.getNormalizedFontFamilyName
((String) layoutContext.getStyleProperty(TextStyleKeys.FONT));
fontSize = (float) layoutContext.getDoubleStyleProperty(TextStyleKeys.FONTSIZE, 10);
bold = layoutContext.getBooleanStyleProperty(TextStyleKeys.BOLD);
italics = layoutContext.getBooleanStyleProperty(TextStyleKeys.ITALIC);
}
public boolean isSame(final StyleSheet layoutContext)
{
if (layoutContext == null)
{
throw new NullPointerException();
}
if (this.fontName.equals(metaData.getNormalizedFontFamilyName
((String) layoutContext.getStyleProperty(TextStyleKeys.FONT))) == false)
{
return false;
}
if (this.fontSize != (float) layoutContext.getDoubleStyleProperty(TextStyleKeys.FONTSIZE, 10))
{
return false;
}
if (bold != layoutContext.getBooleanStyleProperty(TextStyleKeys.BOLD))
{
return false;
}
if (italics == layoutContext.getBooleanStyleProperty(TextStyleKeys.ITALIC))
{
return false;
}
return true;
}
public boolean isBold()
{
return bold;
}
public boolean isItalics()
{
return italics;
}
public String getFontName()
{
return fontName;
}
public float getFontSize()
{
return fontSize;
}
public Graphics2D getGraphics()
{
return graphics;
}
public void close()
{
graphics.dispose();
graphics = null;
}
}
private static class FontDecorationSpec
{
private double end;
private double start;
private double verticalPosition;
private double lineWidth;
private Color textColor;
protected FontDecorationSpec()
{
start = -1;
end = -1;
}
public Color getTextColor()
{
return textColor;
}
public void setTextColor(final Color textColor)
{
this.textColor = textColor;
}
public void updateStart(final double start)
{
if (this.start < 0)
{
this.start = start;
}
else if (start < this.start)
{
this.start = start;
}
}
public double getEnd()
{
return end;
}
public void updateEnd(final double end)
{
if (this.end < 0)
{
this.end = end;
}
else if (end > this.end)
{
this.end = end;
}
}
public double getStart()
{
return start;
}
public double getLineWidth()
{
return lineWidth;
}
public void updateLineWidth(final double lineWidth)
{
if (lineWidth > this.lineWidth)
{
this.lineWidth = lineWidth;
}
}
public void updateVerticalPosition(final double verticalPosition)
{
if (verticalPosition > this.verticalPosition)
{
this.verticalPosition = verticalPosition;
}
}
public double getVerticalPosition()
{
return verticalPosition;
}
}
public static final BasicStroke DEFAULT_STROKE = new BasicStroke(1);
private static final Log logger = LogFactory.getLog(LogicalPageDrawable.class);
private FontDecorationSpec strikeThrough;
private FontDecorationSpec underline;
private boolean outlineMode;
private LogicalPageBox rootBox;
private OutputProcessorMetaData metaData;
private PageFormat pageFormat;
private double width;
private double height;
private CodePointBuffer codePointBuffer;
private Graphics2D graphics;
private boolean textLineOverflow;
private long contentAreaX1;
private long contentAreaX2;
private RevalidateTextEllipseProcessStep revalidateTextEllipseProcessStep;
private StrictBounds drawArea;
// A reusable rectangle for rendering; not used for decisions
private Rectangle2D.Double boxArea;
private TextSpec textSpec;
private boolean ellipseDrawn;
private CollectSelectedNodesStep collectSelectedNodesStep;
private BorderRenderer borderRenderer;
private boolean drawPageBackground;
private ResourceManager resourceManager;
private boolean clipOnWordBoundary;
private boolean strictClipping;
public LogicalPageDrawable(final LogicalPageBox rootBox,
final OutputProcessorMetaData metaData,
final ResourceManager resourceManager)
{
if (rootBox == null)
{
throw new NullPointerException();
}
if (metaData == null)
{
throw new NullPointerException();
}
if (resourceManager == null)
{
throw new NullPointerException();
}
this.borderRenderer = new BorderRenderer();
this.codePointBuffer = new CodePointBuffer(400);
this.resourceManager = resourceManager;
this.boxArea = new Rectangle2D.Double();
this.rootBox = rootBox;
this.metaData = metaData;
this.width = StrictGeomUtility.toExternalValue(rootBox.getPageWidth());
this.height = StrictGeomUtility.toExternalValue(rootBox.getPageHeight());
this.drawPageBackground = true;
final Paper paper = new Paper();
paper.setImageableArea(0, 0, width, height);
this.pageFormat = new PageFormat();
this.pageFormat.setPaper(paper);
this.strictClipping = "true".equals(metaData.getConfiguration().getConfigProperty
("org.pentaho.reporting.engine.classic.core.modules.output.pageable.graphics.StrictClipping"));
this.outlineMode = "true".equals(metaData.getConfiguration().getConfigProperty
("org.pentaho.reporting.engine.classic.core.modules.output.pageable.graphics.debug.OutlineMode"));
if ("true".equals(metaData.getConfiguration().getConfigProperty
("org.pentaho.reporting.engine.classic.core.modules.output.pageable.graphics.debug.PrintPageContents")))
{
ModelPrinter.print(rootBox);
}
revalidateTextEllipseProcessStep = new RevalidateTextEllipseProcessStep(metaData);
collectSelectedNodesStep = new CollectSelectedNodesStep();
this.clipOnWordBoundary = "true".equals
(metaData.getConfiguration().getConfigProperty(
"org.pentaho.reporting.engine.classic.core.LastLineBreaksOnWordBoundary"));
}
protected ResourceManager getResourceManager()
{
return resourceManager;
}
public boolean isClipOnWordBoundary()
{
return clipOnWordBoundary;
}
public boolean isOutlineMode()
{
return outlineMode;
}
public void setOutlineMode(final boolean outlineMode)
{
this.outlineMode = outlineMode;
}
protected StrictBounds getDrawArea()
{
return drawArea;
}
public PageFormat getPageFormat()
{
return (PageFormat) pageFormat.clone();
}
/**
* Returns the preferred size of the drawable. If the drawable is aspect ratio aware, these bounds should be used to
* compute the preferred aspect ratio for this drawable.
*
* @return the preferred size.
*/
public Dimension getPreferredSize()
{
return new Dimension((int) width, (int) height);
}
public double getHeight()
{
return height;
}
public double getWidth()
{
return width;
}
/**
* Returns true, if this drawable will preserve an aspect ratio during the drawing.
*
* @return true, if an aspect ratio is preserved, false otherwise.
*/
public boolean isPreserveAspectRatio()
{
return true;
}
public boolean isDrawPageBackground()
{
return drawPageBackground;
}
public void setDrawPageBackground(final boolean drawPageBackground)
{
this.drawPageBackground = drawPageBackground;
}
/**
* Draws the object.
*
* @param g2 the graphics device.
* @param area the area inside which the object should be drawn.
*/
public void draw(final Graphics2D g2, final Rectangle2D area)
{
if (drawPageBackground)
{
g2.setPaint(Color.white);
g2.fill(area);
}
g2.translate(-area.getX(), -area.getY());
try
{
final StrictBounds pageBounds = StrictGeomUtility.createBounds(area.getX(), area.getY(), area.getWidth(),
area.getHeight());
this.drawArea = pageBounds;
this.graphics = g2;
if (rootBox.isNodeVisible(drawArea) == false)
{
return;
}
if (startBlockBox(rootBox))
{
processRootBand(pageBounds);
}
finishBlockBox(rootBox);
}
finally
{
this.graphics = null;
this.drawArea = null;
}
}
protected void processRootBand(final StrictBounds pageBounds)
{
startProcessing(rootBox.getWatermarkArea());
final BlockRenderBox headerArea = rootBox.getHeaderArea();
final BlockRenderBox footerArea = rootBox.getFooterArea();
final StrictBounds headerBounds = new StrictBounds(headerArea.getX(), headerArea.getY(), headerArea.getWidth(),
headerArea.getHeight());
final StrictBounds footerBounds = new StrictBounds(footerArea.getX(), footerArea.getY(), footerArea.getWidth(),
footerArea.getHeight());
final StrictBounds contentBounds = new StrictBounds
(rootBox.getX(), headerArea.getY() + headerArea.getHeight(),
rootBox.getWidth(), footerArea.getY() - headerArea.getHeight());
this.drawArea = headerBounds;
final Shape clip = this.graphics.getClip();
this.graphics.clip(createClipRect(drawArea));
startProcessing(headerArea);
this.drawArea = contentBounds;
this.graphics.setClip(clip);
this.graphics.clip(createClipRect(drawArea));
processBoxChilds(rootBox);
this.drawArea = footerBounds;
this.graphics.setClip(clip);
this.graphics.clip(createClipRect(drawArea));
startProcessing(footerArea);
this.drawArea = pageBounds;
}
private Rectangle2D createClipRect(final StrictBounds bounds)
{
return StrictGeomUtility.createAWTRectangle(bounds.getX() - 1, bounds.getY() - 1,
bounds.getWidth() + 2, bounds.getHeight() + 2);
}
protected LogicalPageBox getRootBox()
{
return rootBox;
}
protected void setDrawArea(final StrictBounds drawArea)
{
this.drawArea = drawArea;
}
protected boolean drawOutlineBox(final Graphics2D g2, final RenderBox box)
{
if (box.getNodeType() == LayoutNodeTypes.TYPE_BOX_PARAGRAPH)
{
g2.setPaint(Color.magenta);
}
else if (box.getNodeType() == LayoutNodeTypes.TYPE_BOX_LINEBOX)
{
g2.setPaint(Color.orange);
}
else
{
g2.setPaint(Color.lightGray);
}
final double x = StrictGeomUtility.toExternalValue(box.getX());
final double y = StrictGeomUtility.toExternalValue(box.getY());
final double w = StrictGeomUtility.toExternalValue(box.getWidth());
final double h = StrictGeomUtility.toExternalValue(box.getHeight());
boxArea.setFrame(x, y, w, h);
g2.draw(boxArea);
return true;
}
protected void processLinksAndAnchors(final RenderNode box)
{
final StyleSheet styleSheet = box.getStyleSheet();
final String target = (String) styleSheet.getStyleProperty(ElementStyleKeys.HREF_TARGET);
final String title = (String) styleSheet.getStyleProperty(ElementStyleKeys.HREF_TITLE);
if (target != null || title != null)
{
final String window = (String) styleSheet.getStyleProperty(ElementStyleKeys.HREF_WINDOW);
drawHyperlink(box, target, window, title);
}
final String anchor = (String) styleSheet.getStyleProperty(ElementStyleKeys.ANCHOR_NAME);
if (anchor != null)
{
drawAnchor(box);
}
final String bookmark = (String) styleSheet.getStyleProperty(BandStyleKeys.BOOKMARK);
if (bookmark != null)
{
drawBookmark(box, bookmark);
}
}
protected void drawBookmark(final RenderNode box, final String bookmark)
{
}
protected void drawHyperlink(final RenderNode box, final String target, final String window, final String title)
{
}
public boolean startCanvasBox(final CanvasRenderBox box)
{
if (box.getStaticBoxLayoutProperties().isVisible() == false)
{
return false;
}
if (box.isBoxVisible(drawArea) == false)
{
return false;
}
renderBoxBorderAndBackground(box);
processLinksAndAnchors(box);
return true;
}
protected boolean startBlockBox(final BlockRenderBox box)
{
if (box.getStaticBoxLayoutProperties().isVisible() == false)
{
return false;
}
if (box.isBoxVisible(drawArea) == false)
{
return false;
}
renderBoxBorderAndBackground(box);
processLinksAndAnchors(box);
return true;
}
protected boolean startRowBox(final RenderBox box)
{
if (box.getStaticBoxLayoutProperties().isVisible() == false)
{
return false;
}
if (box.isBoxVisible(drawArea) == false)
{
return false;
}
renderBoxBorderAndBackground(box);
processLinksAndAnchors(box);
return true;
}
protected boolean startInlineBox(final InlineRenderBox box)
{
if (box.getStaticBoxLayoutProperties().isVisible() == false)
{
return false;
}
if (box.isBoxVisible(drawArea) == false)
{
return false;
}
renderBoxBorderAndBackground(box);
if (textSpec != null)
{
textSpec.close();
textSpec = null;
}
final FontDecorationSpec newUnderlineSpec = computeUnderline(box, underline);
if (underline != null && newUnderlineSpec == null)
{
drawTextDecoration(underline);
underline = null;
}
else
{
underline = newUnderlineSpec;
}
final FontDecorationSpec newStrikeThroughSpec = computeStrikeThrough(box, strikeThrough);
if (strikeThrough != null && newStrikeThroughSpec == null)
{
drawTextDecoration(strikeThrough);
strikeThrough = null;
}
else
{
strikeThrough = newStrikeThroughSpec;
}
processLinksAndAnchors(box);
return true;
}
protected void renderBoxBorderAndBackground(final RenderBox box)
{
final Graphics2D g2 = getGraphics();
if (outlineMode)
{
if (drawOutlineBox(g2, box))
{
return;
}
}
if (box.getBoxDefinition().getBorder().isEmpty() == false)
{
borderRenderer.paintBackgroundAndBorder(box, g2);
}
else
{
final Color backgroundColor = (Color) box.getStyleSheet().getStyleProperty(ElementStyleKeys.BACKGROUND_COLOR);
if (backgroundColor != null)
{
final double x = StrictGeomUtility.toExternalValue(box.getX());
final double y = StrictGeomUtility.toExternalValue(box.getY());
final double w = StrictGeomUtility.toExternalValue(box.getWidth());
final double h = StrictGeomUtility.toExternalValue(box.getHeight());
boxArea.setFrame(x, y, w, h);
g2.setColor(backgroundColor);
g2.fill(boxArea);
}
}
}
protected Rectangle2D.Double getBoxArea()
{
return boxArea;
}
protected TextSpec getTextSpec()
{
return textSpec;
}
protected void setTextSpec(final TextSpec textSpec)
{
this.textSpec = textSpec;
}
private FontDecorationSpec computeUnderline(final RenderBox box, FontDecorationSpec oldSpec)
{
final StyleSheet styleSheet = box.getStyleSheet();
if (styleSheet.getBooleanStyleProperty(TextStyleKeys.UNDERLINED) == false)
{
return null;
}
if (oldSpec == null)
{
oldSpec = new FontDecorationSpec();
}
final double size = box.getStyleSheet().getDoubleStyleProperty(TextStyleKeys.FONTSIZE, 0);
final double lineWidth = Math.max(1, size / 20.0);
oldSpec.updateLineWidth(lineWidth);
oldSpec.setTextColor((Color) box.getStyleSheet().getStyleProperty(ElementStyleKeys.PAINT));
return oldSpec;
}
private FontDecorationSpec computeStrikeThrough(final RenderBox box, FontDecorationSpec oldSpec)
{
final StyleSheet styleSheet = box.getStyleSheet();
if (styleSheet.getBooleanStyleProperty(TextStyleKeys.STRIKETHROUGH) == false)
{
return null;
}
if (oldSpec == null)
{
oldSpec = new FontDecorationSpec();
}
final double size = box.getStyleSheet().getDoubleStyleProperty(TextStyleKeys.FONTSIZE, 0);
final double lineWidth = Math.max(1, size / 20.0);
oldSpec.updateLineWidth(lineWidth);
oldSpec.setTextColor((Color) box.getStyleSheet().getStyleProperty(ElementStyleKeys.PAINT));
return oldSpec;
}
private boolean isStyleActive(final StyleKey key, final RenderBox parent)
{
if ((parent.getNodeType() & LayoutNodeTypes.MASK_BOX_INLINE) != LayoutNodeTypes.MASK_BOX_INLINE)
{
return false;
}
return parent.getStyleSheet().getBooleanStyleProperty(key);
}
protected void finishInlineBox(final InlineRenderBox box)
{
final RenderBox parent = box.getParent();
if (underline != null)
{
if (isStyleActive(TextStyleKeys.UNDERLINED, parent) == false)
{
// The parent has no underline style, but this box has. So finish up the underline.
drawTextDecoration(underline);
underline = null;
}
}
else
{
// maybe this inlinebox has no underline, but the parent has ...
underline = computeUnderline(box, null);
}
if (strikeThrough != null)
{
if (isStyleActive(TextStyleKeys.STRIKETHROUGH, parent) == false)
{
// The parent has no underline style, but this box has. So finish up the underline.
drawTextDecoration(strikeThrough);
strikeThrough = null;
}
}
else
{
underline = computeUnderline(box, null);
}
if (textSpec != null)
{
textSpec.close();
textSpec = null;
}
}
private void drawTextDecoration(final FontDecorationSpec decorationSpec)
{
final Graphics2D graphics = (Graphics2D) this.graphics.create();
graphics.setColor(decorationSpec.getTextColor());
graphics.setStroke(new BasicStroke((float) decorationSpec.getLineWidth()));
graphics.draw(new Line2D.Double(decorationSpec.getStart(), decorationSpec.getVerticalPosition(),
decorationSpec.getEnd(), decorationSpec.getVerticalPosition()));
graphics.dispose();
}
protected void processParagraphChilds(final ParagraphRenderBox box)
{
this.contentAreaX1 = box.getContentAreaX1();
this.contentAreaX2 = box.getContentAreaX2();
RenderBox lineBox = (RenderBox) box.getFirstChild();
while (lineBox != null)
{
processTextLine(lineBox, contentAreaX1, contentAreaX2);
lineBox = (RenderBox) lineBox.getNext();
}
}
protected RevalidateTextEllipseProcessStep getRevalidateTextEllipseProcessStep()
{
return revalidateTextEllipseProcessStep;
}
protected void processTextLine(final RenderBox lineBox,
final long contentAreaX1,
final long contentAreaX2)
{
if (lineBox.isNodeVisible(drawArea) == false)
{
return;
}
final boolean overflowProperty = lineBox.getParent().getStaticBoxLayoutProperties().isOverflowX();
this.textLineOverflow =
((lineBox.getX() + lineBox.getWidth()) > contentAreaX2) && overflowProperty == false;
this.ellipseDrawn = false;
if (textLineOverflow)
{
revalidateTextEllipseProcessStep.compute(lineBox, contentAreaX1, contentAreaX2);
}
underline = null;
strikeThrough = null;
startProcessing(lineBox);
}
public long getContentAreaX2()
{
return contentAreaX2;
}
public void setContentAreaX2(final long contentAreaX2)
{
this.contentAreaX2 = contentAreaX2;
}
public long getContentAreaX1()
{
return contentAreaX1;
}
public void setContentAreaX1(final long contentAreaX1)
{
this.contentAreaX1 = contentAreaX1;
}
public boolean isTextLineOverflow()
{
return textLineOverflow;
}
public void setTextLineOverflow(final boolean textLineOverflow)
{
this.textLineOverflow = textLineOverflow;
}
protected void processOtherNode(final RenderNode node)
{
final int type = node.getNodeType();
if (isTextLineOverflow())
{
if (node.isNodeVisible(drawArea) == false)
{
return;
}
if (node.isVirtualNode())
{
if (ellipseDrawn == false)
{
if (clipOnWordBoundary == false && type == LayoutNodeTypes.TYPE_NODE_TEXT)
{
final RenderableText text = (RenderableText) node;
final long ellipseSize = extractEllipseSize(node);
final long x1 = text.getX();
final long effectiveAreaX2 = (contentAreaX2 - ellipseSize);
if (x1 >= contentAreaX2)
{
// Skip, the node will not be visible.
}
else
{
// The text node that is printed will overlap with the ellipse we need to print.
drawText(text, effectiveAreaX2);
}
}
ellipseDrawn = true;
final RenderBox parent = node.getParent();
if (parent != null)
{
final RenderBox textEllipseBox = parent.getTextEllipseBox();
if (textEllipseBox != null)
{
processBoxChilds(textEllipseBox);
}
}
return;
}
}
}
if (type == LayoutNodeTypes.TYPE_NODE_TEXT)
{
final RenderableText text = (RenderableText) node;
if (underline != null)
{
final ExtendedBaselineInfo baselineInfo = text.getBaselineInfo();
final long underlinePos = text.getY() + baselineInfo.getUnderlinePosition();
underline.updateVerticalPosition(StrictGeomUtility.toExternalValue(underlinePos));
underline.updateStart(StrictGeomUtility.toExternalValue(text.getX()));
underline.updateEnd(StrictGeomUtility.toExternalValue(text.getX() + text.getWidth()));
}
if (strikeThrough != null)
{
final ExtendedBaselineInfo baselineInfo = text.getBaselineInfo();
final long strikethroughPos = text.getY() + baselineInfo.getStrikethroughPosition();
strikeThrough.updateVerticalPosition(StrictGeomUtility.toExternalValue(strikethroughPos));
strikeThrough.updateStart(StrictGeomUtility.toExternalValue(text.getX()));
strikeThrough.updateEnd(StrictGeomUtility.toExternalValue(text.getX() + text.getWidth()));
}
if (isTextLineOverflow())
{
if (node.isNodeVisible(drawArea) == false)
{
return;
}
final long ellipseSize = extractEllipseSize(node);
final long x1 = text.getX();
final long x2 = x1 + text.getWidth();
final long effectiveAreaX2 = (contentAreaX2 - ellipseSize);
if (x2 <= effectiveAreaX2)
{
// the text will be fully visible.
drawText(text);
}
else
{
if (x1 >= contentAreaX2)
{
// Skip, the node will not be visible.
}
else
{
// The text node that is printed will overlap with the ellipse we need to print.
drawText(text, effectiveAreaX2);
}
final RenderBox parent = node.getParent();
if (parent != null)
{
final RenderBox textEllipseBox = parent.getTextEllipseBox();
if (textEllipseBox != null)
{
processBoxChilds(textEllipseBox);
}
}
ellipseDrawn = true;
}
}
else
{
drawText(text);
}
}
}
protected void processRenderableContent(final RenderableReplacedContentBox box)
{
if (box.getStaticBoxLayoutProperties().isVisible() == false)
{
return;
}
if (box.isBoxVisible(drawArea) == false)
{
return;
}
renderBoxBorderAndBackground(box);
processLinksAndAnchors(box);
drawReplacedContent(box);
}
private long extractEllipseSize(final RenderNode node)
{
if (node == null)
{
return 0;
}
final RenderBox parent = node.getParent();
if (parent == null)
{
return 0;
}
final RenderBox textEllipseBox = parent.getTextEllipseBox();
if (textEllipseBox == null)
{
return 0;
}
return textEllipseBox.getWidth();
}
protected void drawReplacedContent(final RenderableReplacedContentBox content)
{
final Graphics2D g2 = getGraphics();
final Object o = content.getContent().getRawObject();
if (o instanceof Image)
{
drawImage(content, (Image) o);
}
else if (o instanceof DrawableWrapper)
{
final DrawableWrapper d = (DrawableWrapper) o;
drawDrawable(content, g2, d);
}
else if (o instanceof LocalImageContainer)
{
final LocalImageContainer imageContainer = (LocalImageContainer) o;
final Image image = imageContainer.getImage();
drawImage(content, image);
}
else if (o instanceof URLImageContainer)
{
final URLImageContainer imageContainer = (URLImageContainer) o;
if (imageContainer.isLoadable() == false)
{
LogicalPageDrawable.logger.info("URL-image cannot be rendered, as it was declared to be not loadable.");
return;
}
final ResourceKey sourceURL = imageContainer.getResourceKey();
if (sourceURL == null)
{
LogicalPageDrawable.logger.info("URL-image cannot be rendered, as it did not return a valid URL.");
}
try
{
final Resource resource = resourceManager.create(sourceURL, null, Image.class);
final Image image = (Image) resource.getResource();
drawImage(content, image);
}
catch (ResourceException e)
{
LogicalPageDrawable.logger.info("URL-image cannot be rendered, as the image was not loadable.", e);
}
}
else
{
LogicalPageDrawable.logger.debug("Unable to handle " + o);
}
}
/**
* To be overriden in the PDF drawable.
*
* @param content the render-node that defines the anchor.
*/
protected void drawAnchor(final RenderNode content)
{
}
/**
* @param content
* @param image
*/
protected boolean drawImage(final RenderableReplacedContentBox content, Image image)
{
final StyleSheet layoutContext = content.getStyleSheet();
final boolean shouldScale = layoutContext.getBooleanStyleProperty(ElementStyleKeys.SCALE);
final int x = (int) StrictGeomUtility.toExternalValue(content.getX());
final int y = (int) StrictGeomUtility.toExternalValue(content.getY());
final int width = (int) StrictGeomUtility.toExternalValue(content.getWidth());
final int height = (int) StrictGeomUtility.toExternalValue(content.getHeight());
if (width == 0 || height == 0)
{
LogicalPageDrawable.logger.debug("Error: Image area is empty: " + content);
return false;
}
WaitingImageObserver obs = new WaitingImageObserver(image);
obs.waitImageLoaded();
final int imageWidth = image.getWidth(obs);
final int imageHeight = image.getHeight(obs);
if (imageWidth < 1 || imageHeight < 1)
{
return false;
}
final Rectangle2D.Double drawAreaBounds = new Rectangle2D.Double(x, y, width, height);
final AffineTransform scaleTransform;
final Graphics2D g2;
if (shouldScale == false)
{
double deviceScaleFactor = 1;
final double devResolution = metaData.getNumericFeatureValue(OutputProcessorFeature.DEVICE_RESOLUTION);
if (metaData.isFeatureSupported(OutputProcessorFeature.IMAGE_RESOLUTION_MAPPING))
{
if (devResolution != 72.0 && devResolution > 0)
{
// Need to scale the device to its native resolution before attempting to draw the image..
deviceScaleFactor = (72.0 / devResolution);
}
}
final int clipWidth = Math.min(width, (int) Math.ceil(deviceScaleFactor * imageWidth));
final int clipHeight = Math.min(height, (int) Math.ceil(deviceScaleFactor * imageHeight));
final ElementAlignment horizontalAlignment =
(ElementAlignment) layoutContext.getStyleProperty(ElementStyleKeys.ALIGNMENT);
final ElementAlignment verticalAlignment =
(ElementAlignment) layoutContext.getStyleProperty(ElementStyleKeys.VALIGNMENT);
final int alignmentX = (int) RenderUtility.computeHorizontalAlignment(horizontalAlignment, width, clipWidth);
final int alignmentY = (int) RenderUtility.computeVerticalAlignment(verticalAlignment, height, clipHeight);
g2 = (Graphics2D) getGraphics().create();
g2.clip(drawAreaBounds);
g2.translate(x, y);
g2.translate(alignmentX, alignmentY);
g2.clip(new Rectangle2D.Float(0, 0, clipWidth, clipHeight));
g2.scale(deviceScaleFactor, deviceScaleFactor);
scaleTransform = null;
}
else
{
g2 = (Graphics2D) getGraphics().create();
g2.clip(drawAreaBounds);
g2.translate(x, y);
g2.clip(new Rectangle2D.Float(0, 0, width, height));
final double scaleX;
final double scaleY;
final boolean keepAspectRatio = layoutContext.getBooleanStyleProperty(ElementStyleKeys.KEEP_ASPECT_RATIO);
if (keepAspectRatio)
{
final double scaleFactor = Math.min(width / (double) imageWidth, height / (double) imageHeight);
scaleX = scaleFactor;
scaleY = scaleFactor;
}
else
{
scaleX = width / (double) imageWidth;
scaleY = height / (double) imageHeight;
}
final int clipWidth = (int) (scaleX * imageWidth);
final int clipHeight = (int) (scaleY * imageHeight);
final ElementAlignment horizontalAlignment =
(ElementAlignment) layoutContext.getStyleProperty(ElementStyleKeys.ALIGNMENT);
final ElementAlignment verticalAlignment =
(ElementAlignment) layoutContext.getStyleProperty(ElementStyleKeys.VALIGNMENT);
final int alignmentX = (int) RenderUtility.computeHorizontalAlignment(horizontalAlignment, width, clipWidth);
final int alignmentY = (int) RenderUtility.computeVerticalAlignment(verticalAlignment, height, clipHeight);
g2.translate(alignmentX, alignmentY);
final Object contentCached = content.getContent().getContentCached();
if (contentCached instanceof Image)
{
image = (Image) contentCached;
scaleTransform = null;
}
else if (metaData.isFeatureSupported(OutputProcessorFeature.PREFER_NATIVE_SCALING) == false)
{
image = RenderUtility.scaleImage(image, clipWidth, clipHeight, RenderingHints.VALUE_INTERPOLATION_BICUBIC,
true);
content.getContent().setContentCached(image);
obs = new WaitingImageObserver(image);
obs.waitImageLoaded();
scaleTransform = null;
}
else
{
scaleTransform = AffineTransform.getScaleInstance(scaleX, scaleY);
}
}
while (g2.drawImage(image, scaleTransform, obs) == false)
{
obs.waitImageLoaded();
if (obs.isError())
{
LogicalPageDrawable.logger.warn("Error while loading the image during the rendering.");
break;
}
}
g2.dispose();
return true;
}
protected boolean drawDrawable(final RenderableReplacedContentBox content, final Graphics2D g2, final DrawableWrapper d)
{
final double x = StrictGeomUtility.toExternalValue(content.getX());
final double y = StrictGeomUtility.toExternalValue(content.getY());
final double width = StrictGeomUtility.toExternalValue(content.getWidth());
final double height = StrictGeomUtility.toExternalValue(content.getHeight());
if ((width < 0 || height < 0) || (width == 0 && height == 0))
{
return false;
}
final Graphics2D clone = (Graphics2D) g2.create();
final StyleSheet styleSheet = content.getStyleSheet();
final Object attribute = styleSheet.getStyleProperty(ElementStyleKeys.ANTI_ALIASING);
if (attribute != null)
{
if (Boolean.TRUE.equals(attribute))
{
clone.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
}
else if (Boolean.FALSE.equals(attribute))
{
clone.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_OFF);
}
}
if (RenderUtility.isFontSmooth(styleSheet, metaData))
{
clone.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
}
else
{
clone.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_OFF);
}
if (strictClipping == false)
{
final double extraPadding;
final Object o = styleSheet.getStyleProperty(ElementStyleKeys.STROKE);
if (o instanceof BasicStroke)
{
final BasicStroke stroke = (BasicStroke) o;
extraPadding = stroke.getLineWidth() / 2.0;
}
else
{
extraPadding = 0.5;
}
final Rectangle2D.Double clipBounds = new Rectangle2D.Double
(x - extraPadding, y - extraPadding, width + 2 * extraPadding, height + 2 * extraPadding);
clone.clip(clipBounds);
clone.translate(x, y);
}
else
{
final Rectangle2D.Double clipBounds = new Rectangle2D.Double(x, y, width + 1, height + 1);
clone.clip(clipBounds);
clone.translate(x, y);
}
configureGraphics(styleSheet, clone);
configureStroke(styleSheet, clone);
final Rectangle2D.Double bounds = new Rectangle2D.Double(0, 0, width, height);
d.draw(clone, bounds);
clone.dispose();
return true;
}
protected void drawText(final RenderableText renderableText)
{
drawText(renderableText, renderableText.getX() + renderableText.getWidth());
}
/**
* Renders the glyphs stored in the text node.
*
* @param renderableText the text node that should be rendered.
* @param contentX2
*/
protected void drawText(final RenderableText renderableText, final long contentX2)
{
if (renderableText.getLength() == 0)
{
// This text is empty.
return;
}
final long posX = renderableText.getX();
final long posY = renderableText.getY();
final Graphics2D g2;
if (textSpec == null)
{
g2 = (Graphics2D) getGraphics().create();
final StyleSheet layoutContext = renderableText.getStyleSheet();
configureGraphics(layoutContext, g2);
g2.setStroke(LogicalPageDrawable.DEFAULT_STROKE);
if (RenderUtility.isFontSmooth(layoutContext, metaData))
{
g2.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
}
else
{
g2.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_OFF);
}
}
else
{
g2 = textSpec.getGraphics();
}
// This shifting is necessary to make sure that all text is rendered like in the previous versions.
// In the earlier versions, we did not really obey to the baselines of the text, we just hoped and prayed.
// Therefore, all text was printed at the bottom of the text elements. With the introduction of the full
// font metrics setting, this situation got a little bit better, for the price that text-elements became
// nearly unpredictable ..
//
// The code below may be weird, but at least it is predictable weird.
final ExtendedBaselineInfo baselineInfo = renderableText.getBaselineInfo();
final long baseline = baselineInfo.getBaseline(baselineInfo.getDominantBaseline());
final FontMetrics fm = g2.getFontMetrics();
final Rectangle2D rect = fm.getMaxCharBounds(g2);
final long awtBaseLine = StrictGeomUtility.toInternalValue(-rect.getY());
final GlyphList gs = renderableText.getGlyphs();
if (metaData.isFeatureSupported(OutputProcessorFeature.FAST_FONTRENDERING) &&
isNormalTextSpacing(renderableText))
{
final int maxLength = computeMaximumTextSize(renderableText, contentX2);
final String text = gs.getText(renderableText.getOffset(), maxLength, codePointBuffer);
final float y = (float) StrictGeomUtility.toExternalValue(posY + awtBaseLine);
g2.drawString(text, (float) StrictGeomUtility.toExternalValue(posX), y);
}
else
{
final int maxPos = renderableText.getOffset() + computeMaximumTextSize(renderableText, contentX2);
long runningPos = posX;
final long baselineDelta = awtBaseLine - baseline;
for (int i = renderableText.getOffset(); i < maxPos; i++)
{
final Glyph g = gs.getGlyph(i);
final float y = (float) StrictGeomUtility.toExternalValue(posY + g.getBaseLine() + baselineDelta);
g2.drawString(gs.getGlyphAsString(i, codePointBuffer), (float) StrictGeomUtility.toExternalValue(runningPos), y);
runningPos += g.getWidth() + g.getSpacing().getMinimum();
}
}
g2.dispose();
}
protected final CodePointBuffer getCodePointBuffer()
{
return codePointBuffer;
}
protected int computeMaximumTextSize(final RenderableText node, final long contentX2)
{
final int length = node.getLength();
final long x = node.getX();
if (contentX2 >= (x + node.getWidth()))
{
return length;
}
final GlyphList gs = node.getGlyphs();
long runningPos = x;
final int offset = node.getOffset();
final int maxPos = offset + length;
for (int i = offset; i < maxPos; i++)
{
final Glyph g = gs.getGlyph(i);
runningPos += g.getWidth();
if (runningPos > contentX2)
{
return Math.max(0, i - offset);
}
}
return length;
}
protected boolean isNormalTextSpacing(final RenderableText text)
{
return text.isNormalTextSpacing();
// final Glyph[] glyphs = text.getGlyphs();
// for (int i = 0; i < glyphs.length; i++)
// {
// final Glyph glyph = glyphs[i];
// if (Spacing.EMPTY_SPACING.equals(glyph.getSpacing()) == false)
// {
// return false;
// }
// }
// return true;
}
protected void configureStroke(final StyleSheet layoutContext, final Graphics2D g2)
{
final Stroke styleProperty = (Stroke) layoutContext.getStyleProperty(ElementStyleKeys.STROKE);
if (styleProperty != null)
{
g2.setStroke(styleProperty);
}
else
{
// Apply a default one ..
g2.setStroke(LogicalPageDrawable.DEFAULT_STROKE);
}
}
protected void configureGraphics(final StyleSheet layoutContext, final Graphics2D g2)
{
final boolean bold = layoutContext.getBooleanStyleProperty(TextStyleKeys.BOLD);
final boolean italics = layoutContext.getBooleanStyleProperty(TextStyleKeys.ITALIC);
int style = Font.PLAIN;
if (bold)
{
style |= Font.BOLD;
}
if (italics)
{
style |= Font.ITALIC;
}
final Color cssColor = (Color) layoutContext.getStyleProperty(ElementStyleKeys.PAINT);
g2.setColor(cssColor);
final int fontSize = layoutContext.getIntStyleProperty(TextStyleKeys.FONTSIZE,
(int) metaData.getNumericFeatureValue(OutputProcessorFeature.DEFAULT_FONT_SIZE));
final String fontName = metaData.getNormalizedFontFamilyName
((String) layoutContext.getStyleProperty(TextStyleKeys.FONT));
g2.setFont(new Font(fontName, style, fontSize));
}
public OutputProcessorMetaData getMetaData()
{
return metaData;
}
public Graphics2D getGraphics()
{
return graphics;
}
/**
* Retries the nodes under the given coordinate which have a given attribute set. If name and namespace are null, all
* nodes are returned. The nodes returned are listed in their respective hierarchical order.
*
* @param x the x coordinate
* @param y the y coordinate
* @param namespace the namespace on which to filter on
* @param name the name on which to filter on
* @return the ordered list of nodes.
*/
public RenderNode[] getNodesAt(final double x, final double y, final String namespace, final String name)
{
return collectSelectedNodesStep.getNodesAt
(this.rootBox, StrictGeomUtility.createBounds(x, y, 1, 1), namespace, name);
}
public RenderNode[] getNodesAt(final double x, final double y,
final double width, final double height,
final String namespace, final String name)
{
return collectSelectedNodesStep.getNodesAt
(this.rootBox, StrictGeomUtility.createBounds(x, y, width, height), namespace, name);
}
}