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}