/* * 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.model; import org.pentaho.reporting.engine.classic.core.ReportAttributeMap; import org.pentaho.reporting.engine.classic.core.layout.model.context.NodeLayoutProperties; 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.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.util.InstanceID; import org.pentaho.reporting.engine.classic.core.util.geom.StrictGeomUtility; import org.pentaho.reporting.libraries.fonts.encoding.CodePointBuffer; import org.pentaho.reporting.libraries.fonts.text.Spacing; import org.pentaho.reporting.libraries.fonts.text.breaks.BreakOpportunityProducer; import org.pentaho.reporting.libraries.fonts.tools.FontStrictGeomUtility; /** * The renderable text is a text chunk, enriched with layouting information, such as break opportunities, character * sizes, kerning information and spacing information. * <p/> * Text is given as codepoints. Break opportunities are given as integer values, where zero forbids breaking, and higher * values denote better breaks. Spacing and glyph sizes and kerning is given in micro-points; Spacing is the 'added' * space between codepoints if text-justification is enabled. * <p/> * The text is computed as grapheme clusters; this means that several unicode codepoints may result in a single * /virtual/ glyph/codepoint/character. (Example: 'A' + accent symbols). If the font supports Lithurges, these lithurges * may also be represented as a single grapheme cluster (and thus behave unbreakable). * <p/> * Grapheme clusters with more than one unicode char have the size of that char added to the first codepoint, all * subsequence codepoints of the same cluster have a size/kerning/etc of zero and are unbreakable. * <p/> * This text chunk is perfectly suitable for horizontal text, going either from left-to-right or right-to-left. * (Breaking mixed text is up to the textfactory). * * @author Thomas Morgner */ public final class RenderableText extends RenderNode implements SplittableRenderNode { private static long conversionFactor; static { final long value = StrictGeomUtility.toInternalValue( 1 ); conversionFactor = value / FontStrictGeomUtility.toInternalValue( 1 ); } private GlyphList glyphs; private int offset; private int length; private int script; private long minimumWidth; private long preferredWidth; private boolean forceLinebreak; private ExtendedBaselineInfo baselineInfo; private boolean normalTextSpacing; public RenderableText( final StyleSheet layoutContext, final ElementType elementType, final InstanceID instanceID, final ReportAttributeMap<Object> attributes, final ExtendedBaselineInfo baselineInfo, final GlyphList glyphs, final int offset, final int length, final int script, final boolean forceLinebreak ) { super( new NodeLayoutProperties( layoutContext, attributes, instanceID, elementType ) ); initialize( glyphs, offset, length, baselineInfo, script, forceLinebreak ); } protected void initialize( final GlyphList glyphs, final int offset, final int length, final ExtendedBaselineInfo baselineInfo, final int script, final boolean forceLinebreak ) { if ( glyphs == null ) { throw new NullPointerException(); } if ( forceLinebreak == false && length == 0 ) { throw new IllegalArgumentException( "Do not create zero-length renderable text!" ); } if ( glyphs.getSize() < ( offset + length ) ) { throw new IllegalArgumentException(); } this.baselineInfo = baselineInfo; this.script = script; this.glyphs = glyphs; this.offset = offset; this.length = length; this.forceLinebreak = forceLinebreak; normalTextSpacing = true; long wordMinChunkWidth = 0; // long heightAbove = 0; // long heightBelow = 0; long minimumChunkWidth = 0; long realCharTotal = 0; long spacerMin = 0; long spacerMax = 0; long spacerOpt = 0; final int lastPos = Math.min( glyphs.getSize(), offset + length ); for ( int i = offset; i < lastPos; i++ ) { final Glyph glyph = glyphs.getGlyph( i ); // heightAbove = Math.max(glyph.getBaseLine(), heightAbove); // heightBelow = Math.max(glyph.getHeight() - glyph.getBaseLine(), heightBelow); final int kerning = glyph.getKerning(); final int width = glyph.getWidth(); final long realCharSpace = convert( width - kerning ); realCharTotal += realCharSpace; wordMinChunkWidth += realCharSpace; if ( i != ( lastPos - 1 ) ) { final Spacing spacing = glyph.getSpacing(); spacerMax += spacing.getMaximum(); spacerMin += spacing.getMinimum(); spacerOpt += spacing.getOptimum(); if ( normalTextSpacing == true && Spacing.EMPTY_SPACING.equals( spacing ) == false ) { normalTextSpacing = false; } wordMinChunkWidth += spacing.getMinimum(); } if ( glyph.getBreakWeight() > BreakOpportunityProducer.BREAK_CHAR ) { minimumChunkWidth = Math.max( minimumChunkWidth, wordMinChunkWidth ); wordMinChunkWidth = 0; // Paranoid sanity checks: The word- and linebreaks should have been // replaced by other definitions in the text factory. if ( glyph.getBreakWeight() == BreakOpportunityProducer.BREAK_LINE ) { throw new IllegalStateException( "A renderable text cannot and must " + "not contain linebreaks." ); } } } final long wordMinWidth = spacerMin + realCharTotal; final long wordPrefWidth = spacerOpt + realCharTotal; final long wordMaxWidth = spacerMax + realCharTotal; minimumChunkWidth = Math.max( minimumChunkWidth, wordMinChunkWidth ); minimumWidth = wordMinWidth; preferredWidth = wordPrefWidth; setMaximumBoxWidth( wordMaxWidth ); setMinimumChunkWidth( minimumChunkWidth ); } public int getNodeType() { return LayoutNodeTypes.TYPE_NODE_TEXT; } public boolean isNormalTextSpacing() { return normalTextSpacing; } public boolean isForceLinebreak() { return forceLinebreak; } public GlyphList getGlyphs() { return glyphs; } public int getOffset() { return offset; } public int getLength() { return length; } public String getRawText() { final GlyphList gs = getGlyphs(); return gs.getText( offset, length, new CodePointBuffer( length ) ); } public boolean isEmpty() { return length == 0 && forceLinebreak == false; } public boolean isDiscardable() { if ( forceLinebreak ) { return false; } return glyphs.getSize() == 0; } /** * Returns the baseline info for the given node. This can be null, if the node does not have any baseline info. * * @return */ public ExtendedBaselineInfo getBaselineInfo() { return baselineInfo; } public int getScript() { return script; } @Override public long getMinimumWidth() { return minimumWidth; } public long getPreferredWidth() { return preferredWidth; } public String toString() { return "RenderableText={glyphs=" + glyphs + "'}"; } public static long convert( final long fontMetricsValue ) { return fontMetricsValue * conversionFactor; } public int computeMaximumTextSize( final long contentX2 ) { final int length = getLength(); final long x = getX(); if ( contentX2 >= ( x + getWidth() ) ) { return length; } final GlyphList gs = getGlyphs(); long runningPos = x; final int offset = getOffset(); final int maxPos = offset + length; for ( int i = offset; i < maxPos; i++ ) { final Glyph g = gs.getGlyph( i ); runningPos += RenderableText.convert( g.getWidth() ); if ( i != offset ) { runningPos += g.getSpacing().getMinimum(); } if ( runningPos > contentX2 ) { return Math.max( 0, i - offset ); } } return length; } /** * {@inheritDoc} * <p/> * <b>Important!</b> The separation is allowed only if * {@linkplain org.pentaho.reporting.engine.classic.core.style .TextStyleKeys#WORDBREAK TextStyleKeys.WORDBREAK} * property is {@code true} * * @throws IllegalArgumentException * if {@code widthOfFirst <= 0} * @throws IllegalStateException * if {@code widthOfFirst >= getMinimumWidth()} */ @Override public RenderableText[] splitBy( long widthOfFirst ) { if ( widthOfFirst <= 0 ) { throw new IllegalArgumentException( String.format( "Illegal width: %d. Only text nodes with non-zero width are not allowed!", widthOfFirst ) ); } if ( widthOfFirst >= getMinimumWidth() ) { throw new IllegalStateException( String.format( "Split width (%d) should be less than the component's minimum width (%d)!", widthOfFirst, getMinimumWidth() ) ); } if ( !getStyleSheet().getBooleanStyleProperty( TextStyleKeys.WORDBREAK ) ) { // word-breaking should be allowed return null; } // length cannot be 0 - see guard check in initialize() final int last = offset + length; final GlyphList glyphs = getGlyphs(); int index = offset; long currentWidth = 0; while ( index < last && currentWidth <= widthOfFirst ) { currentWidth += convert( glyphs.getGlyph( index ).getWidth() ); index++; } index--; if ( index == offset ) { // the first element's width exceeds widthOfFirst return null; } RenderableText first = (RenderableText) derive( true ); int firstLength = index - offset; first.initialize( glyphs, offset, firstLength, baselineInfo, script, forceLinebreak ); RenderableText rest = (RenderableText) derive( true ); rest.initialize( glyphs, index, length - firstLength, baselineInfo, script, forceLinebreak ); RenderableText[] pair = { first, rest }; RenderBox parent = getParent(); if ( parent != null ) { parent.replaceChilds( this, pair ); } return pair; } }