All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.aowagie.text.pdf.PdfChunk Maven / Gradle / Ivy

/*
 * $Id: PdfChunk.java 3407 2008-05-21 16:56:55Z blowagie $
 *
 * Copyright 1999, 2000, 2001, 2002 Bruno Lowagie
 *
 * The contents of this file are subject to the Mozilla Public License Version 1.1
 * (the "License"); you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at http://www.mozilla.org/MPL/
 *
 * Software distributed under the License is distributed on an "AS IS" basis,
 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
 * for the specific language governing rights and limitations under the License.
 *
 * The Original Code is 'iText, a free JAVA-PDF library'.
 *
 * The Initial Developer of the Original Code is Bruno Lowagie. Portions created by
 * the Initial Developer are Copyright (C) 1999, 2000, 2001, 2002 by Bruno Lowagie.
 * All Rights Reserved.
 * Co-Developer of the code is Paulo Soares. Portions created by the Co-Developer
 * are Copyright (C) 2000, 2001, 2002 by Paulo Soares. All Rights Reserved.
 *
 * Contributor(s): all the names of the contributors are added in the source code
 * where applicable.
 *
 * Alternatively, the contents of this file may be used under the terms of the
 * LGPL license (the "GNU LIBRARY GENERAL PUBLIC LICENSE"), in which case the
 * provisions of LGPL are applicable instead of those above.  If you wish to
 * allow use of your version of this file only under the terms of the LGPL
 * License and not to allow others to use your version of this file under
 * the MPL, indicate your decision by deleting the provisions above and
 * replace them with the notice and other provisions required by the LGPL.
 * If you do not delete the provisions above, a recipient may use your version
 * of this file under either the MPL or the GNU LIBRARY GENERAL PUBLIC LICENSE.
 *
 * This library is free software; you can redistribute it and/or modify it
 * under the terms of the MPL as stated above or under the terms of the GNU
 * Library General Public License as published by the Free Software Foundation;
 * either version 2 of the License, or any later version.
 *
 * This library is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 * FOR A PARTICULAR PURPOSE. See the GNU Library general Public License for more
 * details.
 *
 * If you didn't download this code from the following link, you should check if
 * you aren't using an obsolete version:
 * http://www.lowagie.com/iText/
 */

package com.aowagie.text.pdf;

import java.awt.Color;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;

import com.aowagie.text.Chunk;
import com.aowagie.text.Font;
import com.aowagie.text.Image;
import com.aowagie.text.SplitCharacter;
import com.aowagie.text.Utilities;

/**
 * A PdfChunk is the PDF translation of a Chunk.
 * 

* A PdfChunk is a PdfString in a certain * PdfFont and Color. * * @see PdfString * @see com.aowagie.text.Chunk * @see com.aowagie.text.Font */ public class PdfChunk { private static final char singleSpace[] = {' '}; private static final PdfChunk thisChunk[] = new PdfChunk[1]; private static final float ITALIC_ANGLE = 0.21256f; /** The allowed attributes in variable attributes. */ private static final HashMap keysAttributes = new LinkedHashMap(); /** The allowed attributes in variable noStroke. */ private static final HashMap keysNoStroke = new HashMap(); static { keysAttributes.put(Chunk.ACTION, null); keysAttributes.put(Chunk.UNDERLINE, null); keysAttributes.put(Chunk.REMOTEGOTO, null); keysAttributes.put(Chunk.LOCALGOTO, null); keysAttributes.put(Chunk.LOCALDESTINATION, null); keysAttributes.put(Chunk.GENERICTAG, null); keysAttributes.put(Chunk.NEWPAGE, null); keysAttributes.put(Chunk.IMAGE, null); keysAttributes.put(Chunk.BACKGROUND, null); keysAttributes.put(Chunk.PDFANNOTATION, null); keysAttributes.put(Chunk.SKEW, null); keysAttributes.put(Chunk.HSCALE, null); keysAttributes.put(Chunk.SEPARATOR, null); keysAttributes.put(Chunk.TAB, null); keysNoStroke.put(Chunk.SUBSUPSCRIPT, null); keysNoStroke.put(Chunk.SPLITCHARACTER, null); keysNoStroke.put(Chunk.HYPHENATION, null); keysNoStroke.put(Chunk.TEXTRENDERMODE, null); } // membervariables /** The value of this object. */ private String value = PdfObject.NOTHING; /** The encoding. */ private String encoding = BaseFont.WINANSI; /** The font for this PdfChunk. */ protected PdfFont font; private BaseFont baseFont; private SplitCharacter splitCharacter; /** * Metric attributes. *

* This attributes require the measurement of characters widths when rendering * such as underline. */ private HashMap attributes = new LinkedHashMap(); /** * Non metric attributes. *

* This attributes do not require the measurement of characters widths when rendering * such as Color. */ private HashMap noStroke = new HashMap(); /** true if the chunk split was cause by a newline. */ private boolean newlineSplit; /** The image in this PdfChunk, if it has one */ private Image image; /** The offset in the x direction for the image */ private float offsetX; /** The offset in the y direction for the image */ private float offsetY; /** Indicates if the height and offset of the Image has to be taken into account */ protected boolean changeLeading = false; // constructors /** * Constructs a PdfChunk-object. * * @param string the content of the PdfChunk-object * @param other Chunk with the same style you want for the new Chunk */ PdfChunk(final String string, final PdfChunk other) { thisChunk[0] = this; this.value = string; this.font = other.font; this.attributes = other.attributes; this.noStroke = other.noStroke; this.baseFont = other.baseFont; final Object obj[] = (Object[])this.attributes.get(Chunk.IMAGE); if (obj == null) { this.image = null; } else { this.image = (Image)obj[0]; this.offsetX = ((Float)obj[1]).floatValue(); this.offsetY = ((Float)obj[2]).floatValue(); this.changeLeading = ((Boolean)obj[3]).booleanValue(); } this.encoding = this.font.getFont().getEncoding(); this.splitCharacter = (SplitCharacter)this.noStroke.get(Chunk.SPLITCHARACTER); if (this.splitCharacter == null) { this.splitCharacter = DefaultSplitCharacter.DEFAULT; } } /** * Constructs a PdfChunk-object. * * @param chunk the original Chunk-object * @param action the PdfAction if the Chunk comes from an Anchor */ PdfChunk(final Chunk chunk, final PdfAction action) { thisChunk[0] = this; this.value = chunk.getContent(); final Font f = chunk.getFont(); float size = f.getSize(); if (size == Font.UNDEFINED) { size = 12; } this.baseFont = f.getBaseFont(); int style = f.getStyle(); if (style == Font.UNDEFINED) { style = Font.NORMAL; } if (this.baseFont == null) { // translation of the font-family to a PDF font-family this.baseFont = f.getCalculatedBaseFont(false); } else { // bold simulation if ((style & Font.BOLD) != 0) { this.attributes.put(Chunk.TEXTRENDERMODE, new Object[]{Integer.valueOf(PdfContentByte.TEXT_RENDER_MODE_FILL_STROKE), Float.valueOf(size / 30f), null}); } // italic simulation if ((style & Font.ITALIC) != 0) { this.attributes.put(Chunk.SKEW, new float[]{0, ITALIC_ANGLE}); } } this.font = new PdfFont(this.baseFont, size); // other style possibilities final HashMap attr = chunk.getAttributes(); if (attr != null) { for (final Iterator i = attr.entrySet().iterator(); i.hasNext();) { final Map.Entry entry = (Map.Entry) i.next(); final Object name = entry.getKey(); if (keysAttributes.containsKey(name)) { this.attributes.put(name, entry.getValue()); } else if (keysNoStroke.containsKey(name)) { this.noStroke.put(name, entry.getValue()); } } if ("".equals(attr.get(Chunk.GENERICTAG))) { this.attributes.put(Chunk.GENERICTAG, chunk.getContent()); } } if (f.isUnderlined()) { final Object obj[] = {null, new float[]{0, 1f / 15, 0, -1f / 3, 0}}; final Object unders[][] = Utilities.addToArray((Object[][])this.attributes.get(Chunk.UNDERLINE), obj); this.attributes.put(Chunk.UNDERLINE, unders); } if (f.isStrikethru()) { final Object obj[] = {null, new float[]{0, 1f / 15, 0, 1f / 3, 0}}; final Object unders[][] = Utilities.addToArray((Object[][])this.attributes.get(Chunk.UNDERLINE), obj); this.attributes.put(Chunk.UNDERLINE, unders); } if (action != null) { this.attributes.put(Chunk.ACTION, action); } // the color can't be stored in a PdfFont this.noStroke.put(Chunk.COLOR, f.getColor()); this.noStroke.put(Chunk.ENCODING, this.font.getFont().getEncoding()); final Object obj[] = (Object[])this.attributes.get(Chunk.IMAGE); if (obj == null) { this.image = null; } else { this.attributes.remove(Chunk.HSCALE); // images are scaled in other ways this.image = (Image)obj[0]; this.offsetX = ((Float)obj[1]).floatValue(); this.offsetY = ((Float)obj[2]).floatValue(); this.changeLeading = ((Boolean)obj[3]).booleanValue(); } this.font.setImage(this.image); final Float hs = (Float)this.attributes.get(Chunk.HSCALE); if (hs != null) { this.font.setHorizontalScaling(hs.floatValue()); } this.encoding = this.font.getFont().getEncoding(); this.splitCharacter = (SplitCharacter)this.noStroke.get(Chunk.SPLITCHARACTER); if (this.splitCharacter == null) { this.splitCharacter = DefaultSplitCharacter.DEFAULT; } } // methods /** Gets the Unicode equivalent to a CID. * The (inexistent) CID is translated as '\n'. * It has only meaning with CJK fonts with Identity encoding. * @param c the CID code * @return the Unicode equivalent */ int getUnicodeEquivalent(final int c) { return this.baseFont.getUnicodeEquivalent(c); } private int getWord(final String text, int start) { final int len = text.length(); while (start < len) { if (!Character.isLetter(text.charAt(start))) { break; } ++start; } return start; } /** * Splits this PdfChunk if it's too long for the given width. *

* Returns null if the PdfChunk wasn't truncated. * * @param width a given width * @return the PdfChunk that doesn't fit into the width. */ PdfChunk split(final float width) { this.newlineSplit = false; if (this.image != null) { if (this.image.getScaledWidth() > width) { final PdfChunk pc = new PdfChunk(Chunk.OBJECT_REPLACEMENT_CHARACTER, this); this.value = ""; this.attributes = new HashMap(); this.image = null; this.font = PdfFont.getDefaultFont(); return pc; } else { return null; } } final HyphenationEvent hyphenationEvent = (HyphenationEvent)this.noStroke.get(Chunk.HYPHENATION); int currentPosition = 0; int splitPosition = -1; float currentWidth = 0; // loop over all the characters of a string // or until the totalWidth is reached int lastSpace = -1; float lastSpaceWidth = 0; final int length = this.value.length(); final char valueArray[] = this.value.toCharArray(); char character = 0; final BaseFont ft = this.font.getFont(); boolean surrogate = false; if (ft.getFontType() == BaseFont.FONT_TYPE_CJK && ft.getUnicodeEquivalent(' ') != ' ') { while (currentPosition < length) { // the width of every character is added to the currentWidth final char cidChar = valueArray[currentPosition]; character = (char)ft.getUnicodeEquivalent(cidChar); // if a newLine or carriageReturn is encountered if (character == '\n') { this.newlineSplit = true; final String returnValue = this.value.substring(currentPosition + 1); this.value = this.value.substring(0, currentPosition); if (this.value.length() < 1) { this.value = "\u0001"; } final PdfChunk pc = new PdfChunk(returnValue, this); return pc; } currentWidth += this.font.width(cidChar); if (character == ' ') { lastSpace = currentPosition + 1; lastSpaceWidth = currentWidth; } if (currentWidth > width) { break; } // if a split-character is encountered, the splitPosition is altered if (this.splitCharacter.isSplitCharacter(0, currentPosition, length, valueArray, thisChunk)) { splitPosition = currentPosition + 1; } currentPosition++; } } else { while (currentPosition < length) { // the width of every character is added to the currentWidth character = valueArray[currentPosition]; // if a newLine or carriageReturn is encountered if (character == '\r' || character == '\n') { this.newlineSplit = true; int inc = 1; if (character == '\r' && currentPosition + 1 < length && valueArray[currentPosition + 1] == '\n') { inc = 2; } final String returnValue = this.value.substring(currentPosition + inc); this.value = this.value.substring(0, currentPosition); if (this.value.length() < 1) { this.value = " "; } final PdfChunk pc = new PdfChunk(returnValue, this); return pc; } surrogate = Utilities.isSurrogatePair(valueArray, currentPosition); if (surrogate) { currentWidth += this.font.width(Utilities.convertToUtf32(valueArray[currentPosition], valueArray[currentPosition + 1])); } else { currentWidth += this.font.width(character); } if (character == ' ') { lastSpace = currentPosition + 1; lastSpaceWidth = currentWidth; } if (surrogate) { currentPosition++; } if (currentWidth > width) { break; } // if a split-character is encountered, the splitPosition is altered if (this.splitCharacter.isSplitCharacter(0, currentPosition, length, valueArray, null)) { splitPosition = currentPosition + 1; } currentPosition++; } } // if all the characters fit in the total width, null is returned (there is no overflow) if (currentPosition == length) { return null; } // otherwise, the string has to be truncated if (splitPosition < 0) { final String returnValue = this.value; this.value = ""; final PdfChunk pc = new PdfChunk(returnValue, this); return pc; } if (lastSpace > splitPosition && this.splitCharacter.isSplitCharacter(0, 0, 1, singleSpace, null)) { splitPosition = lastSpace; } if (hyphenationEvent != null && lastSpace >= 0 && lastSpace < currentPosition) { final int wordIdx = getWord(this.value, lastSpace); if (wordIdx > lastSpace) { final String pre = hyphenationEvent.getHyphenatedWordPre(this.value.substring(lastSpace, wordIdx), this.font.getFont(), this.font.size(), width - lastSpaceWidth); final String post = hyphenationEvent.getHyphenatedWordPost(); if (pre.length() > 0) { final String returnValue = post + this.value.substring(wordIdx); this.value = trim(this.value.substring(0, lastSpace) + pre); final PdfChunk pc = new PdfChunk(returnValue, this); return pc; } } } final String returnValue = this.value.substring(splitPosition); this.value = trim(this.value.substring(0, splitPosition)); final PdfChunk pc = new PdfChunk(returnValue, this); return pc; } /** * Truncates this PdfChunk if it's too long for the given width. *

* Returns null if the PdfChunk wasn't truncated. * * @param width a given width * @return the PdfChunk that doesn't fit into the width. */ PdfChunk truncate(final float width) { if (this.image != null) { if (this.image.getScaledWidth() > width) { final PdfChunk pc = new PdfChunk("", this); this.value = ""; this.attributes.remove(Chunk.IMAGE); this.image = null; this.font = PdfFont.getDefaultFont(); return pc; } else { return null; } } int currentPosition = 0; float currentWidth = 0; // it's no use trying to split if there isn't even enough place for a space if (width < this.font.width()) { final String returnValue = this.value.substring(1); this.value = this.value.substring(0, 1); final PdfChunk pc = new PdfChunk(returnValue, this); return pc; } // loop over all the characters of a string // or until the totalWidth is reached final int length = this.value.length(); boolean surrogate = false; final char character; while (currentPosition < length) { // the width of every character is added to the currentWidth surrogate = Utilities.isSurrogatePair(this.value, currentPosition); if (surrogate) { currentWidth += this.font.width(Utilities.convertToUtf32(this.value, currentPosition)); } else { currentWidth += this.font.width(this.value.charAt(currentPosition)); } if (currentWidth > width) { break; } if (surrogate) { currentPosition++; } currentPosition++; } // if all the characters fit in the total width, null is returned (there is no overflow) if (currentPosition == length) { return null; } // otherwise, the string has to be truncated //currentPosition -= 2; // we have to chop off minimum 1 character from the chunk if (currentPosition == 0) { currentPosition = 1; if (surrogate) { ++currentPosition; } } final String returnValue = this.value.substring(currentPosition); this.value = this.value.substring(0, currentPosition); final PdfChunk pc = new PdfChunk(returnValue, this); return pc; } // methods to retrieve the membervariables /** * Returns the font of this Chunk. * * @return a PdfFont */ PdfFont font() { return this.font; } /** * Returns the color of this Chunk. * * @return a Color */ Color color() { return (Color)this.noStroke.get(Chunk.COLOR); } /** * Returns the width of this PdfChunk. * * @return a width */ float width() { return this.font.width(this.value); } /** * Checks if the PdfChunk split was caused by a newline. * @return true if the PdfChunk split was caused by a newline. */ public boolean isNewlineSplit() { return this.newlineSplit; } /** * Gets the width of the PdfChunk taking into account the * extra character and word spacing. * @param charSpacing the extra character spacing * @param wordSpacing the extra word spacing * @return the calculated width */ float getWidthCorrected(final float charSpacing, final float wordSpacing) { if (this.image != null) { return this.image.getScaledWidth() + charSpacing; } int numberOfSpaces = 0; int idx = -1; while ((idx = this.value.indexOf(' ', idx + 1)) >= 0) { ++numberOfSpaces; } return width() + (this.value.length() * charSpacing + numberOfSpaces * wordSpacing); } /** * Gets the text displacement relative to the baseline. * @return a displacement in points */ public float getTextRise() { final Float f = (Float) getAttribute(Chunk.SUBSUPSCRIPT); if (f != null) { return f.floatValue(); } return 0.0f; } /** * Trims the last space. * @return the width of the space trimmed, otherwise 0 */ float trimLastSpace() { final BaseFont ft = this.font.getFont(); if (ft.getFontType() == BaseFont.FONT_TYPE_CJK && ft.getUnicodeEquivalent(' ') != ' ') { if (this.value.length() > 1 && this.value.endsWith("\u0001")) { this.value = this.value.substring(0, this.value.length() - 1); return this.font.width('\u0001'); } } else { if (this.value.length() > 1 && this.value.endsWith(" ")) { this.value = this.value.substring(0, this.value.length() - 1); return this.font.width(' '); } } return 0; } public float trimFirstSpace() { final BaseFont ft = this.font.getFont(); if (ft.getFontType() == BaseFont.FONT_TYPE_CJK && ft.getUnicodeEquivalent(' ') != ' ') { if (this.value.length() > 1 && this.value.startsWith("\u0001")) { this.value = this.value.substring(1); return this.font.width('\u0001'); } } else { if (this.value.length() > 1 && this.value.startsWith(" ")) { this.value = this.value.substring(1); return this.font.width(' '); } } return 0; } /** * Gets an attribute. The search is made in attributes * and noStroke. * @param name the attribute key * @return the attribute value or null if not found */ Object getAttribute(final String name) { if (this.attributes.containsKey(name)) { return this.attributes.get(name); } return this.noStroke.get(name); } /** *Checks if the attribute exists. * @param name the attribute key * @return true if the attribute exists */ boolean isAttribute(final String name) { if (this.attributes.containsKey(name)) { return true; } return this.noStroke.containsKey(name); } /** * Checks if this PdfChunk needs some special metrics handling. * @return true if this PdfChunk needs some special metrics handling. */ boolean isStroked() { return !this.attributes.isEmpty(); } /** * Checks if this PdfChunk is a Separator Chunk. * @return true if this chunk is a separator. * @since 2.1.2 */ boolean isSeparator() { return isAttribute(Chunk.SEPARATOR); } /** * Checks if this PdfChunk is a horizontal Separator Chunk. * @return true if this chunk is a horizontal separator. * @since 2.1.2 */ boolean isHorizontalSeparator() { if (isAttribute(Chunk.SEPARATOR)) { final Object[] o = (Object[])getAttribute(Chunk.SEPARATOR); return !((Boolean)o[1]).booleanValue(); } return false; } /** * Checks if this PdfChunk is a tab Chunk. * @return true if this chunk is a separator. * @since 2.1.2 */ boolean isTab() { return isAttribute(Chunk.TAB); } /** * Correction for the tab position based on the left starting position. * @param newValue the new value for the left X. * @since 2.1.2 */ void adjustLeft(final float newValue) { final Object[] o = (Object[])this.attributes.get(Chunk.TAB); if (o != null) { this.attributes.put(Chunk.TAB, new Object[]{o[0], o[1], o[2], Float.valueOf(newValue)}); } } /** * Checks if there is an image in the PdfChunk. * @return true if an image is present */ boolean isImage() { return this.image != null; } /** * Gets the image in the PdfChunk. * @return the image or null */ Image getImage() { return this.image; } /** * Gets the image offset in the x direction * @return the image offset in the x direction */ float getImageOffsetX() { return this.offsetX; } /** * Gets the image offset in the y direction * @return Gets the image offset in the y direction */ float getImageOffsetY() { return this.offsetY; } /** * sets the value. * @param value content of the Chunk */ void setValue(final String value) { this.value = value; } /** * @see java.lang.Object#toString() */ @Override public String toString() { return this.value; } /** * Tells you if this string is in Chinese, Japanese, Korean or Identity-H. * @return true if the Chunk has a special encoding */ boolean isSpecialEncoding() { return this.encoding.equals(CJKFont.CJK_ENCODING) || this.encoding.equals(BaseFont.IDENTITY_H); } int length() { return this.value.length(); } int lengthUtf32() { if (!BaseFont.IDENTITY_H.equals(this.encoding)) { return this.value.length(); } int total = 0; final int len = this.value.length(); for (int k = 0; k < len; ++k) { if (Utilities.isSurrogateHigh(this.value.charAt(k))) { ++k; } ++total; } return total; } boolean isExtSplitCharacter(final int start, final int current, final int end, final char[] cc, final PdfChunk[] ck) { return this.splitCharacter.isSplitCharacter(start, current, end, cc, ck); } /** * Removes all the ' ' and '-'-characters on the right of a String. *

* @param string the String that has to be trimmed. * @return the trimmed String */ private String trim(String string) { final BaseFont ft = this.font.getFont(); if (ft.getFontType() == BaseFont.FONT_TYPE_CJK && ft.getUnicodeEquivalent(' ') != ' ') { while (string.endsWith("\u0001")) { string = string.substring(0, string.length() - 1); } } else { while (string.endsWith(" ") || string.endsWith("\t")) { string = string.substring(0, string.length() - 1); } } return string; } float getCharWidth(final int c) { if (noPrint(c)) { return 0; } return this.font.width(c); } static boolean noPrint(final int c) { return c >= 0x200b && c <= 0x200f || c >= 0x202a && c <= 0x202e; } }