GlyphBboxCalculationTest.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;
import com.itextpdf.kernel.geom.Rectangle;
import com.itextpdf.kernel.geom.Vector;
import com.itextpdf.kernel.pdf.PdfDocument;
import com.itextpdf.kernel.pdf.PdfPage;
import com.itextpdf.kernel.pdf.PdfReader;
import com.itextpdf.kernel.pdf.canvas.PdfCanvas;
import com.itextpdf.kernel.pdf.canvas.parser.data.IEventData;
import com.itextpdf.kernel.pdf.canvas.parser.data.TextRenderInfo;
import com.itextpdf.kernel.pdf.canvas.parser.listener.IEventListener;
import com.itextpdf.kernel.pdf.canvas.parser.listener.ITextExtractionStrategy;
import com.itextpdf.kernel.utils.CompareTool;
import com.itextpdf.test.ExtendedITextTest;
import com.itextpdf.test.TestUtil;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
@Tag("IntegrationTest")
public class GlyphBboxCalculationTest extends ExtendedITextTest {
private static final String sourceFolder = "./src/test/resources/com/itextpdf/kernel/pdf/canvas/parser/GlyphBboxCalculationTest/";
private static final String destinationFolder = TestUtil.getOutputPath() + "/kernel/pdf/canvas/parser/GlyphBboxCalculationTest/";
@BeforeAll
public static void beforeClass() {
createOrClearDestinationFolder(destinationFolder);
}
@AfterAll
public static void afterClass() {
CompareTool.cleanup(destinationFolder);
}
@Test
public void checkBboxCalculationForType3FontsWithFontMatrix01() throws IOException {
String inputPdf = sourceFolder + "checkBboxCalculationForType3FontsWithFontMatrix01.pdf";
PdfDocument pdfDocument = new PdfDocument(new PdfReader(inputPdf));
CharacterPositionEventListener listener = new CharacterPositionEventListener();
PdfCanvasProcessor processor = new PdfCanvasProcessor(listener);
processor.processPageContent(pdfDocument.getPage(1));
// font size (36) * |fontMatrix| (0.001) * glyph width (600) = 21.6
Assertions.assertEquals(21.6, listener.glyphWidth, 1e-5);
}
@Test
public void checkBboxCalculationForType3FontsWithFontMatrix02() throws IOException {
String inputPdf = sourceFolder + "checkBboxCalculationForType3FontsWithFontMatrix02.pdf";
PdfDocument pdfDocument = new PdfDocument(new PdfReader(inputPdf));
CharacterPositionEventListener listener = new CharacterPositionEventListener();
PdfCanvasProcessor processor = new PdfCanvasProcessor(listener);
processor.processPageContent(pdfDocument.getPage(1));
// font size (36) * |fontMatrix| (1) * glyph width (0.6) = 21.6
Assertions.assertEquals(21.6, listener.glyphWidth, 1e-5);
}
@Test
public void checkAverageBboxCalculationForType3FontsWithFontMatrix01Test() throws IOException {
String inputPdf = sourceFolder + "checkAverageBboxCalculationForType3FontsWithFontMatrix01.pdf";
PdfDocument pdfDocument = new PdfDocument(new PdfReader(inputPdf));
CharacterPositionEventListener listener = new CharacterPositionEventListener();
PdfCanvasProcessor processor = new PdfCanvasProcessor(listener);
processor.processPageContent(pdfDocument.getPage(1));
Assertions.assertEquals(600, listener.firstTextRenderInfo.getFont().getFontProgram().getAvgWidth(), 0.01f);
}
@Test
public void type3FontsWithIdentityFontMatrixAndMultiplier() throws IOException, InterruptedException {
String inputPdf = sourceFolder + "type3FontsWithIdentityFontMatrixAndMultiplier.pdf";
String outputPdf = destinationFolder + "type3FontsWithIdentityFontMatrixAndMultiplier.pdf";
PdfDocument pdfDocument = new PdfDocument(new PdfReader(inputPdf), CompareTool.createTestPdfWriter(outputPdf));
CharacterPositionEventListener listener = new CharacterPositionEventListener();
PdfCanvasProcessor processor = new PdfCanvasProcessor(listener);
processor.processPageContent(pdfDocument.getPage(1));
PdfPage page = pdfDocument.getPage(1);
Rectangle pageSize = page.getPageSize();
PdfCanvas pdfCanvas = new PdfCanvas(page);
pdfCanvas.beginText().setFontAndSize(processor.getGraphicsState().getFont(), processor.getGraphicsState().getFontSize())
.moveText(pageSize.getWidth() / 2 - 24, pageSize.getHeight() / 2)
.showText("A")
.endText();
pdfDocument.close();
Assertions.assertNull(new CompareTool().compareByContent(outputPdf, sourceFolder + "cmp_type3FontsWithIdentityFontMatrixAndMultiplier.pdf", destinationFolder, "diff_"));
}
@Test
public void type3FontCustomFontMatrixAndFontBBoxTest() throws IOException {
String inputPdf = sourceFolder + "type3FontCustomFontMatrixAndFontBBox.pdf";
// Resultant rectangle is expected to be a bounding box over the text on the page.
Rectangle expectedRectangle = new Rectangle(10f, 97.84f, 14.400002f, 8.880005f);
List<Rectangle> actualRectangles;
try (PdfDocument pdfDoc = new PdfDocument(new PdfReader(inputPdf))) {
TextBBoxEventListener eventListener = new TextBBoxEventListener();
PdfCanvasProcessor canvasProcessor = new PdfCanvasProcessor(eventListener);
PdfPage page = pdfDoc.getPage(1);
canvasProcessor.processPageContent(page);
actualRectangles = eventListener.getRectangles();
}
Assertions.assertEquals(1, actualRectangles.size());
Assertions.assertTrue(expectedRectangle.equalsWithEpsilon(actualRectangles.get(0)));
}
private static class CharacterPositionEventListener implements ITextExtractionStrategy {
float glyphWidth;
TextRenderInfo firstTextRenderInfo;
@Override
public String getResultantText() {
return null;
}
@Override
public void eventOccurred(IEventData data, EventType type) {
if (type.equals(EventType.RENDER_TEXT)) {
TextRenderInfo renderInfo = (TextRenderInfo) data;
if (firstTextRenderInfo == null) {
firstTextRenderInfo = renderInfo;
firstTextRenderInfo.preserveGraphicsState();
}
List<TextRenderInfo> subs = renderInfo.getCharacterRenderInfos();
for (int i = 0; i < subs.size(); i++) {
TextRenderInfo charInfo = subs.get(i);
glyphWidth = charInfo.getBaseline().getLength();
}
}
}
@Override
public Set<EventType> getSupportedEvents() {
return new LinkedHashSet<>(Collections.singletonList(EventType.RENDER_TEXT));
}
}
private static class TextBBoxEventListener implements IEventListener {
private final List<Rectangle> rectangles = new ArrayList<>();
public List<Rectangle> getRectangles() {
return rectangles;
}
@Override
public void eventOccurred(IEventData data, EventType type) {
if (EventType.RENDER_TEXT.equals(type)) {
TextRenderInfo renderInfo = (TextRenderInfo) data;
Vector startPoint = renderInfo.getDescentLine().getStartPoint();
Vector endPoint = renderInfo.getAscentLine().getEndPoint();
float x1 = Math.min(startPoint.get(0), endPoint.get(0));
float x2 = Math.max(startPoint.get(0), endPoint.get(0));
float y1 = Math.min(startPoint.get(1), endPoint.get(1));
float y2 = Math.max(startPoint.get(1), endPoint.get(1));
rectangles.add(new Rectangle(x1, y1, x2 - x1, y2 - y1));
}
}
@Override
public Set<EventType> getSupportedEvents() {
return null;
}
}
}