com.lowagie.text.pdf.PdfChunk Maven / Gradle / Ivy
/*
* $Id: PdfChunk.java 4092 2009-11-11 17:58:16Z psoares33 $
*
* 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:
* https://github.com/LibrePDF/OpenPDF
*/
package com.lowagie.text.pdf;
import com.lowagie.text.Chunk;
import com.lowagie.text.Font;
import com.lowagie.text.Image;
import com.lowagie.text.SplitCharacter;
import com.lowagie.text.Utilities;
import java.awt.Color;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
/**
* A PdfChunk
is the PDF translation of a Chunk
.
*
* A PdfChunk
is a PdfString
in a certain
* PdfFont
and Color
.
*
* @see PdfString
* @see com.lowagie.text.Chunk
* @see com.lowagie.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 Map keysAttributes = new HashMap<>();
/** The allowed attributes in variable noStroke
. */
private static final Map 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);
keysAttributes.put(Chunk.CHAR_SPACING, 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. */
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 Map attributes = new HashMap<>();
/**
* Non metric attributes.
*
* This attributes do not require the measurement of characters widths when rendering
* such as Color.
*/
protected Map 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;
/** 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;
// 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) {
thisChunk[0] = this;
value = string;
this.font = other.font;
this.attributes = other.attributes;
this.noStroke = other.noStroke;
this.baseFont = other.baseFont;
Object[] obj = (Object[]) attributes.get(Chunk.IMAGE);
if (obj == null)
image = null;
else {
image = (Image)obj[0];
offsetX = (Float) obj[1];
offsetY = (Float) obj[2];
changeLeading = (Boolean) obj[3];
}
encoding = font.getFont().getEncoding();
splitCharacter = (SplitCharacter)noStroke.get(Chunk.SPLITCHARACTER);
if (splitCharacter == null)
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(Chunk chunk, PdfAction action) {
thisChunk[0] = this;
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) {
/*
Change baseFont set behaviour for non text PdfChunk content to be compliant with PDF/A.
Use case : Convert docx file with non text content (non printable content or draw object).
This type of content has no font, the chunk is set with default Helvetica basefont.
Helvetica BaseFont cannot be embedded, the result won't never be compliant.
Proposal: Use Liberation Sans instead of Helvetica
*/
// Check if the chunk content is text
if (chunk.getContent().chars().allMatch(c -> ((c >= 0x20 && c <= 0xFF) || c == 0x09))) {
// translation of the font-family to a PDF font-family
baseFont = f.getCalculatedBaseFont(false);
} else {
// translation to the embeddable free font
try {
baseFont = BaseFont.createFont("font-fallback/LiberationSans-Regular.ttf",
BaseFont.IDENTITY_H, BaseFont.EMBEDDED);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
else {
// bold simulation
if ((style & Font.BOLD) != 0)
attributes.put(Chunk.TEXTRENDERMODE, new Object[]{PdfContentByte.TEXT_RENDER_MODE_FILL_STROKE, 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
Map attr = chunk.getChunkAttributes();
if (attr != null) {
for (Map.Entry entry : attr.entrySet()) {
String name = entry.getKey();
if (keysAttributes.containsKey(name)) {
attributes.put(name, entry.getValue());
} else if (keysNoStroke.containsKey(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, 1f / 15, 0, -1f / 3, 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());
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];
offsetY = (Float) obj[2];
changeLeading = (Boolean) obj[3];
}
font.setImage(image);
Float hs = (Float)attributes.get(Chunk.HSCALE);
if (hs != null)
font.setHorizontalScaling(hs);
encoding = font.getFont().getEncoding();
splitCharacter = (SplitCharacter)noStroke.get(Chunk.SPLITCHARACTER);
if (splitCharacter == null)
splitCharacter = DefaultSplitCharacter.DEFAULT;
}
// methods
/**
* Gets the Unicode equivalent to a CID.
* The (nonexistent) CID FF00
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";
}
return new PdfChunk(returnValue, this);
}
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, 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') {
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 = " ";
}
return new PdfChunk(returnValue, this);
}
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 = "";
return new PdfChunk(returnValue, this);
}
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);
return new PdfChunk(returnValue, this);
}
}
}
String returnValue = value.substring(splitPosition);
value = trim(value.substring(0, splitPosition));
return new PdfChunk(returnValue, this);
}
/**
* 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) {
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);
return new PdfChunk(returnValue, this);
}
// loop over all the characters of a string
// or until the totalWidth is reached
int length = value.length();
boolean surrogate = false;
char character;
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);
return new PdfChunk(returnValue, this);
}
// 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 Color
*/
Color color() {
return (Color)noStroke.get(Chunk.COLOR);
}
/**
* Returns the width of this PdfChunk
.
*
* @return a width
*/
float width() {
if (isAttribute(Chunk.TAB))
return 0.0f;
if (isAttribute(Chunk.CHAR_SPACING)) {
Float cs = (Float) getAttribute(Chunk.CHAR_SPACING);
return font.width(value) + value.length() * cs;
}
return font.width(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 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 width() + (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;
}
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];
}
return false;
}
/**
* Checks if this PdfChunk
is a vertical Separator Chunk.
* @return true if this chunk is a vertical separator.
* @since OpenPDF
*/
boolean isVerticalSeparator() {
if (isAttribute(Chunk.SEPARATOR)) {
Object[] o = (Object[])getAttribute(Chunk.SEPARATOR);
return (Boolean) o[1];
}
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(float newValue) {
Object[] o = (Object[])attributes.get(Chunk.TAB);
if (o != null) {
attributes.put(Chunk.TAB, new Object[]{o[0], o[1], o[2], newValue});
}
}
/**
* 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;
}
/**
* 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()
*/
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;
}
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;
}
return font.width(c);
}
public static boolean noPrint(int c) {
return ((c >= 0x200b && c <= 0x200f) || (c >= 0x202a && c <= 0x202e));
}
}