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