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

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

/*
 * $Id: fee2ea52b0c97e488c125f0205768790902070bd $
 *
 * This file is part of the iText (R) project.
 * Copyright (c) 1998-2016 iText Group NV
 * Authors: Bruno Lowagie, Paulo Soares, et al.
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License version 3
 * as published by the Free Software Foundation with the addition of the
 * following permission added to Section 15 as permitted in Section 7(a):
 * FOR ANY PART OF THE COVERED WORK IN WHICH THE COPYRIGHT IS OWNED BY
 * ITEXT GROUP. ITEXT GROUP DISCLAIMS THE WARRANTY OF NON INFRINGEMENT
 * OF THIRD PARTY RIGHTS
 *
 * This program 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 Affero General Public License for more details.
 * You should have received a copy of the GNU Affero General Public License
 * along with this program; if not, see http://www.gnu.org/licenses or write to
 * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
 * Boston, MA, 02110-1301 USA, or download the license from the following URL:
 * http://itextpdf.com/terms-of-use/
 *
 * The interactive user interfaces in modified source and object code versions
 * of this program must display Appropriate Legal Notices, as required under
 * Section 5 of the GNU Affero General Public License.
 *
 * In accordance with Section 7(b) of the GNU Affero General Public License,
 * a covered work must retain the producer line in every PDF that is created
 * or manipulated using iText.
 *
 * You can be released from the requirements of the license by purchasing
 * a commercial license. Buying such a license is mandatory as soon as you
 * develop commercial activities involving the iText software without
 * disclosing the source code of your own applications.
 * These activities include: offering paid services to customers as an ASP,
 * serving PDFs on the fly in a web application, shipping iText with a closed
 * source product.
 *
 * For more information, please contact iText Software Corp. at this
 * address: [email protected]
 */
package com.itextpdf.text.pdf;

import com.itextpdf.text.*;
import com.itextpdf.text.pdf.interfaces.IAccessibleElement;

import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;

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

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

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

* This attributes do not require the measurement of characters widths when rendering * such as BaseColor. */ protected HashMap noStroke = new HashMap(); /** true if the chunk split was cause by a newline. */ protected boolean newlineSplit; /** The image in this PdfChunk, if it has one */ protected Image image; protected float imageScalePercentage = 1.0f; /** The offset in the x direction for the image */ protected float offsetX; /** The offset in the y direction for the image */ protected float offsetY; /** Indicates if the height and offset of the Image has to be taken into account */ protected boolean changeLeading = false; /** The leading that can overrule the existing leading. */ protected float leading = 0; protected IAccessibleElement accessibleElement = null; public static final float UNDERLINE_THICKNESS = 1f / 15; public static final float UNDERLINE_OFFSET = -1f / 3; // 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(String string, PdfChunk other) { value = string; this.font = other.font; this.attributes = other.attributes; this.noStroke = other.noStroke; this.baseFont = other.baseFont; this.changeLeading = other.changeLeading; this.leading = other.leading; Object obj[] = (Object[])attributes.get(Chunk.IMAGE); if (obj == null) image = null; else { image = (Image)obj[0]; offsetX = ((Float)obj[1]).floatValue(); offsetY = ((Float)obj[2]).floatValue(); changeLeading = ((Boolean)obj[3]).booleanValue(); } encoding = font.getFont().getEncoding(); splitCharacter = (SplitCharacter)noStroke.get(Chunk.SPLITCHARACTER); if (splitCharacter == null) splitCharacter = DefaultSplitCharacter.DEFAULT; accessibleElement = other.accessibleElement; } /** * Constructs a PdfChunk-object. * * @param chunk the original Chunk-object * @param action the PdfAction if the Chunk comes from an Anchor */ PdfChunk(Chunk chunk, PdfAction action) { value = chunk.getContent(); Font f = chunk.getFont(); float size = f.getSize(); if (size == Font.UNDEFINED) size = 12; baseFont = f.getBaseFont(); int style = f.getStyle(); if (style == Font.UNDEFINED) { style = Font.NORMAL; } if (baseFont == null) { // translation of the font-family to a PDF font-family baseFont = f.getCalculatedBaseFont(false); } else { // bold simulation if ((style & Font.BOLD) != 0) attributes.put(Chunk.TEXTRENDERMODE, new Object[]{Integer.valueOf(PdfContentByte.TEXT_RENDER_MODE_FILL_STROKE), new Float(size / 30f), null}); // italic simulation if ((style & Font.ITALIC) != 0) attributes.put(Chunk.SKEW, new float[]{0, ITALIC_ANGLE}); } font = new PdfFont(baseFont, size); // other style possibilities HashMap attr = chunk.getAttributes(); if (attr != null) { for (Map.Entryentry: attr.entrySet()) { String name = entry.getKey(); if (keysAttributes.contains(name)) { attributes.put(name, entry.getValue()); } else if (keysNoStroke.contains(name)) { noStroke.put(name, entry.getValue()); } } if ("".equals(attr.get(Chunk.GENERICTAG))) { attributes.put(Chunk.GENERICTAG, chunk.getContent()); } } if (f.isUnderlined()) { Object obj[] = {null, new float[]{0, UNDERLINE_THICKNESS, 0, UNDERLINE_OFFSET, 0}}; Object unders[][] = Utilities.addToArray((Object[][])attributes.get(Chunk.UNDERLINE), obj); attributes.put(Chunk.UNDERLINE, unders); } if (f.isStrikethru()) { Object obj[] = {null, new float[]{0, 1f / 15, 0, 1f / 3, 0}}; Object unders[][] = Utilities.addToArray((Object[][])attributes.get(Chunk.UNDERLINE), obj); attributes.put(Chunk.UNDERLINE, unders); } if (action != null) attributes.put(Chunk.ACTION, action); // the color can't be stored in a PdfFont noStroke.put(Chunk.COLOR, f.getColor()); noStroke.put(Chunk.ENCODING, font.getFont().getEncoding()); Float lh = (Float)attributes.get(Chunk.LINEHEIGHT); if (lh != null) { changeLeading = true; leading = lh; } Object[] obj = (Object[])attributes.get(Chunk.IMAGE); if (obj == null) { image = null; } else { attributes.remove(Chunk.HSCALE); // images are scaled in other ways image = (Image)obj[0]; offsetX = ((Float)obj[1]).floatValue(); offsetY = ((Float)obj[2]).floatValue(); changeLeading = ((Boolean)obj[3]).booleanValue(); } Float hs = (Float)attributes.get(Chunk.HSCALE); if (hs != null) font.setHorizontalScaling(hs.floatValue()); encoding = font.getFont().getEncoding(); splitCharacter = (SplitCharacter)noStroke.get(Chunk.SPLITCHARACTER); if (splitCharacter == null) splitCharacter = DefaultSplitCharacter.DEFAULT; accessibleElement = chunk; } /** * Constructs a PdfChunk-object. * * @param chunk the original Chunk-object * @param action the PdfAction if the Chunk comes from an Anchor * @param tabSettings the Phrase tab settings */ PdfChunk(Chunk chunk, PdfAction action, TabSettings tabSettings) { this(chunk, action); if (tabSettings != null && attributes.get(Chunk.TABSETTINGS) == null) attributes.put(Chunk.TABSETTINGS, tabSettings); } // 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 */ public int getUnicodeEquivalent(int c) { return baseFont.getUnicodeEquivalent(c); } protected int getWord(String text, int start) { 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(float width) { newlineSplit = false; if (image != null) { if (image.getScaledWidth() > width) { PdfChunk pc = new PdfChunk(Chunk.OBJECT_REPLACEMENT_CHARACTER, this); value = ""; attributes = new HashMap(); image = null; font = PdfFont.getDefaultFont(); return pc; } else return null; } HyphenationEvent hyphenationEvent = (HyphenationEvent)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; int length = value.length(); char valueArray[] = value.toCharArray(); char character = 0; BaseFont ft = 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 char cidChar = valueArray[currentPosition]; character = (char)ft.getUnicodeEquivalent(cidChar); // if a newLine or carriageReturn is encountered if (character == '\n') { newlineSplit = true; String returnValue = value.substring(currentPosition + 1); value = value.substring(0, currentPosition); if (value.length() < 1) { value = "\u0001"; } PdfChunk pc = new PdfChunk(returnValue, this); return pc; } currentWidth += getCharWidth(cidChar); if (character == ' ') { lastSpace = currentPosition + 1; lastSpaceWidth = currentWidth; } if (currentWidth > width) break; // if a split-character is encountered, the splitPosition is altered if (splitCharacter.isSplitCharacter(0, currentPosition, length, valueArray, new PdfChunk[] {this})) 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') { newlineSplit = true; int inc = 1; if (character == '\r' && currentPosition + 1 < length && valueArray[currentPosition + 1] == '\n') inc = 2; String returnValue = value.substring(currentPosition + inc); value = value.substring(0, currentPosition); if (value.length() < 1) { value = " "; } PdfChunk pc = new PdfChunk(returnValue, this); return pc; } surrogate = Utilities.isSurrogatePair(valueArray, currentPosition); if (surrogate) currentWidth += getCharWidth(Utilities.convertToUtf32(valueArray[currentPosition], valueArray[currentPosition + 1])); else currentWidth += getCharWidth(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 (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) { String returnValue = value; value = ""; PdfChunk pc = new PdfChunk(returnValue, this); return pc; } if (lastSpace > splitPosition && splitCharacter.isSplitCharacter(0, 0, 1, singleSpace, null)) splitPosition = lastSpace; if (hyphenationEvent != null && lastSpace >= 0 && lastSpace < currentPosition) { int wordIdx = getWord(value, lastSpace); if (wordIdx > lastSpace) { String pre = hyphenationEvent.getHyphenatedWordPre(value.substring(lastSpace, wordIdx), font.getFont(), font.size(), width - lastSpaceWidth); String post = hyphenationEvent.getHyphenatedWordPost(); if (pre.length() > 0) { String returnValue = post + value.substring(wordIdx); value = trim(value.substring(0, lastSpace) + pre); PdfChunk pc = new PdfChunk(returnValue, this); return pc; } } } String returnValue = value.substring(splitPosition); value = trim(value.substring(0, splitPosition)); 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(float width) { if (image != null) { if (image.getScaledWidth() > width) { // Image does not fit the line, resize if requested if (image.isScaleToFitLineWhenOverflow()) { //float scalePercent = width / image.getWidth() * 100; //image.scalePercent(scalePercent); this.setImageScalePercentage(width / image.getWidth()); return null; } PdfChunk pc = new PdfChunk("", this); value = ""; attributes.remove(Chunk.IMAGE); image = null; 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 < font.width()) { String returnValue = value.substring(1); value = value.substring(0, 1); PdfChunk pc = new PdfChunk(returnValue, this); return pc; } // loop over all the characters of a string // or until the totalWidth is reached int length = value.length(); boolean surrogate = false; while (currentPosition < length) { // the width of every character is added to the currentWidth surrogate = Utilities.isSurrogatePair(value, currentPosition); if (surrogate) currentWidth += getCharWidth(Utilities.convertToUtf32(value, currentPosition)); else currentWidth += getCharWidth(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; } String returnValue = value.substring(currentPosition); value = value.substring(0, currentPosition); 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 font; } /** * Returns the color of this Chunk. * * @return a BaseColor */ BaseColor color() { return (BaseColor)noStroke.get(Chunk.COLOR); } /** * Returns the width of this PdfChunk. * * @return a width */ float width() { return width(value); } float width(String str) { if (isAttribute(Chunk.SEPARATOR)) { return 0; } if (isImage()) { return getImageWidth(); } float width = font.width(str); if (isAttribute(Chunk.CHAR_SPACING)) { Float cs = (Float) getAttribute(Chunk.CHAR_SPACING); width += str.length() * cs.floatValue(); } if (isAttribute(Chunk.WORD_SPACING)) { int numberOfSpaces = 0; int idx = -1; while ((idx = str.indexOf(' ', idx + 1)) >= 0) ++numberOfSpaces; Float ws = (Float) getAttribute(Chunk.WORD_SPACING); width += numberOfSpaces * ws; } return width; } float height() { if (isImage()) { return getImageHeight(); } else { return font.size(); } } /** * 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 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 */ public float getWidthCorrected(float charSpacing, float wordSpacing) { if (image != null) { return image.getScaledWidth() + charSpacing; } int numberOfSpaces = 0; int idx = -1; while ((idx = value.indexOf(' ', idx + 1)) >= 0) ++numberOfSpaces; return font.width(value) + value.length() * charSpacing + numberOfSpaces * wordSpacing; } /** * Gets the text displacement relative to the baseline. * @return a displacement in points */ public float getTextRise() { 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 */ public float trimLastSpace() { BaseFont ft = font.getFont(); if (ft.getFontType() == BaseFont.FONT_TYPE_CJK && ft.getUnicodeEquivalent(' ') != ' ') { if (value.length() > 1 && value.endsWith("\u0001")) { value = value.substring(0, value.length() - 1); return font.width('\u0001'); } } else { if (value.length() > 1 && value.endsWith(" ")) { value = value.substring(0, value.length() - 1); return font.width(' '); } } return 0; } public float trimFirstSpace() { BaseFont ft = font.getFont(); if (ft.getFontType() == BaseFont.FONT_TYPE_CJK && ft.getUnicodeEquivalent(' ') != ' ') { if (value.length() > 1 && value.startsWith("\u0001")) { value = value.substring(1); return font.width('\u0001'); } } else { if (value.length() > 1 && value.startsWith(" ")) { value = value.substring(1); return 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(String name) { if (attributes.containsKey(name)) return attributes.get(name); return noStroke.get(name); } /** *Checks if the attribute exists. * @param name the attribute key * @return true if the attribute exists */ boolean isAttribute(String name) { if (attributes.containsKey(name)) return true; return 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 !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)) { 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 */ @Deprecated void adjustLeft(float newValue) { Object[] o = (Object[])attributes.get(Chunk.TAB); if (o != null) { attributes.put(Chunk.TAB, new Object[]{o[0], o[1], o[2], new Float(newValue)}); } } static TabStop getTabStop(PdfChunk tab, float tabPosition) { TabStop tabStop = null; Object[] o = (Object[])tab.attributes.get(Chunk.TAB); if (o != null) { Float tabInterval = (Float) o[0]; if (Float.isNaN(tabInterval)) { tabStop = TabSettings.getTabStopNewInstance(tabPosition, (TabSettings) tab.attributes.get(Chunk.TABSETTINGS)); } else { tabStop = TabStop.newInstance(tabPosition, tabInterval); } } return tabStop; } TabStop getTabStop() { return (TabStop)attributes.get(TABSTOP); } void setTabStop(TabStop tabStop) { attributes.put(TABSTOP, tabStop); } /** * Checks if there is an image in the PdfChunk. * @return true if an image is present */ boolean isImage() { return image != null; } /** * Gets the image in the PdfChunk. * @return the image or null */ Image getImage() { return image; } float getImageHeight() { return image.getScaledHeight() * imageScalePercentage; } float getImageWidth() { return image.getScaledWidth() * imageScalePercentage; } /** * Returns a scalePercentage in case the image needs to be scaled. * @return the imageScalePercentage */ public float getImageScalePercentage() { return imageScalePercentage; } /** * Sets a scale percentage in case the image needs to be scaled. * @param imageScalePercentage the imageScalePercentage to set */ public void setImageScalePercentage(float imageScalePercentage) { this.imageScalePercentage = imageScalePercentage; } /** * Sets the image offset in the x direction * @param offsetX the image offset in the x direction */ void setImageOffsetX(float offsetX) { this.offsetX = offsetX; } /** * Gets the image offset in the x direction * @return the image offset in the x direction */ float getImageOffsetX() { return offsetX; } /** * Sets the image offset in the y direction * @param offsetY the image offset in the y direction */ void setImageOffsetY(float offsetY) { this.offsetY = offsetY; } /** * Gets the image offset in the y direction * @return Gets the image offset in the y direction */ float getImageOffsetY() { return offsetY; } /** * sets the value. * @param value content of the Chunk */ void setValue(String value) { this.value = value; } /** * @see java.lang.Object#toString() */ @Override public String toString() { return 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 encoding.equals(CJKFont.CJK_ENCODING) || encoding.equals(BaseFont.IDENTITY_H); } /** * Gets the encoding of this string. * * @return a String */ String getEncoding() { return encoding; } int length() { return value.length(); } int lengthUtf32() { if (!BaseFont.IDENTITY_H.equals(encoding)) return value.length(); int total = 0; int len = value.length(); for (int k = 0; k < len; ++k) { if (Utilities.isSurrogateHigh(value.charAt(k))) ++k; ++total; } return total; } boolean isExtSplitCharacter(int start, int current, int end, char[] cc, PdfChunk[] ck) { return 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 */ String trim(String string) { BaseFont ft = 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; } public boolean changeLeading() { return changeLeading; } public float getLeading() { return leading; } float getCharWidth(int c) { if (noPrint(c)) return 0; if (isAttribute(Chunk.CHAR_SPACING)) { Float cs = (Float) getAttribute(Chunk.CHAR_SPACING); return font.width(c) + cs.floatValue() * font.getHorizontalScaling(); } if (isImage()) { return getImageWidth(); } return font.width(c); } public static boolean noPrint(int c) { return c >= 0x200b && c <= 0x200f || c >= 0x202a && c <= 0x202e; } }