/*
* This program is free software; you can redistribute it and/or modify it under the
* terms of the GNU Lesser General Public License, version 2.1 as published by the Free Software
* Foundation.
*
* You should have received a copy of the GNU Lesser General Public License along with this
* program; if not, you can obtain a copy at http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html
* or from the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the GNU Lesser General Public License for more details.
*
* Copyright (c) 2001 - 2009 Object Refinery Ltd, Pentaho Corporation and Contributors.. All rights reserved.
*/
package org.pentaho.reporting.engine.classic.core.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;
/**
* 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 words;
private GlyphList glyphList;
private long leadingMargin;
private int spaceCount;
private int lastLanguage;
// todo: This is part of a cheap hack.
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 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(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 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 = StrictGeomUtility.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)
{
int clusterStartIdx = -1;
final int maxLen = Math.min(length + offset, text.length);
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 = (RenderNode[]) 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);
final boolean stripWhitespaces;
// 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)
{
stripWhitespaces = true;
}
else
{
// convert it into a space. This might be invalid, but will work for now.
codePoint = DiscardWhiteSpaceFilter.ZERO_WIDTH;
stripWhitespaces = false;
}
}
else
{
stripWhitespaces = false;
}
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 (stripWhitespaces)
{
// Log.debug ("Stripping whitespaces");
return;
}
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(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)// && words.isEmpty() == false)
{
final SpacerRenderNode spacer = new SpacerRenderNode(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))
{
// surpress 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;
}
}