Java tutorial
/* * 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 - 2017 Object Refinery Ltd, Hitachi Vantara 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.Shape; import java.awt.Stroke; 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.RenderableComplexText; 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.model.table.TableCellRenderBox; import org.pentaho.reporting.engine.classic.core.layout.model.table.TableColumnGroupNode; import org.pentaho.reporting.engine.classic.core.layout.model.table.TableRenderBox; import org.pentaho.reporting.engine.classic.core.layout.model.table.TableRowRenderBox; import org.pentaho.reporting.engine.classic.core.layout.model.table.TableSectionRenderBox; 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.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.FastStack; 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 */ @SuppressWarnings("HardCodedStringLiteral") public class LogicalPageDrawable extends IterateStructuralProcessStep implements PageDrawable { protected static class TextSpec { private boolean bold; private boolean italics; private String fontName; private float fontSize; 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; 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 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; } } private static class TableContext { private TableContext parent; private StrictBounds bounds; private StrictBounds drawArea; private TableContext(final TableContext parent) { this.parent = parent; this.bounds = new StrictBounds(); this.drawArea = new StrictBounds(); } public StrictBounds getDrawArea() { return drawArea; } public StrictBounds getBounds() { return bounds; } public TableContext pop() { return parent; } } 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; private boolean unalignedPageBands; private TableContext tableContext; private FastStack<Graphics2D> graphicsContexts; private StrictBounds pageArea; public LogicalPageDrawable() { this.graphicsContexts = new FastStack<Graphics2D>(); this.borderRenderer = new BorderRenderer(); this.codePointBuffer = new CodePointBuffer(400); this.boxArea = new Rectangle2D.Double(); this.drawPageBackground = true; } @Deprecated public LogicalPageDrawable(final LogicalPageBox rootBox, final OutputProcessorMetaData metaData, final ResourceManager resourceManager) { this(); init(rootBox, metaData, resourceManager); } public void init(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.resourceManager = resourceManager; this.metaData = metaData; this.rootBox = rootBox; this.width = StrictGeomUtility.toExternalValue(rootBox.getPageWidth()); this.height = StrictGeomUtility.toExternalValue(rootBox.getPageHeight()); 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.INSTANCE.print(rootBox); } this.unalignedPageBands = metaData.isFeatureSupported(OutputProcessorFeature.UNALIGNED_PAGEBANDS); revalidateTextEllipseProcessStep = new RevalidateTextEllipseProcessStep(metaData); collectSelectedNodesStep = new CollectSelectedNodesStep(); this.clipOnWordBoundary = "true".equals(metaData.getConfiguration() .getConfigProperty("org.pentaho.reporting.engine.classic.core.LastLineBreaksOnWordBoundary")); } public LogicalPageBox getLogicalPageBox() { return rootBox; } 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. */ @SuppressWarnings("UnusedDeclaration") public boolean isPreserveAspectRatio() { return true; } public boolean isDrawPageBackground() { return drawPageBackground; } public void setDrawPageBackground(final boolean drawPageBackground) { this.drawPageBackground = drawPageBackground; } /** * Draws the object. * * @param graphics * the graphics device. * @param area * the area inside which the object should be drawn. */ public void draw(final Graphics2D graphics, final Rectangle2D area) { final Graphics2D g2 = (Graphics2D) graphics.create(); if (isDrawPageBackground()) { 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.pageArea = pageBounds; this.drawArea = pageBounds; this.graphics = g2; if (startBlockBox(rootBox)) { processRootBand(pageBounds); } finishBlockBox(rootBox); } finally { this.graphics = null; this.drawArea = null; g2.dispose(); } } protected void processRootBand(final StrictBounds pageBounds) { final Shape clip = this.graphics.getClip(); boolean watermarkOnTop = getMetaData().isFeatureSupported(OutputProcessorFeature.WATERMARK_PRINTED_ON_TOP); if (!watermarkOnTop) { startProcessing(rootBox.getWatermarkArea()); } final BlockRenderBox headerArea = rootBox.getHeaderArea(); final BlockRenderBox footerArea = rootBox.getFooterArea(); final BlockRenderBox repeatFooterArea = rootBox.getRepeatFooterArea(); 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 repeatFooterBounds = new StrictBounds(repeatFooterArea.getX(), repeatFooterArea.getY(), repeatFooterArea.getWidth(), repeatFooterArea.getHeight()); final StrictBounds contentBounds = new StrictBounds(rootBox.getX(), headerArea.getY() + headerArea.getHeight(), rootBox.getWidth(), repeatFooterArea.getY() - headerArea.getHeight()); final double headerHeight = StrictGeomUtility.toExternalValue(drawArea.getHeight()); setDrawArea(headerBounds); this.graphics.clip(createClipRect(drawArea)); startProcessing(headerArea); if (unalignedPageBands) { this.graphics.translate(0, headerHeight); } setDrawArea(contentBounds); this.graphics.setClip(clip); this.graphics.clip(createClipRect(drawArea)); processBoxChilds(rootBox); if (unalignedPageBands) { this.graphics.translate(0, -headerHeight); this.graphics.translate(0, height - StrictGeomUtility.toExternalValue(footerBounds.getHeight() + repeatFooterBounds.getHeight())); } setDrawArea(repeatFooterBounds); this.graphics.setClip(clip); this.graphics.clip(createClipRect(drawArea)); startProcessing(repeatFooterArea); if (unalignedPageBands) { this.graphics.translate(0, StrictGeomUtility.toExternalValue(repeatFooterBounds.getHeight())); } setDrawArea(footerBounds); this.graphics.setClip(clip); this.graphics.clip(createClipRect(drawArea)); startProcessing(footerArea); setDrawArea(pageBounds); this.graphics.setClip(clip); if (watermarkOnTop) { startProcessing(rootBox.getWatermarkArea()); this.graphics.setClip(clip); } } protected 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 = pageArea.createIntersection(drawArea); } protected void drawOutlineBox(final Graphics2D g2, final RenderBox box) { final int nodeType = box.getNodeType(); if (nodeType == LayoutNodeTypes.TYPE_BOX_PARAGRAPH) { g2.setPaint(Color.magenta); } else if (nodeType == LayoutNodeTypes.TYPE_BOX_LINEBOX) { g2.setPaint(Color.orange); } else if ((nodeType & LayoutNodeTypes.MASK_BOX_TABLE) == LayoutNodeTypes.MASK_BOX_TABLE) { g2.setPaint(Color.cyan); } 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); } 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) { return startBox(box); } protected boolean startBlockBox(final BlockRenderBox box) { return startBox(box); } protected boolean startRowBox(final RenderBox box) { return startBox(box); } protected boolean startTableCellBox(final TableCellRenderBox box) { return startBox(box); } protected boolean startBox(final RenderBox box) { if (box.getStaticBoxLayoutProperties().isVisible() == false) { return false; } if (box instanceof LogicalPageBox == false) { if (box.isBoxVisible(drawArea) == false) { box.isBoxVisible(drawArea); return false; } } renderBoxBorderAndBackground(box); processLinksAndAnchors(box); return true; } protected boolean startTableRowBox(final TableRowRenderBox box) { return startBox(box); } protected boolean startTableSectionBox(final TableSectionRenderBox box) { if (box.getDisplayRole() != TableSectionRenderBox.Role.HEADER) { final StrictBounds bounds = tableContext.getBounds(); if (bounds.getHeight() != 0) { // clip the printable area to an infinite large area below the header. // Pdf output has a limit of 32768 for its floating point numbers (16-bit), // any larger value yields an invalid clipping area. final StrictBounds clipBounds = new StrictBounds(bounds.getX(), bounds.getY() + bounds.getHeight(), StrictGeomUtility.toInternalValue(Short.MAX_VALUE), StrictGeomUtility.toInternalValue(Short.MAX_VALUE)); clip(clipBounds); tableContext.getDrawArea().setRect(drawArea); drawArea.setRect(drawArea.createIntersection(clipBounds)); } } return startBox(box); } protected void finishTableSectionBox(final TableSectionRenderBox box) { if (box.getDisplayRole() == TableSectionRenderBox.Role.HEADER) { tableContext.getBounds().setRect(box.getX(), box.getY(), box.getWidth(), box.getHeight()); } else if (tableContext.getBounds().getHeight() != 0) { drawArea.setRect(tableContext.getDrawArea()); clearClipping(); } } protected boolean startTableBox(final TableRenderBox box) { tableContext = new TableContext(tableContext); return startBox(box); } protected void finishTableBox(final TableRenderBox box) { tableContext = tableContext.pop(); } protected boolean startTableColumnGroupBox(final TableColumnGroupNode box) { return false; } protected boolean startAutoBox(final RenderBox box) { return startBox(box); } protected boolean startInlineBox(final InlineRenderBox box) { if (box.getStaticBoxLayoutProperties().isVisible() == false) { return false; } if (box.isBoxVisible(drawArea) == false) { return false; } renderBoxBorderAndBackground(box); TextSpec textSpec = getTextSpec(); if (textSpec != null) { textSpec.close(); setTextSpec(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 boolean isIgnoreBorderWhenDrawingOutline() { return false; } protected void renderBoxBorderAndBackground(final RenderBox box) { final Graphics2D g2 = getGraphics(); if (isOutlineMode()) { drawOutlineBox(g2, box); if (isIgnoreBorderWhenDrawingOutline()) { 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.getLayoutNodeType() & LayoutNodeTypes.MASK_BOX_INLINE) != LayoutNodeTypes.MASK_BOX_INLINE) { return false; } return parent.getStyleSheet().getBooleanStyleProperty(key); } protected void finishInlineBox(final InlineRenderBox box) { TextSpec textSpec = getTextSpec(); if (textSpec != null) { textSpec.close(); setTextSpec(null); } 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); } } private void drawTextDecoration(final FontDecorationSpec decorationSpec) { final Graphics2D graphics = (Graphics2D) getGraphics().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(); this.textSpec = null; RenderBox lineBox = (RenderBox) box.getFirstChild(); if (lineBox != null) { final boolean needClipping = lineBox.getHeight() > box.getHeight() && !box.isBoxOverflowX(); if (needClipping) { // clip StrictBounds safeBounds = new StrictBounds(lineBox.getX(), lineBox.getY(), lineBox.getWidth() * 3 / 2, lineBox.getHeight()); clip(safeBounds); } while (lineBox != null) { processTextLine(lineBox, contentAreaX1, contentAreaX2); lineBox = (RenderBox) lineBox.getNext(); } if (needClipping) { clearClipping(); } } if (textSpec != null) { throw new IllegalStateException(); } } 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) { if (node.isNodeVisible(drawArea) == false) { return; } final int type = node.getNodeType(); if (isTextLineOverflow()) { if (node.isVirtualNode()) { if (ellipseDrawn == false) { if (isClipOnWordBoundary() == 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) { // The text node that is printed will overlap with the ellipse we need to print. drawText(text, effectiveAreaX2); } } else if (isClipOnWordBoundary() == false && type == LayoutNodeTypes.TYPE_NODE_COMPLEX_TEXT) { final RenderableComplexText text = (RenderableComplexText) node; // final long ellipseSize = extractEllipseSize(node); final long x1 = text.getX(); // final long effectiveAreaX2 = (contentAreaX2 - ellipseSize); if (x1 < contentAreaX2) { // The text node that is printed will overlap with the ellipse we need to print. final Graphics2D g2; if (getTextSpec() == null) { g2 = (Graphics2D) getGraphics().create(); final StyleSheet layoutContext = text.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 = getTextSpec().getGraphics(); } drawComplexText(text, g2); } } 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()) { 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) { // 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); } } else if (type == LayoutNodeTypes.TYPE_NODE_COMPLEX_TEXT) { final RenderableComplexText text = (RenderableComplexText) node; final long x1 = text.getX(); if (x1 < contentAreaX2) { // The text node that is printed will overlap with the ellipse we need to print. final Graphics2D g2; if (getTextSpec() == null) { g2 = (Graphics2D) getGraphics().create(); final StyleSheet layoutContext = text.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 = getTextSpec().getGraphics(); } drawComplexText(text, g2); } } } 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 (getTextSpec() == 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 = getTextSpec().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 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 = renderableText.computeMaximumTextSize(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 ExtendedBaselineInfo baselineInfo = renderableText.getBaselineInfo(); final int maxPos = renderableText.getOffset() + renderableText.computeMaximumTextSize(contentX2); long runningPos = posX; final long baseline = baselineInfo.getBaseline(baselineInfo.getDominantBaseline()); final long baselineDelta = awtBaseLine - baseline; final float y = (float) (StrictGeomUtility.toExternalValue(posY + awtBaseLine + baselineDelta)); for (int i = renderableText.getOffset(); i < maxPos; i++) { final Glyph g = gs.getGlyph(i); g2.drawString(gs.getGlyphAsString(i, codePointBuffer), (float) StrictGeomUtility.toExternalValue(runningPos), y); runningPos += RenderableText.convert(g.getWidth()) + g.getSpacing().getMinimum(); } } g2.dispose(); } protected void drawComplexText(final RenderableComplexText renderableComplexText, final Graphics2D g2) { final long posX = renderableComplexText.getX(); final long posY = renderableComplexText.getY(); float baseline = renderableComplexText.getParagraphFontMetrics().getAscent(); final float y = (float) StrictGeomUtility.toExternalValue(posY) + baseline; renderableComplexText.getTextLayout().draw(g2, (float) StrictGeomUtility.toExternalValue(posX), y); g2.dispose(); } protected final CodePointBuffer getCodePointBuffer() { return codePointBuffer; } protected boolean isNormalTextSpacing(final RenderableText text) { return text.isNormalTextSpacing(); } 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 void clip(final StrictBounds bounds) { final Graphics2D g = getGraphics(); graphicsContexts.push(g); graphics = (Graphics2D) g.create(); graphics.clip(StrictGeomUtility.createAWTRectangle(bounds)); } public void clearClipping() { graphics.dispose(); graphics = graphicsContexts.pop(); } 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); } }