CharacterRenderInfo.java
/*
This file is part of the iText (R) project.
Copyright (c) 1998-2025 Apryse Group NV
Authors: Apryse Software.
This program is offered under a commercial and under the AGPL license.
For commercial licensing, contact us at https://itextpdf.com/sales. For AGPL licensing, see below.
AGPL licensing:
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
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 <https://www.gnu.org/licenses/>.
*/
package com.itextpdf.kernel.pdf.canvas.parser.listener;
import com.itextpdf.kernel.geom.LineSegment;
import com.itextpdf.kernel.geom.Point;
import com.itextpdf.kernel.geom.Rectangle;
import com.itextpdf.kernel.pdf.canvas.parser.data.TextRenderInfo;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* This class represents a single character and its bounding box
*/
public class CharacterRenderInfo extends TextChunk {
private Rectangle boundingBox;
/**
* This method converts a {@link List} of {@link CharacterRenderInfo}.
* The returned data structure contains both the plaintext
* and the mapping of indices (from the list to the string).
* These indices can differ; if there is sufficient spacing between two CharacterRenderInfo
* objects, this algorithm will decide to insert space. The inserted space will cause
* the indices to differ by at least 1.
*/
static StringConversionInfo mapString(List<CharacterRenderInfo> cris) {
Map<Integer, Integer> indexMap = new HashMap<>();
StringBuilder sb = new StringBuilder();
CharacterRenderInfo lastChunk = null;
for (int i = 0; i < cris.size(); i++) {
CharacterRenderInfo chunk = cris.get(i);
if (lastChunk == null) {
putCharsWithIndex(chunk.getText(), i, indexMap, sb);
} else {
if (chunk.sameLine(lastChunk)) {
// we only insert a blank space if the trailing character of the previous string wasn't a space, and the leading character of the current string isn't a space
if (chunk.getLocation().isAtWordBoundary(lastChunk.getLocation()) && !chunk.getText().startsWith(" ") && !chunk.getText().endsWith(" ")) {
sb.append(' ');
}
putCharsWithIndex(chunk.getText(), i, indexMap, sb);
} else {
// we insert a newline character in the resulting string if the chunks are placed on different lines
sb.append('\n');
putCharsWithIndex(chunk.getText(), i, indexMap, sb);
}
}
lastChunk = chunk;
}
CharacterRenderInfo.StringConversionInfo ret = new StringConversionInfo();
ret.indexMap = indexMap;
ret.text = sb.toString();
return ret;
}
private static void putCharsWithIndex(final CharSequence seq, int index, final Map<Integer, Integer> indexMap, StringBuilder sb) {
int charCount = seq.length();
for (int i = 0; i < charCount; i++) {
indexMap.put(sb.length(), index);
sb.append(seq.charAt(i));
}
}
public CharacterRenderInfo(TextRenderInfo tri) {
super(tri == null ? "" : tri.getText(), tri == null ? null : getLocation(tri));
if (tri == null)
throw new IllegalArgumentException("TextRenderInfo argument is not nullable.");
// determine bounding box
List<Point> points = new ArrayList<>();
points.add(new Point(tri.getDescentLine().getStartPoint().get(0),tri.getDescentLine().getStartPoint().get(1)));
points.add(new Point(tri.getDescentLine().getEndPoint().get(0),tri.getDescentLine().getEndPoint().get(1)));
points.add(new Point(tri.getAscentLine().getStartPoint().get(0),tri.getAscentLine().getStartPoint().get(1)));
points.add(new Point(tri.getAscentLine().getEndPoint().get(0),tri.getAscentLine().getEndPoint().get(1)));
this.boundingBox = Rectangle.calculateBBox(points);
}
public Rectangle getBoundingBox() {
return boundingBox;
}
private static ITextChunkLocation getLocation(TextRenderInfo tri) {
LineSegment baseline = tri.getBaseline();
return new TextChunkLocationDefaultImp(baseline.getStartPoint(),
baseline.getEndPoint(),
tri.getSingleSpaceWidth());
}
static class StringConversionInfo {
Map<Integer, Integer> indexMap;
String text;
}
}