001/* 002 * $Id: Phrase.java 4879 2011-05-23 23:27:02Z redlab_b $ 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; 045 046import java.util.ArrayList; 047import java.util.Collection; 048 049import com.itextpdf.text.Font.FontFamily; 050import com.itextpdf.text.error_messages.MessageLocalization; 051import com.itextpdf.text.pdf.HyphenationEvent; 052 053/** 054 * A <CODE>Phrase</CODE> is a series of <CODE>Chunk</CODE>s. 055 * <P> 056 * A <CODE>Phrase</CODE> has a main <CODE>Font</CODE>, but some chunks 057 * within the phrase can have a <CODE>Font</CODE> that differs from the 058 * main <CODE>Font</CODE>. All the <CODE>Chunk</CODE>s in a <CODE>Phrase</CODE> 059 * have the same <CODE>leading</CODE>. 060 * <P> 061 * Example: 062 * <BLOCKQUOTE><PRE> 063 * // When no parameters are passed, the default leading = 16 064 * <STRONG>Phrase phrase0 = new Phrase();</STRONG> 065 * <STRONG>Phrase phrase1 = new Phrase("this is a phrase");</STRONG> 066 * // In this example the leading is passed as a parameter 067 * <STRONG>Phrase phrase2 = new Phrase(16, "this is a phrase with leading 16");</STRONG> 068 * // When a Font is passed (explicitly or embedded in a chunk), the default leading = 1.5 * size of the font 069 * <STRONG>Phrase phrase3 = new Phrase("this is a phrase with a red, normal font Courier, size 12", FontFactory.getFont(FontFactory.COURIER, 12, Font.NORMAL, new Color(255, 0, 0)));</STRONG> 070 * <STRONG>Phrase phrase4 = new Phrase(new Chunk("this is a phrase"));</STRONG> 071 * <STRONG>Phrase phrase5 = new Phrase(18, new Chunk("this is a phrase", FontFactory.getFont(FontFactory.HELVETICA, 16, Font.BOLD, new Color(255, 0, 0)));</STRONG> 072 * </PRE></BLOCKQUOTE> 073 * 074 * @see Element 075 * @see Chunk 076 * @see Paragraph 077 * @see Anchor 078 */ 079 080public class Phrase extends ArrayList<Element> implements TextElementArray { 081 082 // constants 083 private static final long serialVersionUID = 2643594602455068231L; 084 085 // membervariables 086 /** This is the leading of this phrase. */ 087 protected float leading = Float.NaN; 088 089 /** This is the font of this phrase. */ 090 protected Font font; 091 092 /** Null, unless the Phrase has to be hyphenated. 093 * @since 2.1.2 094 */ 095 protected HyphenationEvent hyphenation = null; 096 097 // constructors 098 099 /** 100 * Constructs a <CODE>Phrase</CODE> without specifying a leading. 101 */ 102 public Phrase() { 103 this(16); 104 } 105 106 /** 107 * Copy constructor for <CODE>Phrase</CODE>. 108 * @param phrase the Phrase to copy 109 */ 110 public Phrase(final Phrase phrase) { 111 super(); 112 this.addAll(phrase); 113 leading = phrase.getLeading(); 114 font = phrase.getFont(); 115 setHyphenation(phrase.getHyphenation()); 116 } 117 118 /** 119 * Constructs a <CODE>Phrase</CODE> with a certain leading. 120 * 121 * @param leading the leading 122 */ 123 public Phrase(final float leading) { 124 this.leading = leading; 125 font = new Font(); 126 } 127 128 /** 129 * Constructs a <CODE>Phrase</CODE> with a certain <CODE>Chunk</CODE>. 130 * 131 * @param chunk a <CODE>Chunk</CODE> 132 */ 133 public Phrase(final Chunk chunk) { 134 super.add(chunk); 135 font = chunk.getFont(); 136 setHyphenation(chunk.getHyphenation()); 137 } 138 139 /** 140 * Constructs a <CODE>Phrase</CODE> with a certain <CODE>Chunk</CODE> 141 * and a certain leading. 142 * 143 * @param leading the leading 144 * @param chunk a <CODE>Chunk</CODE> 145 */ 146 public Phrase(final float leading, final Chunk chunk) { 147 this.leading = leading; 148 super.add(chunk); 149 font = chunk.getFont(); 150 setHyphenation(chunk.getHyphenation()); 151 } 152 153 /** 154 * Constructs a <CODE>Phrase</CODE> with a certain <CODE>String</CODE>. 155 * 156 * @param string a <CODE>String</CODE> 157 */ 158 public Phrase(final String string) { 159 this(Float.NaN, string, new Font()); 160 } 161 162 /** 163 * Constructs a <CODE>Phrase</CODE> with a certain <CODE>String</CODE> and a certain <CODE>Font</CODE>. 164 * 165 * @param string a <CODE>String</CODE> 166 * @param font a <CODE>Font</CODE> 167 */ 168 public Phrase(final String string, final Font font) { 169 this(Float.NaN, string, font); 170 } 171 172 /** 173 * Constructs a <CODE>Phrase</CODE> with a certain leading and a certain <CODE>String</CODE>. 174 * 175 * @param leading the leading 176 * @param string a <CODE>String</CODE> 177 */ 178 public Phrase(final float leading, final String string) { 179 this(leading, string, new Font()); 180 } 181 182 /** 183 * Constructs a <CODE>Phrase</CODE> with a certain leading, a certain <CODE>String</CODE> 184 * and a certain <CODE>Font</CODE>. 185 * 186 * @param leading the leading 187 * @param string a <CODE>String</CODE> 188 * @param font a <CODE>Font</CODE> 189 */ 190 public Phrase(final float leading, final String string, final Font font) { 191 this.leading = leading; 192 this.font = font; 193 /* bugfix by August Detlefsen */ 194 if (string != null && string.length() != 0) { 195 super.add(new Chunk(string, font)); 196 } 197 } 198 199 // implementation of the Element-methods 200 201 /** 202 * Processes the element by adding it (or the different parts) to an 203 * <CODE>ElementListener</CODE>. 204 * 205 * @param listener an <CODE>ElementListener</CODE> 206 * @return <CODE>true</CODE> if the element was processed successfully 207 */ 208 public boolean process(final ElementListener listener) { 209 try { 210 for (Object element : this) { 211 listener.add((Element) element); 212 } 213 return true; 214 } 215 catch(DocumentException de) { 216 return false; 217 } 218 } 219 220 /** 221 * Gets the type of the text element. 222 * 223 * @return a type 224 */ 225 public int type() { 226 return Element.PHRASE; 227 } 228 229 /** 230 * Gets all the chunks in this element. 231 * 232 * @return an <CODE>ArrayList</CODE> 233 */ 234 public java.util.List<Chunk> getChunks() { 235 java.util.List<Chunk> tmp = new ArrayList<Chunk>(); 236 for (Element element : this) { 237 tmp.addAll(element.getChunks()); 238 } 239 return tmp; 240 } 241 242 /** 243 * @see com.itextpdf.text.Element#isContent() 244 * @since iText 2.0.8 245 */ 246 public boolean isContent() { 247 return true; 248 } 249 250 /** 251 * @see com.itextpdf.text.Element#isNestable() 252 * @since iText 2.0.8 253 */ 254 public boolean isNestable() { 255 return true; 256 } 257 258 // overriding some of the ArrayList-methods 259 260 /** 261 * Adds a <CODE>Chunk</CODE>, an <CODE>Anchor</CODE> or another <CODE>Phrase</CODE> 262 * to this <CODE>Phrase</CODE>. 263 * 264 * @param index index at which the specified element is to be inserted 265 * @param element an object of type <CODE>Chunk</CODE>, <CODE>Anchor</CODE> or <CODE>Phrase</CODE> 266 * @throws ClassCastException when you try to add something that isn't a <CODE>Chunk</CODE>, <CODE>Anchor</CODE> or <CODE>Phrase</CODE> 267 * @since 5.0.1 (signature changed to use Element) 268 */ 269 @Override 270 public void add(final int index, final Element element) { 271 if (element == null) return; 272 try { 273 if (element.type() == Element.CHUNK) { 274 Chunk chunk = (Chunk) element; 275 if (!font.isStandardFont()) { 276 chunk.setFont(font.difference(chunk.getFont())); 277 } 278 if (hyphenation != null && chunk.getHyphenation() == null && !chunk.isEmpty()) { 279 chunk.setHyphenation(hyphenation); 280 } 281 super.add(index, chunk); 282 } 283 // TODO same as in document - change else-if to generic adding that works everywhere 284 else if (element.type() == Element.PHRASE || 285 element.type() == Element.ANCHOR || 286 element.type() == Element.ANNOTATION || 287 element.type() == Element.YMARK || 288 element.type() == Element.MARKED || element.type() == Element.WRITABLE_DIRECT) { 289 super.add(index, element); 290 } 291 else { 292 throw new ClassCastException(String.valueOf(element.type())); 293 } 294 } 295 catch(ClassCastException cce) { 296 throw new ClassCastException(MessageLocalization.getComposedMessage("insertion.of.illegal.element.1", cce.getMessage())); 297 } 298 } 299 300 /** 301 * Adds a <CODE>String</CODE> to this <CODE>Phrase</CODE>. 302 * 303 * @param s a string 304 * @return a boolean 305 * @since 5.0.1 306 */ 307 public boolean add(final String s) { 308 if (s == null) { 309 return false; 310 } 311 return super.add(new Chunk(s, font)); 312 } 313 314 /** 315 * Adds a <CODE>Chunk</CODE>, <CODE>Anchor</CODE> or another <CODE>Phrase</CODE> 316 * to this <CODE>Phrase</CODE>. 317 * 318 * @param element an object of type <CODE>Chunk</CODE>, <CODE>Anchor</CODE> or <CODE>Phrase</CODE> 319 * @return a boolean 320 * @throws ClassCastException when you try to add something that isn't a <CODE>Chunk</CODE>, <CODE>Anchor</CODE> or <CODE>Phrase</CODE> 321 * @since 5.0.1 (signature changed to use Element) 322 */ 323 @Override 324 public boolean add(final Element element) { 325 if (element == null) return false; 326 try { 327 // TODO same as in document - change switch to generic adding that works everywhere 328 switch(element.type()) { 329 case Element.CHUNK: 330 return addChunk((Chunk) element); 331 case Element.PHRASE: 332 case Element.PARAGRAPH: 333 Phrase phrase = (Phrase) element; 334 boolean success = true; 335 Element e; 336 for (Object element2 : phrase) { 337 e = (Element) element2; 338 if (e instanceof Chunk) { 339 success &= addChunk((Chunk)e); 340 } 341 else { 342 success &= this.add(e); 343 } 344 } 345 return success; 346 case Element.MARKED: 347 case Element.ANCHOR: 348 case Element.ANNOTATION: 349 case Element.PTABLE: // case added by mr. Karen Vardanyan 350 case Element.LIST: 351 case Element.YMARK: 352 case Element.WRITABLE_DIRECT: 353 return super.add(element); 354 default: 355 throw new ClassCastException(String.valueOf(element.type())); 356 } 357 } 358 catch(ClassCastException cce) { 359 throw new ClassCastException(MessageLocalization.getComposedMessage("insertion.of.illegal.element.1", cce.getMessage())); 360 } 361 } 362 363 /** 364 * Adds a collection of <CODE>Chunk</CODE>s 365 * to this <CODE>Phrase</CODE>. 366 * 367 * @param collection a collection of <CODE>Chunk</CODE>s, <CODE>Anchor</CODE>s and <CODE>Phrase</CODE>s. 368 * @return <CODE>true</CODE> if the action succeeded, <CODE>false</CODE> if not. 369 * @throws ClassCastException when you try to add something that isn't a <CODE>Chunk</CODE>, <CODE>Anchor</CODE> or <CODE>Phrase</CODE> 370 */ 371 @Override 372 public boolean addAll(final Collection<? extends Element> collection) { 373 for (Element e: collection) { 374 this.add(e); 375 } 376 return true; 377 } 378 379 /** 380 * Adds a Chunk. 381 * <p> 382 * This method is a hack to solve a problem I had with phrases that were split between chunks 383 * in the wrong place. 384 * @param chunk a Chunk to add to the Phrase 385 * @return true if adding the Chunk succeeded 386 */ 387 protected boolean addChunk(final Chunk chunk) { 388 Font f = chunk.getFont(); 389 String c = chunk.getContent(); 390 if (font != null && !font.isStandardFont()) { 391 f = font.difference(chunk.getFont()); 392 } 393 if (size() > 0 && !chunk.hasAttributes()) { 394 try { 395 Chunk previous = (Chunk) get(size() - 1); 396 if (!previous.hasAttributes() 397 && (f == null 398 || f.compareTo(previous.getFont()) == 0) 399 && !"".equals(previous.getContent().trim()) 400 && !"".equals(c.trim())) { 401 previous.append(c); 402 return true; 403 } 404 } 405 catch(ClassCastException cce) { 406 } 407 } 408 Chunk newChunk = new Chunk(c, f); 409 newChunk.setAttributes(chunk.getAttributes()); 410 if (hyphenation != null && newChunk.getHyphenation() == null && !newChunk.isEmpty()) { 411 newChunk.setHyphenation(hyphenation); 412 } 413 return super.add(newChunk); 414 } 415 416 /** 417 * Adds an <CODE>Element</CODE> to the <CODE>Paragraph</CODE>. 418 * 419 * @param object the object to add. 420 */ 421 protected void addSpecial(final Element object) { 422 super.add(object); 423 } 424 425 // other methods that change the member variables 426 427 /** 428 * Sets the leading of this phrase. 429 * 430 * @param leading the new leading 431 */ 432 433 public void setLeading(final float leading) { 434 this.leading = leading; 435 } 436 437 /** 438 * Sets the main font of this phrase. 439 * @param font the new font 440 */ 441 public void setFont(final Font font) { 442 this.font = font; 443 } 444 445 // methods to retrieve information 446 447 /** 448 * Gets the leading of this phrase. 449 * 450 * @return the linespacing 451 */ 452 public float getLeading() { 453 if (Float.isNaN(leading) && font != null) { 454 return font.getCalculatedLeading(1.5f); 455 } 456 return leading; 457 } 458 459 /** 460 * Checks you if the leading of this phrase is defined. 461 * 462 * @return true if the leading is defined 463 */ 464 public boolean hasLeading() { 465 if (Float.isNaN(leading)) { 466 return false; 467 } 468 return true; 469 } 470 471 /** 472 * Gets the font of the first <CODE>Chunk</CODE> that appears in this <CODE>Phrase</CODE>. 473 * 474 * @return a <CODE>Font</CODE> 475 */ 476 public Font getFont() { 477 return font; 478 } 479 480 /** 481 * Returns the content as a String object. 482 * This method differs from toString because toString will return an ArrayList with the toString value of the Chunks in this Phrase. 483 * @return the content 484 */ 485 public String getContent() { 486 StringBuffer buf = new StringBuffer(); 487 for (Chunk c: getChunks()) { 488 buf.append(c.toString()); 489 } 490 return buf.toString(); 491 } 492 493 /** 494 * Checks is this <CODE>Phrase</CODE> contains no or 1 empty <CODE>Chunk</CODE>. 495 * 496 * @return <CODE>false</CODE> if the <CODE>Phrase</CODE> 497 * contains more than one or more non-empty<CODE>Chunk</CODE>s. 498 */ 499 @Override 500 public boolean isEmpty() { 501 switch(size()) { 502 case 0: 503 return true; 504 case 1: 505 Element element = get(0); 506 if (element.type() == Element.CHUNK && ((Chunk) element).isEmpty()) { 507 return true; 508 } 509 return false; 510 default: 511 return false; 512 } 513 } 514 515 /** 516 * Getter for the hyphenation settings. 517 * @return a HyphenationEvent 518 * @since 2.1.2 519 */ 520 public HyphenationEvent getHyphenation() { 521 return hyphenation; 522 } 523 524 /** 525 * Setter for the hyphenation. 526 * @param hyphenation a HyphenationEvent instance 527 * @since 2.1.2 528 */ 529 public void setHyphenation(final HyphenationEvent hyphenation) { 530 this.hyphenation = hyphenation; 531 } 532 533 // kept for historical reasons; people should use FontSelector 534 // eligible for deprecation, but the methods are mentioned in the book p277. 535 536 /** 537 * Constructs a Phrase that can be used in the static getInstance() method. 538 * @param dummy a dummy parameter 539 */ 540 private Phrase(final boolean dummy) { 541 } 542 543 /** 544 * Gets a special kind of Phrase that changes some characters into corresponding symbols. 545 * @param string 546 * @return a newly constructed Phrase 547 */ 548 public static final Phrase getInstance(final String string) { 549 return getInstance(16, string, new Font()); 550 } 551 552 /** 553 * Gets a special kind of Phrase that changes some characters into corresponding symbols. 554 * @param leading 555 * @param string 556 * @return a newly constructed Phrase 557 */ 558 public static final Phrase getInstance(final int leading, final String string) { 559 return getInstance(leading, string, new Font()); 560 } 561 562 /** 563 * Gets a special kind of Phrase that changes some characters into corresponding symbols. 564 * @param leading 565 * @param string 566 * @param font 567 * @return a newly constructed Phrase 568 */ 569 public static final Phrase getInstance(final int leading, String string, final Font font) { 570 Phrase p = new Phrase(true); 571 p.setLeading(leading); 572 p.font = font; 573 if (font.getFamily() != FontFamily.SYMBOL && font.getFamily() != FontFamily.ZAPFDINGBATS && font.getBaseFont() == null) { 574 int index; 575 while((index = SpecialSymbol.index(string)) > -1) { 576 if (index > 0) { 577 String firstPart = string.substring(0, index); 578 p.add(new Chunk(firstPart, font)); 579 string = string.substring(index); 580 } 581 Font symbol = new Font(FontFamily.SYMBOL, font.getSize(), font.getStyle(), font.getColor()); 582 StringBuffer buf = new StringBuffer(); 583 buf.append(SpecialSymbol.getCorrespondingSymbol(string.charAt(0))); 584 string = string.substring(1); 585 while (SpecialSymbol.index(string) == 0) { 586 buf.append(SpecialSymbol.getCorrespondingSymbol(string.charAt(0))); 587 string = string.substring(1); 588 } 589 p.add(new Chunk(buf.toString(), symbol)); 590 } 591 } 592 if (string != null && string.length() != 0) { 593 p.add(new Chunk(string, font)); 594 } 595 return p; 596 } 597 598}