/* * 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.layout.text; import java.util.ArrayList; import org.pentaho.reporting.engine.classic.core.ReportAttributeMap; import org.pentaho.reporting.engine.classic.core.layout.model.RenderNode; import org.pentaho.reporting.engine.classic.core.layout.model.RenderableText; import org.pentaho.reporting.engine.classic.core.layout.model.SpacerRenderNode; 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.metadata.ElementType; 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.style.TextWrap; import org.pentaho.reporting.engine.classic.core.style.WhitespaceCollapse; import org.pentaho.reporting.engine.classic.core.util.InstanceID; import org.pentaho.reporting.engine.classic.core.util.geom.StrictGeomUtility; import org.pentaho.reporting.libraries.base.util.ObjectUtilities; import org.pentaho.reporting.libraries.fonts.registry.FontMetrics; import org.pentaho.reporting.libraries.fonts.text.ClassificationProducer; import org.pentaho.reporting.libraries.fonts.text.DefaultLanguageClassifier; import org.pentaho.reporting.libraries.fonts.text.GraphemeClusterProducer; import org.pentaho.reporting.libraries.fonts.text.LanguageClassifier; import org.pentaho.reporting.libraries.fonts.text.Spacing; import org.pentaho.reporting.libraries.fonts.text.SpacingProducer; import org.pentaho.reporting.libraries.fonts.text.StaticSpacingProducer; import org.pentaho.reporting.libraries.fonts.text.breaks.BreakOpportunityProducer; import org.pentaho.reporting.libraries.fonts.text.breaks.LineBreakProducer; import org.pentaho.reporting.libraries.fonts.text.breaks.WordBreakProducer; import org.pentaho.reporting.libraries.fonts.text.classifier.GlyphClassificationProducer; import org.pentaho.reporting.libraries.fonts.text.classifier.WhitespaceClassificationProducer; import org.pentaho.reporting.libraries.fonts.text.font.FontSizeProducer; import org.pentaho.reporting.libraries.fonts.text.font.GlyphMetrics; import org.pentaho.reporting.libraries.fonts.text.font.KerningProducer; import org.pentaho.reporting.libraries.fonts.text.font.NoKerningProducer; import org.pentaho.reporting.libraries.fonts.text.font.VariableFontSizeProducer; import org.pentaho.reporting.libraries.fonts.text.whitespace.CollapseWhiteSpaceFilter; import org.pentaho.reporting.libraries.fonts.text.whitespace.DiscardWhiteSpaceFilter; import org.pentaho.reporting.libraries.fonts.text.whitespace.PreserveBreaksWhiteSpaceFilter; import org.pentaho.reporting.libraries.fonts.text.whitespace.PreserveWhiteSpaceFilter; import org.pentaho.reporting.libraries.fonts.text.whitespace.WhiteSpaceFilter; import org.pentaho.reporting.libraries.fonts.tools.FontStrictGeomUtility; /** * Creation-Date: 03.04.2007, 16:43:48 * * @author Thomas Morgner */ public final class DefaultRenderableTextFactory implements RenderableTextFactory { private static final RenderNode[] EMPTY_RENDER_NODE = new RenderNode[0]; private static final RenderableText[] EMPTY_TEXT = new RenderableText[0]; private static final GlyphList EMPTY_GLYPHS = new GlyphList( 1 ).lock(); private static final int[] END_OF_TEXT = new int[] { ClassificationProducer.END_OF_TEXT }; private GraphemeClusterProducer clusterProducer; private boolean startText; private FontSizeProducer fontSizeProducer; private KerningProducer kerningProducer; private SpacingProducer spacingProducer; private Spacing spacingProducerKey; private BreakOpportunityProducer breakOpportunityProducer; private WhiteSpaceFilter whitespaceFilter; private GlyphClassificationProducer classificationProducer; private StyleSheet layoutContext; private LanguageClassifier languageClassifier; private transient GlyphMetrics dims; private ArrayList<RenderNode> words; private GlyphList glyphList; private long leadingMargin; private int spaceCount; private int lastLanguage; private transient FontMetrics fontMetrics; private OutputProcessorMetaData metaData; // cached instance .. private NoKerningProducer noKerningProducer; private WhitespaceCollapse whitespaceFilterValue; private WhitespaceCollapse whitespaceCollapseValue; private TextWrap breakOpportunityValue; private long wordSpacing; private ReportAttributeMap<Object> attributeMap; private ElementType elementType; private ExtendedBaselineInfo uniformBaselineInfo; private InstanceID instanceId; public DefaultRenderableTextFactory( final OutputProcessorMetaData metaData ) { this.metaData = metaData; this.clusterProducer = new GraphemeClusterProducer(); this.languageClassifier = new DefaultLanguageClassifier(); this.startText = true; this.words = new ArrayList<RenderNode>( 20 ); this.dims = new GlyphMetrics(); this.noKerningProducer = new NoKerningProducer(); this.spacingProducer = new StaticSpacingProducer( Spacing.EMPTY_SPACING ); this.spacingProducerKey = Spacing.EMPTY_SPACING; this.glyphList = new GlyphList( 100 ); } /** * The text is given as CodePoints. * * @param text * @return */ public RenderNode[] createText( final int[] text, final int offset, final int length, final StyleSheet layoutContext, final ElementType elementType, final InstanceID instanceId, final ReportAttributeMap<Object> attributeMap ) { this.instanceId = instanceId; if ( layoutContext == null ) { throw new NullPointerException(); } if ( attributeMap == null ) { throw new NullPointerException(); } if ( elementType == null ) { throw new NullPointerException(); } if ( text == null ) { throw new NullPointerException(); } this.layoutContext = layoutContext; // this.parentLayoutContext = new NodeLayoutProperties(majorAxis, minorAxis, layoutContext); this.elementType = elementType; this.attributeMap = attributeMap; this.fontMetrics = metaData.getFontMetrics( layoutContext ); this.uniformBaselineInfo = null; kerningProducer = createKerningProducer( layoutContext ); fontSizeProducer = createFontSizeProducer( layoutContext ); spacingProducer = createSpacingProducer( layoutContext ); breakOpportunityProducer = createBreakProducer( layoutContext ); whitespaceFilter = createWhitespaceFilter( layoutContext ); classificationProducer = createGlyphClassifier( layoutContext ); this.layoutContext = layoutContext; if ( metaData.isFeatureSupported( OutputProcessorFeature.SPACING_SUPPORTED ) ) { this.wordSpacing = FontStrictGeomUtility.toInternalValue( layoutContext.getDoubleStyleProperty( TextStyleKeys.WORD_SPACING, 0 ) ); } else { this.wordSpacing = 0; } if ( startText ) { whitespaceFilter.filter( ClassificationProducer.START_OF_TEXT ); breakOpportunityProducer.createBreakOpportunity( ClassificationProducer.START_OF_TEXT ); kerningProducer.getKerning( ClassificationProducer.START_OF_TEXT ); startText = false; } return processText( text, offset, length ); } protected RenderNode[] processText( final int[] text, final int offset, final int length ) { final int maxLen = Math.min( length + offset, text.length ); int clusterStartIdx = offset < maxLen ? 0 : -1; for ( int i = offset; i < maxLen; i++ ) { final int codePoint = text[i]; final boolean clusterStarted = this.clusterProducer.createGraphemeCluster( codePoint ); // ignore the first cluster start; we need to see the whole cluster. if ( clusterStarted ) { if ( i > offset ) { final int extraCharLength = i - clusterStartIdx - 1; addGlyph( text, clusterStartIdx, extraCharLength ); } clusterStartIdx = i; } } // Process the last cluster ... if ( clusterStartIdx >= offset ) { final int extraCharLength = maxLen - clusterStartIdx - 1; addGlyph( text, clusterStartIdx, extraCharLength ); } if ( words.isEmpty() == false ) { final RenderNode[] renderableTexts = words.toArray( new RenderNode[words.size()] ); words.clear(); return renderableTexts; } else { // we did not produce any text. return DefaultRenderableTextFactory.EMPTY_RENDER_NODE; } } protected void addGlyph( final int[] text, final int offset, final int extraCharCount ) { // Log.debug ("Processing " + rawCodePoint); final int rawCodePoint = text[offset]; if ( rawCodePoint == ClassificationProducer.END_OF_TEXT ) { whitespaceFilter.filter( rawCodePoint ); classificationProducer.getClassification( rawCodePoint ); kerningProducer.getKerning( rawCodePoint ); breakOpportunityProducer.createBreakOpportunity( rawCodePoint ); spacingProducer.createSpacing( rawCodePoint ); fontSizeProducer.getCharacterSize( rawCodePoint, dims ); if ( leadingMargin > 0 || glyphList.getSize() != 0 ) { addWord( false ); } else { // finish up .. glyphList.clear(); leadingMargin = 0; spaceCount = 0; } return; } int codePoint = whitespaceFilter.filter( rawCodePoint ); // No matter whether we will ignore the result, we have to keep our // factories up and running. These beasts need to see all data, no // matter what get printed later. if ( codePoint == WhiteSpaceFilter.STRIP_WHITESPACE ) { // if we dont have extra-chars, ignore the thing .. if ( extraCharCount == 0 ) { return; } else { // convert it into a space. This might be invalid, but will work for now. codePoint = DiscardWhiteSpaceFilter.ZERO_WIDTH; } } int glyphClassification = classificationProducer.getClassification( codePoint ); final long kerning = kerningProducer.getKerning( codePoint ); int breakweight = breakOpportunityProducer.createBreakOpportunity( codePoint ); final Spacing spacing = spacingProducer.createSpacing( codePoint ); dims = fontSizeProducer.getCharacterSize( codePoint, dims ); int width = dims.getWidth(); int height = dims.getHeight(); lastLanguage = languageClassifier.getScript( codePoint ); for ( int i = 0; i < extraCharCount; i++ ) { final int extraChar = text[offset + i + 1]; dims = fontSizeProducer.getCharacterSize( extraChar, dims ); width = Math.max( width, ( dims.getWidth() & 0x7FFFFFFF ) ); height = Math.max( height, ( dims.getHeight() & 0x7FFFFFFF ) ); breakweight = breakOpportunityProducer.createBreakOpportunity( extraChar ); glyphClassification = classificationProducer.getClassification( extraChar ); } if ( ( Glyph.SPACE_CHAR == glyphClassification ) && isWordBreak( breakweight ) ) { // Finish the current word ... final boolean forceLinebreak = breakweight == BreakOpportunityProducer.BREAK_LINE; if ( glyphList.isEmpty() == false || forceLinebreak ) { addWord( forceLinebreak ); if ( forceLinebreak ) { return; } } // This character can be stripped. We increase the leading margin of the // next word by the character's width. leadingMargin += width + wordSpacing; spaceCount += 1; // Log.debug ("Increasing Margin"); return; } // final Glyph glyph = new DefaultGlyph(codePoint, breakweight, glyphClassification, spacing, width, height, // dims.getBaselinePosition(), (int) kerning, extraChars); glyphList.addGlyphData( text, offset, extraCharCount + 1, breakweight, glyphClassification, spacing, width, height, dims.getBaselinePosition(), (int) kerning ); // Log.debug ("Adding Glyph"); // does this finish a word? Check it! if ( isWordBreak( breakweight ) ) { final boolean forceLinebreak = breakweight == BreakOpportunityProducer.BREAK_LINE; addWord( forceLinebreak ); } } private ExtendedBaselineInfo getBaselineInfo( final int character ) { if ( uniformBaselineInfo != null ) { return uniformBaselineInfo; } final ExtendedBaselineInfo baselineInfo = metaData.getBaselineInfo( character, layoutContext ); if ( fontMetrics.isUniformFontMetrics() ) { uniformBaselineInfo = baselineInfo; } return baselineInfo; } protected void addWord( final boolean forceLinebreak ) { if ( glyphList.isEmpty() ) { // This is a forced linebreak, caused by a \n somewhere at the beginning of the text or after a whitespace. // If there is a preservable whitespace, the leading margin will be non-zero. if ( leadingMargin > 0 ) { final SpacerRenderNode spacer = new SpacerRenderNode( RenderableText.convert( leadingMargin ), 0, true, spaceCount ); words.add( spacer ); } if ( forceLinebreak ) { final ExtendedBaselineInfo info = getBaselineInfo( '\n' ); // / TextUtility.createBaselineInfo('\n', fontMetrics, baselineInfo); final RenderableText text = new RenderableText( layoutContext, elementType, instanceId, attributeMap, info, DefaultRenderableTextFactory.EMPTY_GLYPHS, 0, 0, lastLanguage, true ); words.add( text ); } leadingMargin = 0; spaceCount = 0; return; } // final DefaultGlyph[] glyphs = (DefaultGlyph[]) glyphList.toArray(new DefaultGlyph[glyphList.size()]); if ( leadingMargin > 0 ) { final SpacerRenderNode spacer = new SpacerRenderNode( RenderableText.convert( leadingMargin ), 0, true, spaceCount ); words.add( spacer ); } // Compute a suitable text-metrics object for this text. We simply assume that the first character is representive // for all characters of the text chunk. This may be a wrong assumption in complex-text environments but will work // for now. final int codePoint = glyphList.getGlyph( 0 ).getCodepoint(); final ExtendedBaselineInfo baselineInfo = getBaselineInfo( codePoint ); // final ExtendedBaselineInfo baselineInfo = TextUtility.createBaselineInfo(codePoint, fontMetrics, this // .baselineInfo); final RenderableText text = new RenderableText( layoutContext, elementType, instanceId, attributeMap, baselineInfo, glyphList.lock(), 0, glyphList.getSize(), lastLanguage, forceLinebreak ); words.add( text ); glyphList.clear(); leadingMargin = 0; spaceCount = 0; } private boolean isWordBreak( final int breakOp ) { if ( BreakOpportunityProducer.BREAK_WORD == breakOp || BreakOpportunityProducer.BREAK_LINE == breakOp ) { return true; } return false; } protected WhiteSpaceFilter createWhitespaceFilter( final StyleSheet layoutContext ) { final WhitespaceCollapse wsColl = (WhitespaceCollapse) layoutContext.getStyleProperty( TextStyleKeys.WHITE_SPACE_COLLAPSE ); if ( whitespaceFilter != null ) { if ( ObjectUtilities.equal( whitespaceFilterValue, wsColl ) ) { whitespaceFilter.reset(); return whitespaceFilter; } } whitespaceFilterValue = wsColl; if ( WhitespaceCollapse.DISCARD.equals( wsColl ) ) { return new DiscardWhiteSpaceFilter(); } if ( WhitespaceCollapse.PRESERVE.equals( wsColl ) ) { return new PreserveWhiteSpaceFilter(); } if ( WhitespaceCollapse.PRESERVE_BREAKS.equals( wsColl ) ) { return new PreserveBreaksWhiteSpaceFilter(); } return new CollapseWhiteSpaceFilter(); } protected GlyphClassificationProducer createGlyphClassifier( final StyleSheet layoutContext ) { final WhitespaceCollapse wsColl = (WhitespaceCollapse) layoutContext.getStyleProperty( TextStyleKeys.WHITE_SPACE_COLLAPSE ); if ( classificationProducer != null ) { if ( ObjectUtilities.equal( whitespaceCollapseValue, wsColl ) ) { classificationProducer.reset(); return classificationProducer; } } whitespaceCollapseValue = wsColl; // if (WhitespaceCollapse.PRESERVE_BREAKS.equals(wsColl)) // { // return new LinebreakClassificationProducer(); // } classificationProducer = new WhitespaceClassificationProducer(); return classificationProducer; } protected BreakOpportunityProducer createBreakProducer( final StyleSheet layoutContext ) { final TextWrap wordBreak = (TextWrap) layoutContext.getStyleProperty( TextStyleKeys.TEXT_WRAP ); if ( breakOpportunityProducer != null ) { if ( ObjectUtilities.equal( breakOpportunityValue, wordBreak ) ) { breakOpportunityProducer.reset(); return breakOpportunityProducer; } } breakOpportunityValue = wordBreak; if ( TextWrap.NONE.equals( wordBreak ) ) { // suppress all but the linebreaks. This equals the 'pre' mode of HTML breakOpportunityProducer = new LineBreakProducer(); } else { // allow other breaks as well. The wordbreak producer does not perform // advanced break-detection (like syllable based breaks). breakOpportunityProducer = new WordBreakProducer(); } return breakOpportunityProducer; } protected SpacingProducer createSpacingProducer( final StyleSheet layoutContext ) { final Spacing spacing; if ( metaData.isFeatureSupported( OutputProcessorFeature.SPACING_SUPPORTED ) ) { final double minValue = layoutContext.getDoubleStyleProperty( TextStyleKeys.X_MIN_LETTER_SPACING, 0 ); final double optValue = layoutContext.getDoubleStyleProperty( TextStyleKeys.X_OPTIMUM_LETTER_SPACING, 0 ); final double maxValue = layoutContext.getDoubleStyleProperty( TextStyleKeys.X_MAX_LETTER_SPACING, 0 ); final int minIntVal = (int) StrictGeomUtility.toInternalValue( minValue ); final int optIntVal = (int) StrictGeomUtility.toInternalValue( optValue ); final int maxIntVal = (int) StrictGeomUtility.toInternalValue( maxValue ); spacing = new Spacing( minIntVal, optIntVal, maxIntVal ); return new StaticSpacingProducer( spacing ); } spacing = ( Spacing.EMPTY_SPACING ); if ( spacingProducer != null && ObjectUtilities.equal( spacing, spacingProducerKey ) ) { return spacingProducer; } spacingProducer = new StaticSpacingProducer( spacing ); spacingProducerKey = spacing; return spacingProducer; } protected FontSizeProducer createFontSizeProducer( final StyleSheet layoutContext ) { return new VariableFontSizeProducer( fontMetrics ); } protected KerningProducer createKerningProducer( final StyleSheet layoutContext ) { // for now, do nothing .. return noKerningProducer; } public RenderNode[] finishText() { if ( layoutContext == null ) { return DefaultRenderableTextFactory.EMPTY_TEXT; } final RenderNode[] text = processText( DefaultRenderableTextFactory.END_OF_TEXT, 0, 1 ); layoutContext = null; fontSizeProducer = null; this.uniformBaselineInfo = null; return text; } public void startText() { startText = true; } }