/* * 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 - 2013 Object Refinery Ltd, Pentaho Corporation and Contributors.. All rights reserved. */ package org.pentaho.reporting.engine.classic.core.modules.output.pageable.pdf.internal; import java.awt.Color; import java.awt.Graphics2D; import java.awt.Image; import java.awt.font.TextAttribute; import java.awt.geom.AffineTransform; import java.awt.geom.Rectangle2D; import java.text.AttributedCharacterIterator.Attribute; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.pentaho.reporting.engine.classic.core.AttributeNames; import org.pentaho.reporting.engine.classic.core.ElementAlignment; import org.pentaho.reporting.engine.classic.core.InvalidReportStateException; import org.pentaho.reporting.engine.classic.core.LocalImageContainer; import org.pentaho.reporting.engine.classic.core.URLImageContainer; import org.pentaho.reporting.engine.classic.core.imagemap.ImageMap; import org.pentaho.reporting.engine.classic.core.imagemap.ImageMapEntry; import org.pentaho.reporting.engine.classic.core.layout.model.InlineRenderBox; 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.PhysicalPageBox; 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.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.text.RichTextSpec; 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.internal.LogicalPageDrawable; 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.StyleSheet; import org.pentaho.reporting.engine.classic.core.style.TextDirection; import org.pentaho.reporting.engine.classic.core.style.TextStyleKeys; import org.pentaho.reporting.engine.classic.core.util.TypedMapWrapper; import org.pentaho.reporting.engine.classic.core.util.geom.StrictGeomUtility; import org.pentaho.reporting.libraries.base.util.LFUMap; import org.pentaho.reporting.libraries.base.util.StringUtils; import org.pentaho.reporting.libraries.base.util.WaitingImageObserver; import org.pentaho.reporting.libraries.fonts.encoding.CodePointBuffer; import org.pentaho.reporting.libraries.fonts.itext.BaseFontFontMetrics; import org.pentaho.reporting.libraries.fonts.registry.FontNativeContext; import org.pentaho.reporting.libraries.fonts.text.Spacing; import org.pentaho.reporting.libraries.resourceloader.Resource; import org.pentaho.reporting.libraries.resourceloader.ResourceKey; import org.pentaho.reporting.libraries.resourceloader.ResourceManager; import org.pentaho.reporting.libraries.resourceloader.factory.drawable.DrawableWrapper; import org.pentaho.reporting.libraries.xmlns.LibXmlInfo; import com.lowagie.text.Chunk; import com.lowagie.text.DocumentException; import com.lowagie.text.Element; import com.lowagie.text.Font; import com.lowagie.text.Phrase; import com.lowagie.text.Rectangle; import com.lowagie.text.pdf.BaseFont; import com.lowagie.text.pdf.ColumnText; import com.lowagie.text.pdf.PdfAction; import com.lowagie.text.pdf.PdfAnnotation; import com.lowagie.text.pdf.PdfContentByte; import com.lowagie.text.pdf.PdfDestination; import com.lowagie.text.pdf.PdfName; import com.lowagie.text.pdf.PdfObject; import com.lowagie.text.pdf.PdfOutline; import com.lowagie.text.pdf.PdfString; import com.lowagie.text.pdf.PdfTextArray; import com.lowagie.text.pdf.PdfWriter; /** * Creation-Date: 17.07.2007, 18:41:46 * * @author Thomas Morgner */ public class PdfLogicalPageDrawable extends LogicalPageDrawable { private static final Log logger = LogFactory.getLog( PdfLogicalPageDrawable.class ); protected static class PdfTextSpec extends TextSpec { private BaseFontFontMetrics fontMetrics; private PdfContentByte contentByte; protected PdfTextSpec( final StyleSheet layoutContext, final PdfOutputProcessorMetaData metaData, final PdfGraphics2D g2, final BaseFontFontMetrics fontMetrics, final PdfContentByte cb ) { super( layoutContext, metaData, g2 ); if ( fontMetrics == null ) { throw new NullPointerException(); } if ( cb == null ) { throw new NullPointerException(); } this.fontMetrics = fontMetrics; this.contentByte = cb; } public BaseFontFontMetrics getFontMetrics() { return fontMetrics; } public PdfContentByte getContentByte() { return contentByte; } public void close() { contentByte.endText(); // super.close(); // we do not dispose the graphics as we are working on the original object } } private static final float ITALIC_ANGLE = 0.21256f; private PdfWriter writer; private float globalHeight; private boolean globalEmbed; private LFUMap<ResourceKey, com.lowagie.text.Image> imageCache; private char version; private PdfImageHandler imageHandler; private boolean legacyLineHeightCalc; public PdfLogicalPageDrawable( final PdfWriter writer, final LFUMap<ResourceKey, com.lowagie.text.Image> imageCache, final char version ) { if ( writer == null ) { throw new NullPointerException(); } if ( imageCache == null ) { throw new NullPointerException(); } this.writer = writer; this.imageCache = imageCache; this.version = version; } public void init( final LogicalPageBox rootBox, final OutputProcessorMetaData metaData, final ResourceManager resourceManager ) { throw new UnsupportedOperationException(); } public void init( final LogicalPageBox rootBox, final PdfOutputProcessorMetaData metaData, final ResourceManager resourceManager, final PhysicalPageBox page ) { super.init( rootBox, metaData, resourceManager ); if ( page != null ) { this.globalHeight = (float) StrictGeomUtility.toExternalValue( page.getHeight() - page.getImageableY() + page.getGlobalY() ); } else { this.globalHeight = rootBox.getPageHeight(); } this.globalEmbed = getMetaData().isFeatureSupported( OutputProcessorFeature.EMBED_ALL_FONTS ); this.imageHandler = new PdfImageHandler( metaData, resourceManager, imageCache ); this.legacyLineHeightCalc = metaData.isFeatureSupported( OutputProcessorFeature.LEGACY_LINEHEIGHT_CALC ); } public PdfOutputProcessorMetaData getMetaData() { return (PdfOutputProcessorMetaData) super.getMetaData(); } protected float getGlobalHeight() { return globalHeight; } /** * 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 ) { super.draw( graphics, area ); } protected void processLinksAndAnchors( final RenderNode box ) { final StyleSheet styleSheet = box.getStyleSheet(); if ( drawPdfScript( box ) == false ) { 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 boolean drawPdfScript( final RenderNode box ) { final Object attribute = box.getAttributes().getAttribute( AttributeNames.Pdf.NAMESPACE, AttributeNames.Pdf.SCRIPT_ACTION ); if ( attribute == null ) { return false; } final String attributeText = String.valueOf( attribute ); final PdfAction action = PdfAction.javaScript( attributeText, writer, false ); final AffineTransform affineTransform = getGraphics().getTransform(); final float translateX = (float) affineTransform.getTranslateX(); final float leftX = translateX + (float) ( StrictGeomUtility.toExternalValue( box.getX() ) ); final float rightX = translateX + (float) ( StrictGeomUtility.toExternalValue( box.getX() + box.getWidth() ) ); final float lowerY = (float) ( globalHeight - StrictGeomUtility.toExternalValue( box.getY() + box.getHeight() ) ); final float upperY = (float) ( globalHeight - StrictGeomUtility.toExternalValue( box.getY() ) ); final PdfAnnotation annotation = new PdfAnnotation( writer, leftX, lowerY, rightX, upperY, action ); writer.addAnnotation( annotation ); return true; } protected void drawAnchor( final RenderNode content ) { if ( content.isNodeVisible( getDrawArea() ) == false ) { return; } final String anchorName = (String) content.getStyleSheet().getStyleProperty( ElementStyleKeys.ANCHOR_NAME ); if ( anchorName == null ) { return; } final AffineTransform affineTransform = getGraphics().getTransform(); final float translateX = (float) affineTransform.getTranslateX(); final float upperY = translateX + (float) ( globalHeight - StrictGeomUtility.toExternalValue( content.getY() ) ); final float leftX = (float) ( StrictGeomUtility.toExternalValue( content.getX() ) ); final PdfDestination dest = new PdfDestination( PdfDestination.FIT, leftX, upperY, 0 ); writer.getDirectContent().localDestination( anchorName, dest ); } protected void drawBookmark( final RenderNode box, final String bookmark ) { if ( box.isNodeVisible( getDrawArea() ) == false ) { return; } final PdfOutline root = writer.getDirectContent().getRootOutline(); final AffineTransform affineTransform = getGraphics().getTransform(); final float translateX = (float) affineTransform.getTranslateX(); final float upperY = translateX + (float) ( globalHeight - StrictGeomUtility.toExternalValue( box.getY() ) ); final float leftX = (float) ( StrictGeomUtility.toExternalValue( box.getX() ) ); final PdfDestination dest = new PdfDestination( PdfDestination.FIT, leftX, upperY, 0 ); new PdfOutline( root, dest, bookmark ); // destination will always point to the 'current' page // todo: Make this a hierarchy .. } protected void drawHyperlink( final RenderNode box, final String target, final String window, final String title ) { if ( box.isNodeVisible( getDrawArea() ) == false ) { return; } final PdfAction action = createActionForLink( target ); final AffineTransform affineTransform = getGraphics().getTransform(); final float translateX = (float) affineTransform.getTranslateX(); final float leftX = translateX + (float) ( StrictGeomUtility.toExternalValue( box.getX() ) ); final float rightX = translateX + (float) ( StrictGeomUtility.toExternalValue( box.getX() + box.getWidth() ) ); final float lowerY = (float) ( globalHeight - StrictGeomUtility.toExternalValue( box.getY() + box.getHeight() ) ); final float upperY = (float) ( globalHeight - StrictGeomUtility.toExternalValue( box.getY() ) ); if ( action != null ) { final PdfAnnotation annotation = new PdfAnnotation( writer, leftX, lowerY, rightX, upperY, action ); writer.addAnnotation( annotation ); } else if ( StringUtils.isEmpty( title ) == false ) { final Rectangle rect = new Rectangle( leftX, lowerY, rightX, upperY ); final PdfAnnotation commentAnnotation = PdfAnnotation.createText( writer, rect, "Tooltip", title, false, null ); commentAnnotation.setAppearance( PdfAnnotation.APPEARANCE_NORMAL, writer.getDirectContent().createAppearance( rect.getWidth(), rect.getHeight() ) ); writer.addAnnotation( commentAnnotation ); } } private PdfAction createActionForLink( final String target ) { if ( StringUtils.isEmpty( target ) ) { return null; } final PdfAction action = new PdfAction(); if ( target.startsWith( "#" ) ) { // its a local link .. action.put( PdfName.S, PdfName.GOTO ); action.put( PdfName.D, new PdfString( target.substring( 1 ) ) ); } else { action.put( PdfName.S, PdfName.URI ); action.put( PdfName.URI, new PdfString( target ) ); } return action; } protected void drawText( final RenderableText renderableText, final long contentX2 ) { if ( renderableText.getLength() == 0 ) { return; } final long posX = renderableText.getX(); final long posY = renderableText.getY(); final float x1 = (float) ( StrictGeomUtility.toExternalValue( posX ) ); final PdfContentByte cb; PdfTextSpec textSpec = (PdfTextSpec) getTextSpec(); if ( textSpec == null ) { final StyleSheet layoutContext = renderableText.getStyleSheet(); // The code below may be weird, but at least it is predictable weird. final String fontName = getMetaData().getNormalizedFontFamilyName( (String) layoutContext.getStyleProperty( TextStyleKeys.FONT ) ); final String encoding = (String) layoutContext.getStyleProperty( TextStyleKeys.FONTENCODING ); final float fontSize = (float) layoutContext.getDoubleStyleProperty( TextStyleKeys.FONTSIZE, 10 ); final boolean embed = globalEmbed || layoutContext.getBooleanStyleProperty( TextStyleKeys.EMBEDDED_FONT ); final boolean bold = layoutContext.getBooleanStyleProperty( TextStyleKeys.BOLD ); final boolean italics = layoutContext.getBooleanStyleProperty( TextStyleKeys.ITALIC ); final BaseFontFontMetrics fontMetrics = getMetaData().getBaseFontFontMetrics( fontName, fontSize, bold, italics, encoding, embed, false ); final PdfGraphics2D g2 = (PdfGraphics2D) getGraphics(); final Color cssColor = (Color) layoutContext.getStyleProperty( ElementStyleKeys.PAINT ); g2.setPaint( cssColor ); g2.setFillPaint(); g2.setStrokePaint(); // final float translateY = (float) affineTransform.getTranslateY(); cb = g2.getRawContentByte(); textSpec = new PdfTextSpec( layoutContext, getMetaData(), g2, fontMetrics, cb ); setTextSpec( textSpec ); cb.beginText(); cb.setFontAndSize( fontMetrics.getBaseFont(), fontSize ); } else { cb = textSpec.getContentByte(); } final BaseFontFontMetrics baseFontRecord = textSpec.getFontMetrics(); final BaseFont baseFont = baseFontRecord.getBaseFont(); final float ascent; if ( legacyLineHeightCalc ) { final float awtAscent = baseFont.getFontDescriptor( BaseFont.AWT_ASCENT, textSpec.getFontSize() ); final float awtLeading = baseFont.getFontDescriptor( BaseFont.AWT_LEADING, textSpec.getFontSize() ); ascent = awtAscent + awtLeading; } else { ascent = baseFont.getFontDescriptor( BaseFont.BBOXURY, textSpec.getFontSize() ); } final float y2 = (float) ( StrictGeomUtility.toExternalValue( posY ) + ascent ); final float y = globalHeight - y2; final AffineTransform affineTransform = textSpec.getGraphics().getTransform(); final float translateX = (float) affineTransform.getTranslateX(); final FontNativeContext nativeContext = baseFontRecord.getNativeContext(); if ( baseFontRecord.isTrueTypeFont() && textSpec.isBold() && nativeContext.isNativeBold() == false ) { final float strokeWidth = textSpec.getFontSize() / 30.0f; // right from iText ... if ( strokeWidth == 1 ) { cb.setTextRenderingMode( PdfContentByte.TEXT_RENDER_MODE_FILL ); } else { cb.setTextRenderingMode( PdfContentByte.TEXT_RENDER_MODE_FILL_STROKE ); cb.setLineWidth( strokeWidth ); } } else { cb.setTextRenderingMode( PdfContentByte.TEXT_RENDER_MODE_FILL ); } // if the font does not declare to be italics already, emulate it .. if ( baseFontRecord.isTrueTypeFont() && textSpec.isItalics() && nativeContext.isNativeItalics() == false ) { final float italicAngle = baseFont.getFontDescriptor( BaseFont.ITALICANGLE, textSpec.getFontSize() ); if ( italicAngle == 0 ) { // italics requested, but the font itself does not supply italics gylphs. cb.setTextMatrix( 1, 0, PdfLogicalPageDrawable.ITALIC_ANGLE, 1, x1 + translateX, y ); } else { cb.setTextMatrix( x1 + translateX, y ); } } else { cb.setTextMatrix( x1 + translateX, y ); } final OutputProcessorMetaData metaData = getMetaData(); final GlyphList gs = renderableText.getGlyphs(); final int offset = renderableText.getOffset(); final CodePointBuffer codePointBuffer = getCodePointBuffer(); if ( metaData.isFeatureSupported( OutputProcessorFeature.FAST_FONTRENDERING ) && isNormalTextSpacing( renderableText ) ) { final int maxLength = renderableText.computeMaximumTextSize( contentX2 ); final String text = gs.getText( renderableText.getOffset(), maxLength, codePointBuffer ); cb.showText( text ); } else { final PdfTextArray textArray = new PdfTextArray(); final StringBuilder buffer = new StringBuilder( gs.getSize() ); final int maxPos = offset + renderableText.computeMaximumTextSize( contentX2 ); for ( int i = offset; i < maxPos; i++ ) { final Glyph g = gs.getGlyph( i ); final Spacing spacing = g.getSpacing(); if ( i != offset ) { final float optimum = (float) StrictGeomUtility.toFontMetricsValue( spacing.getMinimum() ); if ( optimum != 0 ) { textArray.add( buffer.toString() ); textArray.add( -optimum / textSpec.getFontSize() ); buffer.setLength( 0 ); } } final String text = gs.getGlyphAsString( i, codePointBuffer ); buffer.append( text ); } if ( buffer.length() > 0 ) { textArray.add( buffer.toString() ); } cb.showText( textArray ); } } private ParagraphRenderBox paragraphContext; protected void processParagraphChilds( final ParagraphRenderBox box ) { try { paragraphContext = box; super.processParagraphChilds( box ); } finally { paragraphContext = null; } } private float computeLeadingSafetyMargin( final RenderableComplexText node ) { if ( paragraphContext == null ) { return -SAFETY_MARGIN; } ElementAlignment alignment; if ( node.getNext() == null ) { alignment = paragraphContext.getLastLineAlignment(); } else { alignment = paragraphContext.getTextAlignment(); } if ( ElementAlignment.LEFT.equals( alignment ) ) { return 0; } else if ( ElementAlignment.RIGHT.equals( alignment ) ) { return -SAFETY_MARGIN * 2; } else if ( ElementAlignment.CENTER.equals( alignment ) ) { return -SAFETY_MARGIN; } else { if ( computeRunDirection( node ) == PdfWriter.RUN_DIRECTION_RTL ) { return -SAFETY_MARGIN * 2; } else { return 0f; } } } private static final float SAFETY_MARGIN = 0.1f; private float computeTrailingSafetyMargin( final RenderableComplexText node ) { if ( paragraphContext == null ) { return SAFETY_MARGIN; } ElementAlignment alignment; if ( node.getNext() == null ) { alignment = paragraphContext.getLastLineAlignment(); } else { alignment = paragraphContext.getTextAlignment(); } if ( ElementAlignment.LEFT.equals( alignment ) ) { return SAFETY_MARGIN * 2; } else if ( ElementAlignment.RIGHT.equals( alignment ) ) { return 0; } else if ( ElementAlignment.CENTER.equals( alignment ) ) { return SAFETY_MARGIN; } else { if ( computeRunDirection( node ) == PdfWriter.RUN_DIRECTION_RTL ) { return 0f; } else { return SAFETY_MARGIN * 2; } } } private int mapAlignment( final RenderableComplexText node ) { ElementAlignment alignment; if ( node.getNext() == null ) { alignment = paragraphContext.getLastLineAlignment(); } else { alignment = paragraphContext.getTextAlignment(); } if ( ElementAlignment.LEFT.equals( alignment ) ) { return Element.ALIGN_LEFT; } else if ( ElementAlignment.RIGHT.equals( alignment ) ) { return Element.ALIGN_RIGHT; } else if ( ElementAlignment.CENTER.equals( alignment ) ) { return Element.ALIGN_CENTER; } else { return Element.ALIGN_JUSTIFIED; } } protected void drawComplexText( final RenderableComplexText node, final Graphics2D g2 ) { try { final Phrase p = createPhrase( node ); final ColumnConfig cc = createColumnText( node ); final PdfGraphics2D pg2 = (PdfGraphics2D) getGraphics(); final PdfContentByte cb = pg2.getRawContentByte(); ColumnText ct = cc.reconfigure( cb, p ); ct.setText( p ); if ( ct.go( false ) == ColumnText.NO_MORE_COLUMN ) { throw new InvalidReportStateException( "iText signaled an error when printing text. Failing to prevent silent data-loss: Width=" + ct.getFilledWidth() ); } } catch ( DocumentException e ) { throw new InvalidReportStateException( e ); } } private static class ColumnConfig { private float llx; private float lly; private float urx; private float ury; private float leading; private int alignment; private int runDirection; private ColumnConfig( final float llx, final float lly, final float urx, final float ury, final float leading, final int alignment, final int runDirection ) { this.llx = llx; this.lly = lly; this.urx = urx; this.ury = ury; this.leading = leading; this.alignment = alignment; this.runDirection = runDirection; } public ColumnText reconfigure( PdfContentByte cb, Phrase p ) { float filledWidth = ColumnText.getWidth( p, runDirection, 0 ) + 0.5f; ColumnText ct = new ColumnText( cb ); ct.setRunDirection( runDirection ); if ( alignment == Element.ALIGN_LEFT ) { ct.setSimpleColumn( llx, lly, llx + filledWidth, ury, leading, alignment ); } else if ( alignment == Element.ALIGN_RIGHT ) { ct.setSimpleColumn( urx - filledWidth, lly, urx, ury, leading, alignment ); } else if ( alignment == Element.ALIGN_CENTER ) { float delta = ( ( urx - llx ) - filledWidth ) / 2; ct.setSimpleColumn( urx + delta, lly, urx - delta, ury, leading, alignment ); } else { ct.setSimpleColumn( llx, lly, urx, ury, leading, alignment ); } return ct; } } private ColumnConfig createColumnText( final RenderableComplexText node ) { final AffineTransform affineTransform = getGraphics().getTransform(); final float translateX = (float) affineTransform.getTranslateX(); final float width = (float) StrictGeomUtility.toExternalValue( node.getWidth() ); final float nodeX = (float) StrictGeomUtility.toExternalValue( node.getX() ); float marginLeft = width * computeLeadingSafetyMargin( node ); final float llx = translateX + nodeX + marginLeft; float marginRight = width * computeTrailingSafetyMargin( node ); final float urx = translateX + nodeX + width + marginRight; final float y2 = (float) ( StrictGeomUtility.toExternalValue( node.getY() ) ); final float lly = globalHeight - y2; final float ury = (float) ( lly - 1.2 * StrictGeomUtility.toExternalValue( node.getHeight() ) ); final ColumnConfig ct = new ColumnConfig( llx, lly, urx, ury, node.getParagraphFontMetrics().getAscent(), mapAlignment( node ), computeRunDirection( node ) ); return ct; } private Phrase createPhrase( final RenderableComplexText node ) { Phrase p = new Phrase(); RichTextSpec text = node.getRichText(); for ( RichTextSpec.StyledChunk c : text.getStyleChunks() ) { TypedMapWrapper<Attribute, Object> attributes = new TypedMapWrapper<Attribute, Object>( c.getAttributes() ); final Number size = attributes.get( TextAttribute.SIZE, 10f, Number.class ); final PdfTextSpec pdfTextSpec = computeFont( c ); final int style = computeStyle( attributes, pdfTextSpec ); final Color paint = (Color) c.getStyleSheet().getStyleProperty( ElementStyleKeys.PAINT ); // add chunks BaseFont baseFont = pdfTextSpec.getFontMetrics().getBaseFont(); Font font = new Font( baseFont, size.floatValue(), style, paint ); if ( c.getOriginatingTextNode() instanceof RenderableReplacedContentBox ) { RenderableReplacedContentBox content = (RenderableReplacedContentBox) c.getOriginatingTextNode(); com.lowagie.text.Image image = imageHandler.createImage( content ); if ( image != null ) { Chunk chunk = new Chunk( image, 0, 0 ); // chunk.setFont(font); p.add( chunk ); } } else { String textToPrint = c.getText(); Chunk chunk = new Chunk( textToPrint, font ); p.add( chunk ); } } return p; } private int computeRunDirection( final RenderableComplexText node ) { Object styleProperty = node.getStyleSheet().getStyleProperty( TextStyleKeys.DIRECTION, TextDirection.LTR ); if ( TextDirection.RTL.equals( styleProperty ) ) { return PdfWriter.RUN_DIRECTION_RTL; } else { return PdfWriter.RUN_DIRECTION_LTR; } } private PdfTextSpec computeFont( final RichTextSpec.StyledChunk c ) { final StyleSheet layoutContext = c.getStyleSheet(); final PdfGraphics2D g2 = (PdfGraphics2D) getGraphics(); final PdfContentByte cb = g2.getRawContentByte(); final TypedMapWrapper<Attribute, Object> attributes = new TypedMapWrapper<Attribute, Object>( c.getAttributes() ); final String fontName = attributes.get( TextAttribute.FAMILY, String.class ); final Number fontSize = attributes.get( TextAttribute.SIZE, 10f, Number.class ); final String encoding = (String) layoutContext.getStyleProperty( TextStyleKeys.FONTENCODING ); final boolean embed = globalEmbed || layoutContext.getBooleanStyleProperty( TextStyleKeys.EMBEDDED_FONT ); final Float weightRaw = attributes.get( TextAttribute.WEIGHT, TextAttribute.WEIGHT_REGULAR, Float.class ); final Float italicsRaw = attributes.get( TextAttribute.POSTURE, TextAttribute.POSTURE_OBLIQUE, Float.class ); final BaseFontFontMetrics fontMetrics = getMetaData().getBaseFontFontMetrics( fontName, fontSize.doubleValue(), weightRaw >= TextAttribute.WEIGHT_DEMIBOLD, italicsRaw >= TextAttribute.POSTURE_OBLIQUE, encoding, embed, false ); return new PdfTextSpec( layoutContext, getMetaData(), g2, fontMetrics, cb ); } private int computeStyle( final TypedMapWrapper<Attribute, Object> attributes, final PdfTextSpec pdfTextSpec ) { final Float weight = attributes.get( TextAttribute.WEIGHT, TextAttribute.WEIGHT_REGULAR, Float.class ); final Float italics = attributes.get( TextAttribute.POSTURE, TextAttribute.POSTURE_OBLIQUE, Float.class ); final boolean underlined = attributes.exists( TextAttribute.UNDERLINE ); final boolean strikethrough = attributes.exists( TextAttribute.STRIKETHROUGH ); FontNativeContext nativeContext = pdfTextSpec.getFontMetrics().getNativeContext(); int style = 0; if ( nativeContext.isNativeBold() == false && weight >= TextAttribute.WEIGHT_DEMIBOLD ) { style |= Font.BOLD; } if ( nativeContext.isNativeItalics() == false && italics >= TextAttribute.POSTURE_OBLIQUE ) { style |= Font.ITALIC; } if ( underlined ) { style |= Font.UNDERLINE; } if ( strikethrough ) { style |= Font.STRIKETHRU; } return style; } protected boolean startInlineBox( final InlineRenderBox box ) { if ( box.getStaticBoxLayoutProperties().isVisible() == false ) { return false; } if ( box.isBoxVisible( getDrawArea() ) == false ) { return false; } return super.startInlineBox( box ); } protected void finishInlineBox( final InlineRenderBox box ) { if ( box.getStaticBoxLayoutProperties().isVisible() == false ) { return; } if ( box.isBoxVisible( getDrawArea() ) == false ) { return; } super.finishInlineBox( box ); } protected void drawReplacedContent( final RenderableReplacedContentBox content ) { final Graphics2D g2 = getGraphics(); final Object o = content.getContent().getRawObject(); if ( o instanceof DrawableWrapper ) { final DrawableWrapper drawableWrapper = (DrawableWrapper) o; if ( drawDrawable( content, g2, drawableWrapper ) ) { drawImageMap( content ); } return; } if ( o instanceof Image ) { if ( drawImage( content, (Image) o ) ) { drawImageMap( content ); } return; } if ( o instanceof URLImageContainer ) { final URLImageContainer imageContainer = (URLImageContainer) o; com.lowagie.text.Image instance = imageHandler.createImage( imageContainer ); if ( instance != null ) { try { ResourceManager resourceManager = getResourceManager(); ResourceKey resource = imageContainer.getResourceKey(); final Resource imageWrapped = resourceManager.create( resource, null, Image.class ); final Image image = (Image) imageWrapped.getResource(); if ( drawImage( content, image, instance ) ) { drawImageMap( content ); } return; } catch ( Exception e ) { PdfLogicalPageDrawable.logger.info( "URL-image cannot be rendered, as the image was not loadable.", e ); } } } if ( o instanceof LocalImageContainer ) { final LocalImageContainer imageContainer = (LocalImageContainer) o; final Image image = imageContainer.getImage(); if ( drawImage( content, image ) ) { drawImageMap( content ); } } else { PdfLogicalPageDrawable.logger.debug( "Unable to handle " + o ); } } protected void drawImageMap( final RenderableReplacedContentBox content ) { if ( version < '6' ) { return; } final ImageMap imageMap = RenderUtility.extractImageMap( content ); // only generate a image map, if the user does not specify their own onw via the override. // Of course, they would have to provide the map by other means as well. if ( imageMap == null ) { return; } final ImageMapEntry[] imageMapEntries = imageMap.getMapEntries(); for ( int i = 0; i < imageMapEntries.length; i++ ) { final ImageMapEntry imageMapEntry = imageMapEntries[i]; final String link = imageMapEntry.getAttribute( LibXmlInfo.XHTML_NAMESPACE, "href" ); final String tooltip = imageMapEntry.getAttribute( LibXmlInfo.XHTML_NAMESPACE, "title" ); if ( StringUtils.isEmpty( tooltip ) ) { continue; } final AffineTransform affineTransform = getGraphics().getTransform(); final float translateX = (float) affineTransform.getTranslateX(); final int x = (int) ( translateX + StrictGeomUtility.toExternalValue( content.getX() ) ); final int y = (int) StrictGeomUtility.toExternalValue( content.getY() ); final float[] translatedCoords = translateCoordinates( imageMapEntry.getAreaCoordinates(), x, y ); final PolygonAnnotation polygonAnnotation = new PolygonAnnotation( writer, translatedCoords ); polygonAnnotation.put( PdfName.CONTENTS, new PdfString( tooltip, PdfObject.TEXT_UNICODE ) ); writer.addAnnotation( polygonAnnotation ); } } private float[] translateCoordinates( final float[] coords, final float dx, final float dy ) { final float[] floats = coords.clone(); if ( floats.length % 2 != 0 ) { throw new IllegalArgumentException( "Corrdinates are not x/y pairs" ); } for ( int i = 0; i < floats.length; i += 2 ) { floats[i] += dx; floats[i + 1] = globalHeight - floats[i + 1] + dy; } return floats; } protected boolean drawImage( final RenderableReplacedContentBox content, final Image image, final com.lowagie.text.Image itextImage ) { 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 ) { PdfLogicalPageDrawable.logger.debug( "Error: Image area is empty: " + content ); return false; } final 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 = getMetaData().getNumericFeatureValue( OutputProcessorFeature.DEVICE_RESOLUTION ); if ( getMetaData().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 ); scaleTransform = AffineTransform.getScaleInstance( scaleX, scaleY ); } final PdfGraphics2D pdfGraphics2D = (PdfGraphics2D) g2; pdfGraphics2D.drawPdfImage( itextImage, image, scaleTransform, null ); g2.dispose(); return true; } }