001: /*
002: * $Id: TextLayout.java,v 1.5 2002/07/15 02:15:03 skavish Exp $
003: *
004: * ===========================================================================
005: *
006: * The JGenerator Software License, Version 1.0
007: *
008: * Copyright (c) 2000 Dmitry Skavish (skavish@usa.net). All rights reserved.
009: *
010: * Redistribution and use in source and binary forms, with or without
011: * modification, are permitted provided that the following conditions are met:
012: *
013: * 1. Redistributions of source code must retain the above copyright
014: * notice, this list of conditions and the following disclaimer.
015: *
016: * 2. Redistributions in binary form must reproduce the above copyright
017: * notice, this list of conditions and the following disclaimer in
018: * the documentation and/or other materials provided with the
019: * distribution.
020: *
021: * 3. The end-user documentation included with the redistribution, if
022: * any, must include the following acknowlegement:
023: * "This product includes software developed by Dmitry Skavish
024: * (skavish@usa.net, http://www.flashgap.com/)."
025: * Alternately, this acknowlegement may appear in the software itself,
026: * if and wherever such third-party acknowlegements normally appear.
027: *
028: * 4. The name "The JGenerator" must not be used to endorse or promote
029: * products derived from this software without prior written permission.
030: * For written permission, please contact skavish@usa.net.
031: *
032: * 5. Products derived from this software may not be called "The JGenerator"
033: * nor may "The JGenerator" appear in their names without prior written
034: * permission of Dmitry Skavish.
035: *
036: * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
037: * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
038: * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
039: * DISCLAIMED. IN NO EVENT SHALL DMITRY SKAVISH OR THE OTHER
040: * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
041: * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
042: * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
043: * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
044: * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
045: * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
046: * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
047: * SUCH DAMAGE.
048: *
049: */
050:
051: package org.openlaszlo.iv.flash.util;
052:
053: import java.io.*;
054: import java.awt.geom.*;
055:
056: import org.openlaszlo.iv.flash.api.*;
057: import org.openlaszlo.iv.flash.api.text.*;
058:
059: /**
060: * This class does simple text layout in a box.
061: * <P>
062: *
063: * @author Dmitry Skavish
064: * @see org.openlaszlo.iv.flash.api.text.Text
065: * @see org.openlaszlo.iv.flash.api.text.Font
066: * @see org.openlaszlo.iv.flash.api.text.FontDef
067: * @see org.openlaszlo.iv.flash.api.text.TextRecord
068: * @see org.openlaszlo.iv.flash.api.text.TextStyleChangeRecord
069: */
070: public final class TextLayout {
071:
072: private static final int MAX_CHARS_IN_LINE = 250;
073:
074: /**
075: * One line of text.
076: * <p>
077: * Contains several style runs (text of different font and style)
078: */
079: public final class TextLine extends FlashItem {
080:
081: /**
082: * Vector of TextRecords and TextStyleChangeRecords
083: * <P>
084: * TextStyleChangeRecords are at even positions,
085: * TextRecords are at odd positions.
086: */
087: public IVVector records = new IVVector(4);
088:
089: private TextItem lastItem;
090: private TextRecord lastRecord;
091:
092: public TextLine() {
093: }
094:
095: /**
096: * Creates new text record.
097: */
098: public void newRecord() {
099: flushLastRecord();
100: lastItem = curItem;
101: lastRecord = new TextRecord(MAX_CHARS_IN_LINE);
102: }
103:
104: private void flushLastRecord() {
105: if (lastRecord != null && lastRecord.getSize() != 0) {
106: int size = records.size();
107: if (size % 2 == 0) { // make sure we add stylechange only after textrecord
108: records.addElement(getStyleChange(lastItem));
109: }
110: records.addElement(lastRecord);
111: lastRecord = null;
112: }
113: }
114:
115: /**
116: * Creates text style change record from specified text item
117: * and sets all the properties, like font, color and height as
118: * current properties.
119: *
120: * @param item item for which to create style change record
121: * @return new style change record
122: */
123: private TextStyleChangeRecord getStyleChange(TextItem item) {
124: // create text style change record
125: TextStyleChangeRecord ts = new TextStyleChangeRecord();
126:
127: if (item != null) {
128: ts.setFont(item.font);
129: ts.setHeight(item.height);
130: ts.setColor(item.color);
131: }
132:
133: return ts;
134: }
135:
136: /**
137: * Adds new character to last text record.
138: * <P>
139: * If last text record is overflowed then creates new text record
140: *
141: * @param ch character to be added
142: * @param index index of character in the font
143: * @param adv advance value of the character
144: */
145: public void add(char ch, int index, int adv) {
146: if (lastRecord.getSize() >= MAX_CHARS_IN_LINE) {
147: newRecord();
148: }
149: lastRecord.add(ch, index, adv);
150: }
151:
152: /**
153: * Trims this line from the end
154: * <P>
155: * Removes all spaces from the end
156: *
157: * @return width in twixels of all removed spaces
158: */
159: public int trimEnd() {
160: flushLastRecord();
161:
162: int w = 0;
163:
164: for (int i = records.size(); --i > 0;) {
165: TextRecord tr = (TextRecord) records.elementAt(i);
166: w += tr.trimEnd();
167: if (tr.getSize() > 0)
168: break;
169: records.removeElementAt(i); // remove text record
170: records.removeElementAt(--i); // remove stylechange record
171: }
172:
173: return w;
174: }
175:
176: /**
177: * Trims this line from the start
178: * <P>
179: * Removes all spaces from the start
180: *
181: * @return width in twixels of all removed spaces
182: */
183: public int trimStart() {
184: flushLastRecord();
185:
186: int w = 0;
187:
188: TextStyleChangeRecord lastts = null;
189:
190: for (; records.size() > 0;) {
191: TextStyleChangeRecord ts = (TextStyleChangeRecord) records
192: .elementAt(0);
193: if (lastts != null) {
194: lastts.mergeTo(ts);
195: }
196: lastts = ts;
197: TextRecord tr = (TextRecord) records.elementAt(1);
198: w += tr.trimStart();
199: if (tr.getSize() > 0)
200: break;
201: records.removeElementAt(1); // remove text record
202: records.removeElementAt(0); // remove stylechange record
203: }
204:
205: return w;
206: }
207:
208: /**
209: * Mark current position in current text record.
210: * <P>
211: * Used for rolling back if word does not fit in the text rectangle.
212: */
213: public void markPosition() {
214: markedPosition = lastRecord.getSize();
215: }
216:
217: /**
218: * Rollbacks current position to the marked one.
219: */
220: public void rollBack() {
221: lastRecord.setSize(markedPosition);
222: }
223:
224: /**
225: * Does end-of-line.
226: * <P>
227: * Flushes current text record
228: *
229: * @param x x position of this line
230: * @param y y position of this line
231: */
232: public void endLine(int x, int y) {
233: flushLastRecord();
234: if (records.size() > 0) {
235: TextStyleChangeRecord ts = (TextStyleChangeRecord) records
236: .elementAt(0);
237: ts.setX(x);
238: ts.setY(y);
239: }
240: }
241:
242: /**
243: * Returns maximum advance value from all the records.
244: *
245: * @return maximum advance value from all the records
246: */
247: public int getMaxAdvance() {
248: int max = Integer.MIN_VALUE;
249: // iterate over TextRecords only
250: for (int i = 1; i < records.size(); i += 2) {
251: TextRecord tr = (TextRecord) records.elementAt(i);
252: int adv = tr.getMaxAdvance();
253: if (adv > max)
254: max = adv;
255: }
256: if (max == Integer.MIN_VALUE)
257: return 0;
258: return max;
259: }
260:
261: /**
262: * Returns maximum character index from all the records.
263: *
264: * @return maximum character index from all the records
265: */
266: public int getMaxIndex() {
267: int max = Integer.MIN_VALUE;
268: // iterate over TextRecords only
269: for (int i = 1; i < records.size(); i += 2) {
270: TextRecord tr = (TextRecord) records.elementAt(i);
271: int idx = tr.getMaxIndex();
272: if (idx > max)
273: max = idx;
274: }
275: if (max == Integer.MIN_VALUE)
276: return 0;
277: return max;
278: }
279:
280: /**
281: * Returns width of this line.
282: *
283: * @return width if this line in twixels
284: */
285: public int getWidth() {
286: if (records.size() == 0)
287: return 0;
288: TextStyleChangeRecord ts = (TextStyleChangeRecord) records
289: .elementAt(0);
290: int width = ts.getX();
291: for (int i = 1; i < records.size(); i += 2) {
292: TextRecord tr = (TextRecord) records.elementAt(i);
293: width += tr.getWidth();
294: }
295: return width;
296: }
297:
298: /**
299: * Return number of characters in this line
300: *
301: * @return number of characters in this line
302: */
303: public int getSize() {
304: int size = 0;
305: // iterate over TextRecords only
306: for (int i = 1; i < records.size(); i += 2) {
307: TextRecord tr = (TextRecord) records.elementAt(i);
308: size += tr.getSize();
309: }
310: if (lastRecord != null)
311: size += lastRecord.getSize();
312: return size;
313: }
314:
315: public void printContent(PrintStream out, String indent) {
316: out.println(indent + "TextLine: size=" + records.size()
317: + " x=" + x + " y=" + y);
318: records.printContent(out, indent + " ");
319: }
320:
321: public void write(FlashOutput fob) {
322: records.write(fob);
323: }
324:
325: protected FlashItem copyInto(FlashItem item, ScriptCopier copier) {
326: ((TextLine) item).records = records.getCopy(copier);
327: // there is no need to copy lastItem and lastRecord (?)
328: return item;
329: }
330:
331: public FlashItem getCopy(ScriptCopier copier) {
332: return copyInto(new TextLine(), copier);
333: }
334: }
335:
336: private int markedPosition = 0; // used only by TextLine
337:
338: private TextItem curItem;
339: private TextLine curLine;
340: private boolean line_continued;
341: private int line_width;
342: private int line_window;
343: private int x;
344: private int y;
345: private int max_ascent;
346: private int max_descent;
347: private int max_linesp;
348:
349: private int rect_width;
350: private Text myText;
351: private Rectangle2D bounds;
352: private IVVector lines = new IVVector();
353:
354: private void reCalcHeights() {
355: Font font = curItem.font;
356: int height = curItem.height;
357: max_ascent = (font.ascent * height) / 1024;
358: max_linesp = curItem.linesp;
359: max_descent = (font.descent * height) / 1024 + max_linesp;
360: }
361:
362: private void endLine() {
363: if (curLine != null) {
364: y += max_ascent; // advance y to baseline
365: curLine.endLine(x, y);
366: y += max_descent; // advance y to next line
367: curLine = null;
368: }
369: }
370:
371: /**
372: *
373: * @param cont true if this new line is continued from previous, this affects
374: * alignment of this line
375: */
376: private void newLine(boolean cont) {
377: endLine();
378: reCalcHeights();
379: curLine = new TextLine();
380: line_continued = cont;
381: lines.addElement(curLine);
382: curLine.newRecord();
383: line_width = 0;
384: x = curItem.marginleft;
385: line_window = rect_width
386: - (curItem.marginleft + curItem.marginright);
387: }
388:
389: private void newParagraph() {
390: newLine(false);
391: line_window -= curItem.indent;
392: x += curItem.indent;
393: }
394:
395: /**
396: * Creates new text layout.
397: *
398: * @param myText text to be layed out
399: * @param bounds rectangle to be used for laying the text out in
400: */
401: public TextLayout(Text myText, Rectangle2D bounds) {
402: this .myText = myText;
403: this .bounds = bounds;
404: }
405:
406: /**
407: * Retrieves vector of text records layed out by this
408: * text layout for the specified font.
409: *
410: * @param font font of the records to be retrieved
411: * @return vector of {@link org.openlaszlo.iv.flash.api.text.TextRecord}s
412: */
413: public IVVector getTextRecords(Font font) {
414: IVVector trs = new IVVector();
415: Font lastFont = null;
416: for (int i = 0; i < lines.size(); i++) {
417: TextLine line = (TextLine) lines.elementAt(i);
418: for (int k = 0; k < line.records.size(); k++) {
419: Object o = line.records.elementAt(k);
420: if (o instanceof TextStyleChangeRecord) {
421: Font f = ((TextStyleChangeRecord) o).getFont();
422: if (f != null)
423: lastFont = f;
424: } else {
425: if (lastFont == font)
426: trs.addElement(o);
427: }
428: }
429: }
430: return trs;
431: }
432:
433: /**
434: * Retrieves vector of all text records
435: *
436: * @return vector of {@link org.openlaszlo.iv.flash.api.text.TextRecord}s and {@link org.openlaszlo.iv.flash.api.text.TextStyleChangeRecord}s
437: */
438: public IVVector getAllTextRecords() {
439: IVVector trs = new IVVector();
440: for (int i = 0; i < lines.size(); i++) {
441: TextLine line = (TextLine) lines.elementAt(i);
442: for (int k = 0; k < line.records.size(); k++) {
443: trs.addElement(line.records.elementAt(k));
444: }
445: }
446: return trs;
447: }
448:
449: /**
450: * Updates records' font.
451: * <P>
452: * Changes one specified font into another in all records.
453: * In text records also updates indexes.
454: *
455: * @param old_font old font
456: * @param new_font new font
457: */
458: public void changeFont(Font old_font, Font new_font) {
459: for (int i = 0; i < lines.size(); i++) {
460: TextLine line = (TextLine) lines.elementAt(i);
461: IVVector records = line.records;
462: FontDef.changeRecordsFont(records, old_font, new_font);
463: }
464: }
465:
466: /**
467: * Aligns current line
468: */
469: private void alignLine() {
470: switch (curItem.align) {
471: case 0: // left
472: if (curLine != null /*&& line_continued*/) {
473: line_width -= curLine.trimStart();
474: }
475: break;
476: case 1: // right
477: if (curLine != null) {
478: line_width -= curLine.trimEnd();
479: }
480: x += line_window - line_width;
481: break;
482: case 2: // center
483: if (curLine != null) {
484: line_width -= curLine.trimEnd() + curLine.trimStart();
485: }
486: x += (line_window - line_width) / 2;
487: break;
488: case 3: // justify
489: // ........
490: break;
491: }
492: }
493:
494: /**
495: * Removes whitespace at the end of the text
496: * <P>
497: * Text is specified by vector of TextItems
498: *
499: * @param items vector of TextItems
500: * @return
501: */
502: private void trimEnd(IVVector items) {
503: for (int i = items.size(); --i >= 0;) {
504: TextItem item = (TextItem) items.elementAt(i);
505: String t = item.text;
506: int j = t.length();
507: for (; --j >= 0;) {
508: char ch = t.charAt(j);
509: if (!Character.isWhitespace(ch))
510: break;
511: }
512: if (j >= 0) {
513: item.text = t.substring(0, j + 1);
514: return;
515: } else {
516: items.removeElementAt(i);
517: }
518: }
519: }
520:
521: /**
522: * Does text layout.
523: */
524: public void layout() {
525: IVVector items = myText.getTextItems();
526: trimEnd(items);
527: // System.out.println( "layout of '"+((TextItem)items.elementAt(0)).text+"'" );
528:
529: rect_width = (int) bounds.getWidth();
530:
531: y = 0;
532:
533: if (items.size() > 0) {
534: curItem = (TextItem) items.elementAt(0);
535:
536: newParagraph();
537:
538: int i = 0;
539: for (;;) {
540: Font font = curItem.font;
541: int height = curItem.height;
542: int ascent = (font.ascent * height) / 1024;
543: int descent = (font.descent * height) / 1024
544: + curItem.linesp;
545: if (ascent > max_ascent)
546: max_ascent = ascent;
547: if (descent > max_descent) {
548: max_descent = descent;
549: max_linesp = curItem.linesp;
550: }
551:
552: String text = curItem.text;
553: boolean isNowWord = false;
554: int word_width = 0;
555: int start_word = 0;
556:
557: int text_len = text.length();
558: for (int k = 0; k < text_len; k++) {
559: char ch = text.charAt(k);
560: boolean isWord = isWord(ch);
561: if (!isNowWord && isWord) { // start new word
562: curLine.markPosition();
563: word_width = 0;
564: start_word = k;
565: isNowWord = true;
566: } else {
567: isNowWord = isWord;
568: }
569: if (ch == '\r' || ch == '\n') {
570: alignLine();
571: if (k == text_len - 1)
572: endLine();
573: else {
574: char ch1 = text.charAt(k + 1);
575: if (ch != ch1
576: && (ch1 == '\r' || ch1 == '\n'))
577: k++;
578: if (k == text_len - 1)
579: endLine();
580: else
581: newParagraph();
582: }
583: } else {
584: int idx = font.getIndex(ch);
585: int ch_adv = font.getAdvanceValue(idx);
586: if (k != text_len - 1)
587: ch_adv += font.getKerning(ch, text
588: .charAt(k + 1));
589: int adv = (ch_adv * height) / 1024;
590: adv += curItem.kerning;
591: line_width += adv;
592: word_width += adv;
593: if (line_width <= line_window) {
594: curLine.add(ch, idx, adv);
595: } else {
596: if (curLine.getSize() == 0) { // even one character does not fit
597: // if even one character does not fit the window, then expand the window
598: line_window = line_width;
599: curLine.add(ch, idx, adv);
600: alignLine();
601: newLine(true);
602: isNowWord = false;
603: } else if (isNowWord) {
604: if (word_width > line_window) {
605: // split anyway, because the word does not fit the window at all
606: line_width -= adv;
607: k--;
608: alignLine();
609: newLine(true);
610: } else {
611: // rollback the word and send it to the next line
612: line_width -= word_width;
613: curLine.rollBack();
614: k = start_word - 1;
615: alignLine();
616: newLine(true);
617: isNowWord = false;
618: }
619: } else {
620: line_width -= adv;
621: k--;
622: alignLine();
623: newLine(true);
624: }
625: }
626: }
627: }
628:
629: i++;
630: if (i >= items.size())
631: break;
632: curItem = (TextItem) items.elementAt(i);
633: if (curLine == null)
634: newParagraph();
635: else
636: curLine.newRecord();
637: }
638: alignLine();
639: endLine();
640:
641: y -= max_linesp;
642: }
643:
644: // optimize text
645: optimize();
646:
647: // calculate bounds
648: int maxX = 0;
649: for (int l = 0; l < lines.size(); l++) {
650: TextLine line = (TextLine) lines.elementAt(l);
651: int max = line.getWidth();
652: if (max > maxX)
653: maxX = max;
654: }
655:
656: // check what kind of behavior is expected: MMGen or JGen and
657: // create appropriate bounds
658: int bs = myText.getBoundsStyle();
659: boolean isMMStyle = bs == Text.PROPERTY_CONTROLLED ? PropertyManager.textMMStyle
660: : bs == Text.MM_STYLE;
661: if (isMMStyle) {
662: // set bounds to be exactly equal to the text (MMGen style)
663: bounds
664: .setFrame(bounds.getMinX(), bounds.getMinY(), maxX,
665: y);
666: } else {
667: // set bounds to be equal whatever came from the template (JGen style)
668: double width = bounds.getWidth();
669: double height = bounds.getHeight();
670: if (maxX > width)
671: width = maxX;
672: if (y > height)
673: height = y;
674: bounds.setFrame(bounds.getMinX(), bounds.getMinY(), width,
675: height);
676: }
677:
678: myText.setBounds(bounds);
679: }
680:
681: protected int getNGlyphBits() {
682: int maxIdx = 0;
683: for (int l = 0; l < lines.size(); l++) {
684: TextLine line = (TextLine) lines.elementAt(l);
685: int max = line.getMaxIndex();
686: if (max > maxIdx)
687: maxIdx = max;
688: }
689:
690: return Util.getMinBitsU(maxIdx);
691: }
692:
693: protected int getNAdvanceBits() {
694: int maxAdv = 0;
695: for (int l = 0; l < lines.size(); l++) {
696: TextLine line = (TextLine) lines.elementAt(l);
697: int max = Math.abs(line.getMaxAdvance());
698: if (max > maxAdv)
699: maxAdv = max;
700: }
701:
702: return Util.getMinBitsS(maxAdv);
703: }
704:
705: /**
706: * Removes unneccesary data from stylechangerecords
707: */
708: public void optimize() {
709: Font lastFont = null;
710: int lastHeight = -1;
711: Color lastColor = null;
712:
713: for (int k = 0; k < lines.size(); k++) {
714: TextLine line = (TextLine) lines.elementAt(k);
715: IVVector records = line.records;
716: for (int i = 0; i < records.size(); i += 2) {
717: TextStyleChangeRecord ts = (TextStyleChangeRecord) records
718: .elementAt(i);
719: if (lastFont != null && lastFont == ts.getFont()) {
720: if (lastHeight == ts.getHeight()) {
721: ts.setFont(null);
722: } else {
723: lastHeight = ts.getHeight();
724: }
725: } else {
726: lastFont = ts.getFont();
727: lastHeight = ts.getHeight();
728: }
729: if (lastColor != null
730: && lastColor.equals(ts.getColor())) {
731: ts.setColor(null);
732: } else {
733: lastColor = ts.getColor();
734: }
735: }
736: }
737: }
738:
739: public void write(FlashOutput fob) {
740: int nGlyphBits = getNGlyphBits();
741: int nAdvanceBits = getNAdvanceBits();
742: fob.writeByte(nGlyphBits);
743: fob.writeByte(nAdvanceBits);
744: //System.out.println("Layout write: nGlyphBits="+nGlyphBits+", nAdvanceBits="+nAdvanceBits);
745: fob.setUserData(new int[] { nGlyphBits, nAdvanceBits });
746:
747: for (int i = 0; i < lines.size(); i++) {
748: TextLine line = (TextLine) lines.elementAt(i);
749: line.write(fob);
750: //line.printContent(System.out, "Layout Write: ");
751: }
752: fob.writeByte(0);
753: }
754:
755: public TextLayout getCopy(ScriptCopier copier) {
756: TextLayout tl = new TextLayout(myText, bounds);
757: tl.lines = lines.getCopy(copier);
758: return tl;
759: }
760:
761: private static boolean isWord(char ch) {
762: //Text Flow by FX
763: //Wrap words at whitespace and '-' only!
764:
765: if (ch == '-')
766: return false;
767:
768: return !Character.isWhitespace(ch);
769: }
770: }
|