001: /*
002: * $Id: MultiColumnText.java 2441 2006-10-27 17:24:01Z xlv $
003: * $Name$
004: *
005: * Copyright 2004 Steve Appling
006: *
007: * The contents of this file are subject to the Mozilla Public License Version 1.1
008: * (the "License"); you may not use this file except in compliance with the License.
009: * You may obtain a copy of the License at http://www.mozilla.org/MPL/
010: *
011: * Software distributed under the License is distributed on an "AS IS" basis,
012: * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
013: * for the specific language governing rights and limitations under the License.
014: *
015: * The Original Code is 'iText, a free JAVA-PDF library'.
016: *
017: * The Initial Developer of the Original Code is Bruno Lowagie. Portions created by
018: * the Initial Developer are Copyright (C) 1999-2005 by Bruno Lowagie.
019: * All Rights Reserved.
020: * Co-Developer of the code is Paulo Soares. Portions created by the Co-Developer
021: * are Copyright (C) 2000-2005 by Paulo Soares. All Rights Reserved.
022: *
023: * Contributor(s): all the names of the contributors are added in the source code
024: * where applicable.
025: *
026: * Alternatively, the contents of this file may be used under the terms of the
027: * LGPL license (the "GNU LIBRARY GENERAL PUBLIC LICENSE"), in which case the
028: * provisions of LGPL are applicable instead of those above. If you wish to
029: * allow use of your version of this file only under the terms of the LGPL
030: * License and not to allow others to use your version of this file under
031: * the MPL, indicate your decision by deleting the provisions above and
032: * replace them with the notice and other provisions required by the LGPL.
033: * If you do not delete the provisions above, a recipient may use your version
034: * of this file under either the MPL or the GNU LIBRARY GENERAL PUBLIC LICENSE.
035: *
036: * This library is free software; you can redistribute it and/or modify it
037: * under the terms of the MPL as stated above or under the terms of the GNU
038: * Library General Public License as published by the Free Software Foundation;
039: * either version 2 of the License, or any later version.
040: *
041: * This library is distributed in the hope that it will be useful, but WITHOUT
042: * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
043: * FOR A PARTICULAR PURPOSE. See the GNU Library general Public License for more
044: * details.
045: *
046: * If you didn't download this code from the following link, you should check if
047: * you aren't using an obsolete version:
048: * http://www.lowagie.com/iText/
049: */
050:
051: package com.lowagie.text.pdf;
052:
053: import java.util.ArrayList;
054:
055: import com.lowagie.text.Chunk;
056: import com.lowagie.text.DocumentException;
057: import com.lowagie.text.Element;
058: import com.lowagie.text.ElementListener;
059: import com.lowagie.text.Phrase;
060: import com.lowagie.text.Rectangle;
061:
062: /**
063: * Formats content into one or more columns bounded by a
064: * rectangle. The columns may be simple rectangles or
065: * more complicated shapes. Add all of the columns before
066: * adding content. Column continuation is supported. A MultiColumnText object may be added to
067: * a document using <CODE>Document.add</CODE>.
068: * @author Steve Appling
069: */
070: public class MultiColumnText implements Element {
071:
072: /** special constant for automatic calculation of height */
073: public static final float AUTOMATIC = -1f;
074:
075: /**
076: * total desiredHeight of columns. If <CODE>AUTOMATIC</CODE>, this means fill pages until done.
077: * This may be larger than one page
078: */
079: private float desiredHeight;
080:
081: /**
082: * total height of element written out so far
083: */
084: private float totalHeight;
085:
086: /**
087: * true if all the text could not be written out due to height restriction
088: */
089: private boolean overflow;
090:
091: /**
092: * Top of the columns - y position on starting page.
093: * If <CODE>AUTOMATIC</CODE>, it means current y position when added to document
094: */
095: private float top;
096:
097: /**
098: * used to store the y position of the bottom of the page
099: */
100: private float pageBottom;
101:
102: /**
103: * ColumnText object used to do all the real work. This same object is used for all columns
104: */
105: private ColumnText columnText;
106:
107: /**
108: * Array of <CODE>ColumnDef</CODE> objects used to define the columns
109: */
110: private ArrayList columnDefs;
111:
112: /**
113: * true if all columns are simple (rectangular)
114: */
115: private boolean simple = true;
116:
117: private int currentColumn = 0;
118:
119: private float nextY = AUTOMATIC;
120:
121: private boolean columnsRightToLeft = false;
122:
123: private PdfDocument document;
124:
125: /**
126: * Default constructor. Sets height to <CODE>AUTOMATIC</CODE>.
127: * Columns will repeat on each page as necessary to accomodate content length.
128: */
129: public MultiColumnText() {
130: this (AUTOMATIC);
131: }
132:
133: /**
134: * Construct a MultiColumnText container of the specified height.
135: * If height is <CODE>AUTOMATIC</CODE>, fill complete pages until done.
136: * If a specific height is used, it may span one or more pages.
137: *
138: * @param height
139: */
140: public MultiColumnText(float height) {
141: columnDefs = new ArrayList();
142: desiredHeight = height;
143: top = AUTOMATIC;
144: // canvas will be set later
145: columnText = new ColumnText(null);
146: totalHeight = 0f;
147: }
148:
149: /**
150: * Construct a MultiColumnText container of the specified height
151: * starting at the specified Y position.
152: *
153: * @param height
154: * @param top
155: */
156: public MultiColumnText(float top, float height) {
157: columnDefs = new ArrayList();
158: desiredHeight = height;
159: this .top = top;
160: nextY = top;
161: // canvas will be set later
162: columnText = new ColumnText(null);
163: totalHeight = 0f;
164: }
165:
166: /**
167: * Indicates that all of the text did not fit in the
168: * specified height. Note that isOverflow will return
169: * false before the MultiColumnText object has been
170: * added to the document. It will always be false if
171: * the height is AUTOMATIC.
172: *
173: * @return true if there is still space left in the column
174: */
175: public boolean isOverflow() {
176: return overflow;
177: }
178:
179: /**
180: * Copy the parameters from the specified ColumnText to use
181: * when rendering. Parameters like <CODE>setArabicOptions</CODE>
182: * must be set in this way.
183: *
184: * @param sourceColumn
185: */
186: public void useColumnParams(ColumnText sourceColumn) {
187: // note that canvas will be overwritten later
188: columnText.setSimpleVars(sourceColumn);
189: }
190:
191: /**
192: * Add a new column. The parameters are limits for each column
193: * wall in the format of a sequence of points (x1,y1,x2,y2,...).
194: *
195: * @param left limits for left column
196: * @param right limits for right column
197: */
198: public void addColumn(float[] left, float[] right) {
199: ColumnDef nextDef = new ColumnDef(left, right);
200: simple = nextDef.isSimple();
201: columnDefs.add(nextDef);
202: }
203:
204: /**
205: * Add a simple rectangular column with specified left
206: * and right x position boundaries.
207: *
208: * @param left left boundary
209: * @param right right boundary
210: */
211: public void addSimpleColumn(float left, float right) {
212: ColumnDef newCol = new ColumnDef(left, right);
213: columnDefs.add(newCol);
214: }
215:
216: /**
217: * Add the specified number of evenly spaced rectangular columns.
218: * Columns will be seperated by the specified gutterWidth.
219: *
220: * @param left left boundary of first column
221: * @param right right boundary of last column
222: * @param gutterWidth width of gutter spacing between columns
223: * @param numColumns number of columns to add
224: */
225: public void addRegularColumns(float left, float right,
226: float gutterWidth, int numColumns) {
227: float currX = left;
228: float width = right - left;
229: float colWidth = (width - (gutterWidth * (numColumns - 1)))
230: / numColumns;
231: for (int i = 0; i < numColumns; i++) {
232: addSimpleColumn(currX, currX + colWidth);
233: currX += colWidth + gutterWidth;
234: }
235: }
236:
237: /**
238: * Add an element to be rendered in a column.
239: * Note that you can only add a <CODE>Phrase</CODE>
240: * or a <CODE>Chunk</CODE> if the columns are
241: * not all simple. This is an underlying restriction in
242: * {@link com.lowagie.text.pdf.ColumnText}
243: *
244: * @param element element to add
245: * @throws DocumentException if element can't be added
246: */
247: public void addElement(Element element) throws DocumentException {
248: if (simple) {
249: columnText.addElement(element);
250: } else if (element instanceof Phrase) {
251: columnText.addText((Phrase) element);
252: } else if (element instanceof Chunk) {
253: columnText.addText((Chunk) element);
254: } else {
255: throw new DocumentException("Can't add "
256: + element.getClass()
257: + " to MultiColumnText with complex columns");
258: }
259: }
260:
261: /**
262: * Write out the columns. After writing, use
263: * {@link #isOverflow()} to see if all text was written.
264: * @param canvas PdfContentByte to write with
265: * @param document document to write to (only used to get page limit info)
266: * @param documentY starting y position to begin writing at
267: * @return the current height (y position) after writing the columns
268: * @throws DocumentException on error
269: */
270: public float write(PdfContentByte canvas, PdfDocument document,
271: float documentY) throws DocumentException {
272: this .document = document;
273: columnText.setCanvas(canvas);
274: if (columnDefs.isEmpty()) {
275: throw new DocumentException(
276: "MultiColumnText has no columns");
277: }
278: overflow = false;
279: pageBottom = document.bottom();
280: float currentHeight = 0;
281: boolean done = false;
282: try {
283: while (!done) {
284: if (nextY == AUTOMATIC) {
285: nextY = document.getVerticalPosition(true); // RS - 07/07/2005 - - Get current doc writing position for top of columns on new page.
286: }
287: if (top == AUTOMATIC) {
288: top = document.getVerticalPosition(true); // RS - 07/07/2005 - Get current doc writing position for top of columns on new page.
289: }
290:
291: ColumnDef currentDef = (ColumnDef) columnDefs
292: .get(getCurrentColumn());
293: columnText.setYLine(top);
294:
295: float[] left = currentDef
296: .resolvePositions(Rectangle.LEFT);
297: float[] right = currentDef
298: .resolvePositions(Rectangle.RIGHT);
299: if (document.isMarginMirroring()
300: && document.getPageNumber() % 2 == 0) {
301: float delta = document.rightMargin()
302: - document.left();
303: left = (float[]) left.clone();
304: right = (float[]) right.clone();
305: for (int i = 0; i < left.length; i += 2) {
306: left[i] -= delta;
307: }
308: for (int i = 0; i < right.length; i += 2) {
309: right[i] -= delta;
310: }
311: }
312:
313: currentHeight = Math.max(currentHeight, getHeight(left,
314: right));
315:
316: if (currentDef.isSimple()) {
317: columnText.setSimpleColumn(left[2], left[3],
318: right[0], right[1]);
319: } else {
320: columnText.setColumns(left, right);
321: }
322:
323: int result = columnText.go();
324: if ((result & ColumnText.NO_MORE_TEXT) != 0) {
325: done = true;
326: top = columnText.getYLine();
327: } else if (shiftCurrentColumn()) {
328: top = nextY;
329: } else { // check if we are done because of height
330: totalHeight += currentHeight;
331: if ((desiredHeight != AUTOMATIC)
332: && (totalHeight >= desiredHeight)) {
333: overflow = true;
334: break;
335: } else { // need to start new page and reset the columns
336: documentY = nextY;
337: newPage();
338: currentHeight = 0;
339: }
340: }
341: }
342: } catch (DocumentException ex) {
343: ex.printStackTrace();
344: throw ex;
345: }
346: if (desiredHeight == AUTOMATIC && columnDefs.size() == 1) {
347: currentHeight = documentY - columnText.getYLine();
348: }
349: return currentHeight;
350: }
351:
352: private void newPage() throws DocumentException {
353: resetCurrentColumn();
354: if (desiredHeight == AUTOMATIC) {
355: top = nextY = AUTOMATIC;
356: } else {
357: top = nextY;
358: }
359: totalHeight = 0;
360: if (document != null) {
361: document.newPage();
362: }
363: }
364:
365: /**
366: * Figure out the height of a column from the border extents
367: *
368: * @param left left border
369: * @param right right border
370: * @return height
371: */
372: private float getHeight(float[] left, float[] right) {
373: float max = Float.MIN_VALUE;
374: float min = Float.MAX_VALUE;
375: for (int i = 0; i < left.length; i += 2) {
376: min = Math.min(min, left[i + 1]);
377: max = Math.max(max, left[i + 1]);
378: }
379: for (int i = 0; i < right.length; i += 2) {
380: min = Math.min(min, right[i + 1]);
381: max = Math.max(max, right[i + 1]);
382: }
383: return max - min;
384: }
385:
386: /**
387: * Processes the element by adding it to an
388: * <CODE>ElementListener</CODE>.
389: *
390: * @param listener an <CODE>ElementListener</CODE>
391: * @return <CODE>true</CODE> if the element was processed successfully
392: */
393: public boolean process(ElementListener listener) {
394: try {
395: return listener.add(this );
396: } catch (DocumentException de) {
397: return false;
398: }
399: }
400:
401: /**
402: * Gets the type of the text element.
403: *
404: * @return a type
405: */
406:
407: public int type() {
408: return Element.MULTI_COLUMN_TEXT;
409: }
410:
411: /**
412: * Returns null - not used
413: *
414: * @return null
415: */
416:
417: public ArrayList getChunks() {
418: return null;
419: }
420:
421: /**
422: * Calculates the appropriate y position for the bottom
423: * of the columns on this page.
424: *
425: * @return the y position of the bottom of the columns
426: */
427: private float getColumnBottom() {
428: if (desiredHeight == AUTOMATIC) {
429: return pageBottom;
430: } else {
431: return Math.max(top - (desiredHeight - totalHeight),
432: pageBottom);
433: }
434: }
435:
436: /**
437: * Moves the text insertion point to the beginning of the next column, issuing a page break if
438: * needed.
439: * @throws DocumentException on error
440: */
441: public void nextColumn() throws DocumentException {
442: currentColumn = (currentColumn + 1) % columnDefs.size();
443: top = nextY;
444: if (currentColumn == 0) {
445: newPage();
446: }
447: }
448:
449: /**
450: * Gets the current column.
451: * @return the current column
452: */
453: public int getCurrentColumn() {
454: if (columnsRightToLeft) {
455: return (columnDefs.size() - currentColumn - 1);
456: }
457: return currentColumn;
458: }
459:
460: /**
461: * Resets the current column.
462: */
463: public void resetCurrentColumn() {
464: currentColumn = 0;
465: }
466:
467: /**
468: * Shifts the current column.
469: * @return true if the currentcolumn has changed
470: */
471: public boolean shiftCurrentColumn() {
472: if (currentColumn + 1 < columnDefs.size()) {
473: currentColumn++;
474: return true;
475: }
476: return false;
477: }
478:
479: /**
480: * Sets the direction of the columns.
481: * @param direction true = right2left; false = left2right
482: */
483: public void setColumnsRightToLeft(boolean direction) {
484: columnsRightToLeft = direction;
485: }
486:
487: /** Sets the ratio between the extra word spacing and the extra character spacing
488: * when the text is fully justified.
489: * Extra word spacing will grow <CODE>spaceCharRatio</CODE> times more than extra character spacing.
490: * If the ratio is <CODE>PdfWriter.NO_SPACE_CHAR_RATIO</CODE> then the extra character spacing
491: * will be zero.
492: * @param spaceCharRatio the ratio between the extra word spacing and the extra character spacing
493: */
494: public void setSpaceCharRatio(float spaceCharRatio) {
495: columnText.setSpaceCharRatio(spaceCharRatio);
496: }
497:
498: /** Sets the run direction.
499: * @param runDirection the run direction
500: */
501: public void setRunDirection(int runDirection) {
502: columnText.setRunDirection(runDirection);
503: }
504:
505: /** Sets the arabic shaping options. The option can be AR_NOVOWEL,
506: * AR_COMPOSEDTASHKEEL and AR_LIG.
507: * @param arabicOptions the arabic shaping options
508: */
509: public void setArabicOptions(int arabicOptions) {
510: columnText.setArabicOptions(arabicOptions);
511: }
512:
513: /** Sets the default alignment
514: * @param alignment the default alignment
515: */
516: public void setAlignment(int alignment) {
517: columnText.setAlignment(alignment);
518: }
519:
520: /**
521: * Inner class used to define a column
522: */
523: private class ColumnDef {
524: private float[] left;
525: private float[] right;
526:
527: ColumnDef(float[] newLeft, float[] newRight) {
528: left = newLeft;
529: right = newRight;
530: }
531:
532: ColumnDef(float leftPosition, float rightPosition) {
533: left = new float[4];
534: left[0] = leftPosition; // x1
535: left[1] = top; // y1
536: left[2] = leftPosition; // x2
537: if (desiredHeight == AUTOMATIC || top == AUTOMATIC) {
538: left[3] = AUTOMATIC;
539: } else {
540: left[3] = top - desiredHeight;
541: }
542:
543: right = new float[4];
544: right[0] = rightPosition; // x1
545: right[1] = top; // y1
546: right[2] = rightPosition; // x2
547: if (desiredHeight == AUTOMATIC || top == AUTOMATIC) {
548: right[3] = AUTOMATIC;
549: } else {
550: right[3] = top - desiredHeight;
551: }
552: }
553:
554: /**
555: * Resolves the positions for the specified side of the column
556: * into real numbers once the top of the column is known.
557: *
558: * @param side either <CODE>Rectangle.LEFT</CODE>
559: * or <CODE>Rectangle.RIGHT</CODE>
560: * @return the array of floats for the side
561: */
562: float[] resolvePositions(int side) {
563: if (side == Rectangle.LEFT) {
564: return resolvePositions(left);
565: } else {
566: return resolvePositions(right);
567: }
568: }
569:
570: private float[] resolvePositions(float[] positions) {
571: if (!isSimple()) {
572: return positions;
573: }
574: if (top == AUTOMATIC) {
575: // this is bad - must be programmer error
576: throw new RuntimeException(
577: "resolvePositions called with top=AUTOMATIC (-1). "
578: + "Top position must be set befure lines can be resolved");
579: }
580: positions[1] = top;
581: positions[3] = getColumnBottom();
582: return positions;
583: }
584:
585: /**
586: * Checks if column definition is a simple rectangle
587: * @return true if it is a simple column
588: */
589: private boolean isSimple() {
590: return (left.length == 4 && right.length == 4)
591: && (left[0] == left[2] && right[0] == right[2]);
592: }
593:
594: }
595: }
|