001: /**
002: * ===========================================
003: * JFreeReport : a free Java reporting library
004: * ===========================================
005: *
006: * Project Info: http://reporting.pentaho.org/
007: *
008: * (C) Copyright 2001-2007, by Object Refinery Ltd, Pentaho Corporation and Contributors.
009: *
010: * This library is free software; you can redistribute it and/or modify it under the terms
011: * of the GNU Lesser General Public License as published by the Free Software Foundation;
012: * either version 2.1 of the License, or (at your option) any later version.
013: *
014: * This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
015: * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
016: * See the GNU Lesser General Public License for more details.
017: *
018: * You should have received a copy of the GNU Lesser General Public License along with this
019: * library; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330,
020: * Boston, MA 02111-1307, USA.
021: *
022: * [Java is a trademark or registered trademark of Sun Microsystems, Inc.
023: * in the United States and other countries.]
024: *
025: * ------------
026: * DefaultRenderableTextFactory.java
027: * ------------
028: * (C) Copyright 2001-2007, by Object Refinery Ltd, Pentaho Corporation and Contributors.
029: */package org.jfree.report.layout.text;
030:
031: import java.util.ArrayList;
032:
033: import org.jfree.fonts.registry.BaselineInfo;
034: import org.jfree.fonts.registry.FontMetrics;
035: import org.jfree.fonts.text.ClassificationProducer;
036: import org.jfree.fonts.text.DefaultLanguageClassifier;
037: import org.jfree.fonts.text.GraphemeClusterProducer;
038: import org.jfree.fonts.text.LanguageClassifier;
039: import org.jfree.fonts.text.Spacing;
040: import org.jfree.fonts.text.SpacingProducer;
041: import org.jfree.fonts.text.StaticSpacingProducer;
042: import org.jfree.fonts.text.breaks.BreakOpportunityProducer;
043: import org.jfree.fonts.text.breaks.LineBreakProducer;
044: import org.jfree.fonts.text.breaks.WordBreakProducer;
045: import org.jfree.fonts.text.classifier.GlyphClassificationProducer;
046: import org.jfree.fonts.text.classifier.WhitespaceClassificationProducer;
047: import org.jfree.fonts.text.font.FontSizeProducer;
048: import org.jfree.fonts.text.font.GlyphMetrics;
049: import org.jfree.fonts.text.font.KerningProducer;
050: import org.jfree.fonts.text.font.NoKerningProducer;
051: import org.jfree.fonts.text.font.VariableFontSizeProducer;
052: import org.jfree.fonts.text.whitespace.CollapseWhiteSpaceFilter;
053: import org.jfree.fonts.text.whitespace.DiscardWhiteSpaceFilter;
054: import org.jfree.fonts.text.whitespace.PreserveBreaksWhiteSpaceFilter;
055: import org.jfree.fonts.text.whitespace.PreserveWhiteSpaceFilter;
056: import org.jfree.fonts.text.whitespace.WhiteSpaceFilter;
057: import org.jfree.report.layout.model.RenderNode;
058: import org.jfree.report.layout.model.RenderableText;
059: import org.jfree.report.layout.model.SpacerRenderNode;
060: import org.jfree.report.layout.output.OutputProcessorFeature;
061: import org.jfree.report.layout.output.OutputProcessorMetaData;
062: import org.jfree.report.style.StyleSheet;
063: import org.jfree.report.style.TextStyleKeys;
064: import org.jfree.report.style.TextWrap;
065: import org.jfree.report.style.WhitespaceCollapse;
066: import org.jfree.report.util.geom.StrictGeomUtility;
067: import org.jfree.util.ObjectUtilities;
068:
069: /**
070: * Creation-Date: 03.04.2007, 16:43:48
071: *
072: * @author Thomas Morgner
073: */
074: public class DefaultRenderableTextFactory implements
075: RenderableTextFactory {
076: private static final int[] EMPTY_EXTRA_CHARS = new int[0];
077: private static final RenderNode[] EMPTY_RENDER_NODE = new RenderNode[0];
078: private static final RenderableText[] EMPTY_TEXT = new RenderableText[0];
079: private static final Glyph[] EMPTY_GLYPHS = new Glyph[0];
080: private static final int[] END_OF_TEXT = new int[] { ClassificationProducer.END_OF_TEXT };
081:
082: private GraphemeClusterProducer clusterProducer;
083: private boolean startText;
084: private boolean produced;
085: private FontSizeProducer fontSizeProducer;
086: private KerningProducer kerningProducer;
087: private SpacingProducer spacingProducer;
088: private BreakOpportunityProducer breakOpportunityProducer;
089: private WhiteSpaceFilter whitespaceFilter;
090: private GlyphClassificationProducer classificationProducer;
091: private StyleSheet layoutContext;
092: private LanguageClassifier languageClassifier;
093:
094: private transient GlyphMetrics dims;
095:
096: private ArrayList words;
097: private ArrayList glyphList;
098: private long leadingMargin;
099: private int spaceCount;
100: private int lastLanguage;
101:
102: // todo: This is part of a cheap hack.
103: private transient FontMetrics fontMetrics;
104: private OutputProcessorMetaData metaData;
105: private BaselineInfo baselineInfo;
106:
107: // cached instance ..
108: private NoKerningProducer noKerningProducer;
109:
110: private WhitespaceCollapse whitespaceFilterValue;
111: private WhitespaceCollapse whitespaceCollapseValue;
112: private TextWrap breakOpportunityValue;
113: private long wordSpacing;
114:
115: public DefaultRenderableTextFactory(
116: final OutputProcessorMetaData metaData) {
117: this .metaData = metaData;
118: this .clusterProducer = new GraphemeClusterProducer();
119: this .languageClassifier = new DefaultLanguageClassifier();
120: this .startText = true;
121: this .words = new ArrayList();
122: this .glyphList = new ArrayList();
123: this .dims = new GlyphMetrics();
124: this .baselineInfo = new BaselineInfo();
125: this .noKerningProducer = new NoKerningProducer();
126: }
127:
128: public RenderNode[] createText(final int[] text, final int offset,
129: final int length, final StyleSheet layoutContext) {
130: if (layoutContext == null) {
131: throw new NullPointerException();
132: }
133:
134: fontMetrics = metaData.getFontMetrics(layoutContext);
135:
136: kerningProducer = createKerningProducer(layoutContext);
137: fontSizeProducer = createFontSizeProducer(layoutContext);
138: spacingProducer = createSpacingProducer(layoutContext);
139: breakOpportunityProducer = createBreakProducer(layoutContext);
140: whitespaceFilter = createWhitespaceFilter(layoutContext);
141: classificationProducer = createGlyphClassifier(layoutContext);
142: this .layoutContext = layoutContext;
143:
144: if (metaData
145: .isFeatureSupported(OutputProcessorFeature.SPACING_SUPPORTED)) {
146: this .wordSpacing = StrictGeomUtility
147: .toInternalValue(layoutContext
148: .getDoubleStyleProperty(
149: TextStyleKeys.WORD_SPACING, 0));
150: } else {
151: this .wordSpacing = 0;
152: }
153:
154: if (startText) {
155: whitespaceFilter
156: .filter(ClassificationProducer.START_OF_TEXT);
157: breakOpportunityProducer
158: .createBreakOpportunity(ClassificationProducer.START_OF_TEXT);
159: kerningProducer
160: .getKerning(ClassificationProducer.START_OF_TEXT);
161: startText = false;
162: produced = false;
163: }
164:
165: return processText(text, offset, length);
166: }
167:
168: protected RenderNode[] processText(final int[] text,
169: final int offset, final int length) {
170: int clusterStartIdx = -1;
171: final int maxLen = Math.min(length + offset, text.length);
172: for (int i = offset; i < maxLen; i++) {
173: final int codePoint = text[i];
174: final boolean clusterStarted = this .clusterProducer
175: .createGraphemeCluster(codePoint);
176: // ignore the first cluster start; we need to see the whole cluster.
177: if (clusterStarted) {
178: if (i > offset) {
179: final int extraCharLength = i - clusterStartIdx - 1;
180: if (extraCharLength > 0) {
181: final int[] extraChars = new int[extraCharLength];
182: System.arraycopy(text, clusterStartIdx + 1,
183: extraChars, 0, extraChars.length);
184: addGlyph(text[clusterStartIdx], extraChars);
185: } else {
186: addGlyph(text[clusterStartIdx],
187: EMPTY_EXTRA_CHARS);
188: }
189: }
190:
191: clusterStartIdx = i;
192: }
193: }
194:
195: // Process the last cluster ...
196: if (clusterStartIdx >= offset) {
197: final int extraCharLength = maxLen - clusterStartIdx - 1;
198: if (extraCharLength > 0) {
199: final int[] extraChars = new int[extraCharLength];
200: System.arraycopy(text, clusterStartIdx + 1, extraChars,
201: 0, extraChars.length);
202: addGlyph(text[clusterStartIdx], extraChars);
203: } else {
204: addGlyph(text[clusterStartIdx], EMPTY_EXTRA_CHARS);
205: }
206: }
207:
208: if (words.isEmpty() == false) {
209: final RenderNode[] renderableTexts = (RenderNode[]) words
210: .toArray(new RenderNode[words.size()]);
211: words.clear();
212: produced = true;
213: return renderableTexts;
214: } else {
215: // we did not produce any text.
216: return EMPTY_RENDER_NODE;
217: }
218: }
219:
220: protected void addGlyph(final int rawCodePoint,
221: final int[] extraChars) {
222: // Log.debug ("Processing " + rawCodePoint);
223:
224: if (rawCodePoint == ClassificationProducer.END_OF_TEXT) {
225: whitespaceFilter.filter(rawCodePoint);
226: classificationProducer.getClassification(rawCodePoint);
227: kerningProducer.getKerning(rawCodePoint);
228: breakOpportunityProducer
229: .createBreakOpportunity(rawCodePoint);
230: spacingProducer.createSpacing(rawCodePoint);
231: fontSizeProducer.getCharacterSize(rawCodePoint, dims);
232:
233: if (leadingMargin > 0 || glyphList.isEmpty() == false) {
234: addWord(false);
235: } else {
236: // finish up ..
237: glyphList.clear();
238: leadingMargin = 0;
239: spaceCount = 0;
240: }
241: return;
242: }
243:
244: int codePoint = whitespaceFilter.filter(rawCodePoint);
245: final boolean stripWhitespaces;
246:
247: // No matter whether we will ignore the result, we have to keep our
248: // factories up and running. These beasts need to see all data, no
249: // matter what get printed later.
250: final int extraCharCount = extraChars.length;
251: if (codePoint == WhiteSpaceFilter.STRIP_WHITESPACE) {
252: // if we dont have extra-chars, ignore the thing ..
253: if (extraCharCount == 0) {
254: stripWhitespaces = true;
255: } else {
256: // convert it into a space. This might be invalid, but will work for now.
257: codePoint = ' ';
258: stripWhitespaces = false;
259: }
260: } else {
261: stripWhitespaces = false;
262: }
263:
264: int glyphClassification = classificationProducer
265: .getClassification(codePoint);
266: final long kerning = kerningProducer.getKerning(codePoint);
267: int breakweight = breakOpportunityProducer
268: .createBreakOpportunity(codePoint);
269: final Spacing spacing = spacingProducer
270: .createSpacing(codePoint);
271: dims = fontSizeProducer.getCharacterSize(codePoint, dims);
272: int width = dims.getWidth();
273: int height = dims.getHeight();
274: lastLanguage = languageClassifier.getScript(codePoint);
275:
276: for (int i = 0; i < extraCharCount; i++) {
277: final int extraChar = extraChars[i];
278: dims = fontSizeProducer.getCharacterSize(extraChar, dims);
279: width = Math.max(width, (dims.getWidth() & 0x7FFFFFFF));
280: height = Math.max(height, (dims.getHeight() & 0x7FFFFFFF));
281: breakweight = breakOpportunityProducer
282: .createBreakOpportunity(extraChar);
283: glyphClassification = classificationProducer
284: .getClassification(extraChar);
285: }
286:
287: if (stripWhitespaces) {
288: // Log.debug ("Stripping whitespaces");
289: return;
290: }
291:
292: if (Glyph.SPACE_CHAR == glyphClassification
293: && isWordBreak(breakweight)) {
294:
295: // Finish the current word ...
296: final boolean forceLinebreak = breakweight == BreakOpportunityProducer.BREAK_LINE;
297: if (glyphList.isEmpty() == false || forceLinebreak) {
298: addWord(forceLinebreak);
299: }
300:
301: // This character can be stripped. We increase the leading margin of the
302: // next word by the character's width.
303: leadingMargin += width + wordSpacing;
304: spaceCount += 1;
305: // Log.debug ("Increasing Margin");
306: return;
307: }
308:
309: final Glyph glyph = new Glyph(codePoint, breakweight,
310: glyphClassification, spacing, width, height, dims
311: .getBaselinePosition(), (int) kerning,
312: extraChars);
313: glyphList.add(glyph);
314: // Log.debug ("Adding Glyph");
315:
316: // does this finish a word? Check it!
317: if (isWordBreak(breakweight)) {
318: final boolean forceLinebreak = breakweight == BreakOpportunityProducer.BREAK_LINE;
319: addWord(forceLinebreak);
320: }
321: }
322:
323: protected void addWord(final boolean forceLinebreak) {
324: if (glyphList.isEmpty()) {
325: // This is a forced linebreak, caused by a \n somewhere at the beginning of the text or after a whitespace.
326: // If there is a preservable whitespace, the leading margin will be non-zero.
327: if (leadingMargin > 0) {
328: final SpacerRenderNode spacer = new SpacerRenderNode(
329: leadingMargin, 0, true, spaceCount);
330: words.add(spacer);
331: }
332: if (forceLinebreak) {
333: final ExtendedBaselineInfo info = TextUtility
334: .createBaselineInfo('\n', fontMetrics,
335: baselineInfo);
336: final RenderableText text = new RenderableText(
337: layoutContext, info, EMPTY_GLYPHS, 0, 0,
338: lastLanguage, true);
339: words.add(text);
340: }
341: leadingMargin = 0;
342: spaceCount = 0;
343: return;
344: }
345:
346: final Glyph[] glyphs = (Glyph[]) glyphList
347: .toArray(new Glyph[glyphList.size()]);
348: if (leadingMargin > 0)// && words.isEmpty() == false)
349: {
350: final SpacerRenderNode spacer = new SpacerRenderNode(
351: leadingMargin, 0, true, spaceCount);
352: words.add(spacer);
353: }
354:
355: // Compute a suitable text-metrics object for this text. We simply assume that the first character is representive
356: // for all characters of the text chunk. This may be a wrong assumption in complex-text environments but will work
357: // for now.
358: final int codePoint = glyphs[0].getCodepoint();
359:
360: final ExtendedBaselineInfo baselineInfo = TextUtility
361: .createBaselineInfo(codePoint, fontMetrics,
362: this .baselineInfo);
363: final RenderableText text = new RenderableText(layoutContext,
364: baselineInfo, glyphs, 0, glyphs.length, lastLanguage,
365: forceLinebreak);
366: words.add(text);
367: glyphList.clear();
368: leadingMargin = 0;
369: spaceCount = 0;
370: }
371:
372: private boolean isWordBreak(final int breakOp) {
373: if (BreakOpportunityProducer.BREAK_WORD == breakOp
374: || BreakOpportunityProducer.BREAK_LINE == breakOp) {
375: return true;
376: }
377: return false;
378: }
379:
380: protected WhiteSpaceFilter createWhitespaceFilter(
381: final StyleSheet layoutContext) {
382: final WhitespaceCollapse wsColl = (WhitespaceCollapse) layoutContext
383: .getStyleProperty(TextStyleKeys.WHITE_SPACE_COLLAPSE);
384:
385: if (whitespaceFilter != null) {
386: if (ObjectUtilities.equal(whitespaceFilterValue, wsColl)) {
387: whitespaceFilter.reset();
388: return whitespaceFilter;
389: }
390: }
391:
392: whitespaceFilterValue = wsColl;
393:
394: if (WhitespaceCollapse.DISCARD.equals(wsColl)) {
395: return new DiscardWhiteSpaceFilter();
396: }
397: if (WhitespaceCollapse.PRESERVE.equals(wsColl)) {
398: return new PreserveWhiteSpaceFilter();
399: }
400: if (WhitespaceCollapse.PRESERVE_BREAKS.equals(wsColl)) {
401: return new PreserveBreaksWhiteSpaceFilter();
402: }
403: return new CollapseWhiteSpaceFilter();
404: }
405:
406: protected GlyphClassificationProducer createGlyphClassifier(
407: final StyleSheet layoutContext) {
408: final WhitespaceCollapse wsColl = (WhitespaceCollapse) layoutContext
409: .getStyleProperty(TextStyleKeys.WHITE_SPACE_COLLAPSE);
410: if (classificationProducer != null) {
411: if (ObjectUtilities.equal(whitespaceCollapseValue, wsColl)) {
412: classificationProducer.reset();
413: return classificationProducer;
414: }
415: }
416:
417: whitespaceCollapseValue = wsColl;
418:
419: // if (WhitespaceCollapse.PRESERVE_BREAKS.equals(wsColl))
420: // {
421: // return new LinebreakClassificationProducer();
422: // }
423: classificationProducer = new WhitespaceClassificationProducer();
424: return classificationProducer;
425: }
426:
427: protected BreakOpportunityProducer createBreakProducer(
428: final StyleSheet layoutContext) {
429: final TextWrap wordBreak = (TextWrap) layoutContext
430: .getStyleProperty(TextStyleKeys.TEXT_WRAP);
431: if (breakOpportunityProducer != null) {
432: if (ObjectUtilities.equal(breakOpportunityValue, wordBreak)) {
433: breakOpportunityProducer.reset();
434: return breakOpportunityProducer;
435: }
436: }
437:
438: breakOpportunityValue = wordBreak;
439:
440: if (TextWrap.NONE.equals(wordBreak)) {
441: // surpress all but the linebreaks. This equals the 'pre' mode of HTML
442: breakOpportunityProducer = new LineBreakProducer();
443: } else {
444: // allow other breaks as well. The wordbreak producer does not perform
445: // advanced break-detection (like syllable based breaks).
446: breakOpportunityProducer = new WordBreakProducer();
447: }
448: return breakOpportunityProducer;
449: }
450:
451: protected SpacingProducer createSpacingProducer(
452: final StyleSheet layoutContext) {
453: if (metaData
454: .isFeatureSupported(OutputProcessorFeature.SPACING_SUPPORTED)) {
455: final double minValue = layoutContext
456: .getDoubleStyleProperty(
457: TextStyleKeys.X_MIN_LETTER_SPACING, 0);
458: final double optValue = layoutContext
459: .getDoubleStyleProperty(
460: TextStyleKeys.X_OPTIMUM_LETTER_SPACING, 0);
461: final double maxValue = layoutContext
462: .getDoubleStyleProperty(
463: TextStyleKeys.X_MAX_LETTER_SPACING, 0);
464:
465: final int minIntVal = (int) StrictGeomUtility
466: .toInternalValue(minValue);
467: final int optIntVal = (int) StrictGeomUtility
468: .toInternalValue(optValue);
469: final int maxIntVal = (int) StrictGeomUtility
470: .toInternalValue(maxValue);
471:
472: final Spacing spacing = new Spacing(minIntVal, optIntVal,
473: maxIntVal);
474: return new StaticSpacingProducer(spacing);
475: }
476: return new StaticSpacingProducer(Spacing.EMPTY_SPACING);
477: }
478:
479: protected FontSizeProducer createFontSizeProducer(
480: final StyleSheet layoutContext) {
481: return new VariableFontSizeProducer(fontMetrics);
482: }
483:
484: protected KerningProducer createKerningProducer(
485: final StyleSheet layoutContext) {
486: // for now, do nothing ..
487: return noKerningProducer;
488: }
489:
490: public RenderNode[] finishText() {
491: if (layoutContext == null) {
492: return EMPTY_TEXT;
493: }
494:
495: final RenderNode[] text = processText(END_OF_TEXT, 0, 1);
496: layoutContext = null;
497: fontSizeProducer = null;
498: spacingProducer = null;
499:
500: return text;
501: }
502:
503: public void startText() {
504: startText = true;
505: }
506: }
|