PageFlushingTest.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;
import com.itextpdf.commons.utils.MessageFormatUtil;
import com.itextpdf.io.font.constants.StandardFonts;
import com.itextpdf.io.image.ImageDataFactory;
import com.itextpdf.kernel.colors.ColorConstants;
import com.itextpdf.kernel.font.PdfFont;
import com.itextpdf.kernel.font.PdfFontFactory;
import com.itextpdf.kernel.geom.AffineTransform;
import com.itextpdf.kernel.geom.PageSize;
import com.itextpdf.kernel.geom.Rectangle;
import com.itextpdf.kernel.pdf.action.PdfAction;
import com.itextpdf.kernel.pdf.annot.PdfLineAnnotation;
import com.itextpdf.kernel.pdf.annot.PdfLinkAnnotation;
import com.itextpdf.kernel.pdf.canvas.PdfCanvas;
import com.itextpdf.kernel.pdf.canvas.parser.PdfTextExtractor;
import com.itextpdf.kernel.pdf.navigation.PdfExplicitDestination;
import com.itextpdf.kernel.pdf.xobject.PdfImageXObject;
import com.itextpdf.kernel.utils.CompareTool;
import com.itextpdf.kernel.validation.IValidationChecker;
import com.itextpdf.kernel.validation.IValidationContext;
import com.itextpdf.kernel.validation.ValidationContainer;
import com.itextpdf.kernel.validation.ValidationType;
import com.itextpdf.kernel.validation.context.PdfPageValidationContext;
import com.itextpdf.test.ExtendedITextTest;
import com.itextpdf.test.TestUtil;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.Map;
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 PageFlushingTest extends ExtendedITextTest {
private static final String sourceFolder = "./src/test/resources/com/itextpdf/kernel/pdf/PageFlushingTest/";
private static final String destinationFolder = TestUtil.getOutputPath() + "/kernel/pdf/PageFlushingTest/";
@BeforeAll
public static void beforeClass() {
createOrClearDestinationFolder(destinationFolder);
}
@AfterAll
public static void afterClass() {
CompareTool.cleanup(destinationFolder);
}
@Test
public void baseWriting01() throws IOException {
// not all objects are made indirect before closing
int total = 414;
int flushedExpected = 0;
int notReadExpected = 0;
test("baseWriting01.pdf", DocMode.WRITING, FlushMode.NONE, PagesOp.MODIFY,
total, flushedExpected, notReadExpected);
}
@Test
public void pageFlushWriting01() throws IOException {
int total = 715;
int flushedExpected = 400;
int notReadExpected = 0;
test("pageFlushWriting01.pdf", DocMode.WRITING, FlushMode.PAGE_FLUSH, PagesOp.MODIFY,
total, flushedExpected, notReadExpected);
}
@Test
public void unsafeDeepFlushWriting01() throws IOException {
int total = 816;
// 100 still hanging: new font dictionaries on every page shall not be flushed before closing
int flushedExpected = 702;
int notReadExpected = 0;
test("unsafeDeepFlushWriting01.pdf", DocMode.WRITING, FlushMode.UNSAFE_DEEP, PagesOp.MODIFY,
total, flushedExpected, notReadExpected);
}
@Test
public void appendModeFlushWriting01() throws IOException {
int total = 715;
int flushedExpected = 400;
int notReadExpected = 0;
test("appendModeFlushWriting01.pdf", DocMode.WRITING, FlushMode.APPEND_MODE, PagesOp.MODIFY,
total, flushedExpected, notReadExpected);
}
@Test
public void baseReading01() throws IOException {
int total = 817;
int flushedExpected = 0;
// link annots, line annots, actions and images: one hundred of each
int notReadExpected = 402;
test("baseReading01.pdf", DocMode.READING, FlushMode.NONE, PagesOp.READ,
total, flushedExpected, notReadExpected);
}
@Test
public void releaseDeepReading01() throws IOException {
int total = 817;
int flushedExpected = 0;
int notReadExpected = 804;
test("releaseDeepReading01.pdf", DocMode.READING, FlushMode.RELEASE_DEEP, PagesOp.READ,
total, flushedExpected, notReadExpected);
}
@Test
public void baseStamping01() throws IOException {
// not all objects are made indirect before closing
int total = 1618;
int flushedExpected = 0;
int notReadExpected = 603;
test("baseStamping01.pdf", DocMode.STAMPING, FlushMode.NONE, PagesOp.MODIFY,
total, flushedExpected, notReadExpected);
}
@Test
public void pageFlushStamping01() throws IOException {
int total = 2219;
int flushedExpected = 1200;
int notReadExpected = 403;
test("pageFlushStamping01.pdf", DocMode.STAMPING, FlushMode.PAGE_FLUSH, PagesOp.MODIFY,
total, flushedExpected, notReadExpected);
}
@Test
public void unsafeDeepFlushStamping01() throws IOException {
int total = 2420;
// 200 still hanging: new font dictionaries on every page shall not be flushed before closing
int flushedExpected = 1602;
int notReadExpected = 603;
test("unsafeDeepFlushStamping01.pdf", DocMode.STAMPING, FlushMode.UNSAFE_DEEP, PagesOp.MODIFY,
total, flushedExpected, notReadExpected);
}
@Test
public void appendModeFlushStamping01() throws IOException {
int total = 2219;
// 300 less than with page#flush, because of not modified released objects
int flushedExpected = 900;
int notReadExpected = 703;
test("appendModeFlushStamping01.pdf", DocMode.STAMPING, FlushMode.APPEND_MODE, PagesOp.MODIFY,
total, flushedExpected, notReadExpected);
}
@Test
public void releaseDeepStamping01() throws IOException {
int total = 1618;
int flushedExpected = 0;
// new objects cannot be released
int notReadExpected = 703;
test("releaseDeepStamping01.pdf", DocMode.STAMPING, FlushMode.RELEASE_DEEP, PagesOp.MODIFY,
total, flushedExpected, notReadExpected);
}
@Test
public void baseAppendMode01() throws IOException {
int total = 1618;
int flushedExpected = 0;
int notReadExpected = 603;
test("baseAppendMode01.pdf", DocMode.APPEND, FlushMode.NONE, PagesOp.MODIFY,
total, flushedExpected, notReadExpected);
}
@Test
public void pageFlushAppendMode01() throws IOException {
int total = 2219;
int flushedExpected = 900;
int notReadExpected = 403;
test("pageFlushAppendMode01.pdf", DocMode.APPEND, FlushMode.PAGE_FLUSH, PagesOp.MODIFY,
total, flushedExpected, notReadExpected);
}
@Test
public void unsafeDeepFlushAppendMode01() throws IOException {
int total = 2420;
// 200 still hanging: new font dictionaries on every page shall not be flushed before closing
int flushedExpected = 1502;
int notReadExpected = 703;
test("unsafeDeepFlushAppendMode01.pdf", DocMode.APPEND, FlushMode.UNSAFE_DEEP, PagesOp.MODIFY,
total, flushedExpected, notReadExpected);
}
@Test
public void appendModeFlushAppendMode01() throws IOException {
int total = 2219;
// 600 still hanging: every new page contains image, font and action
int flushedExpected = 900;
int notReadExpected = 703;
test("appendModeFlushAppendMode01.pdf", DocMode.APPEND, FlushMode.APPEND_MODE, PagesOp.MODIFY,
total, flushedExpected, notReadExpected);
}
@Test
public void releaseDeepAppendMode01() throws IOException {
int total = 1618;
int flushedExpected = 0;
// new objects cannot be released
int notReadExpected = 703;
test("releaseDeepAppendMode01.pdf", DocMode.APPEND, FlushMode.RELEASE_DEEP, PagesOp.MODIFY,
total, flushedExpected, notReadExpected);
}
@Test
public void baseLightAppendMode01() throws IOException {
int total = 1018;
int flushedExpected = 0;
int notReadExpected = 603;
test("baseLightAppendMode01.pdf", DocMode.APPEND, FlushMode.NONE, PagesOp.MODIFY_LIGHTLY,
total, flushedExpected, notReadExpected);
}
@Test
public void pageFlushLightAppendMode01() throws IOException {
int total = 1318;
int flushedExpected = 500;
// in default PdfPage#flush annotations are always read and attempted to be flushed.
int notReadExpected = 403;
test("pageFlushLightAppendMode01.pdf", DocMode.APPEND, FlushMode.PAGE_FLUSH, PagesOp.MODIFY_LIGHTLY,
total, flushedExpected, notReadExpected);
}
@Test
public void unsafeDeepFlushLightAppendMode01() throws IOException {
int total = 1318;
int flushedExpected = 600;
int notReadExpected = 703;
test("unsafeDeepFlushLightAppendMode01.pdf", DocMode.APPEND, FlushMode.UNSAFE_DEEP, PagesOp.MODIFY_LIGHTLY,
total, flushedExpected, notReadExpected);
}
@Test
public void appendModeFlushLightAppendMode01() throws IOException {
int total = 1318;
// resources are not flushed, here it's font dictionaries for every page which in any case shall not be flushed before closing.
int flushedExpected = 500;
int notReadExpected = 703;
test("appendModeFlushLightAppendMode01.pdf", DocMode.APPEND, FlushMode.APPEND_MODE, PagesOp.MODIFY_LIGHTLY,
total, flushedExpected, notReadExpected);
}
@Test
public void releaseDeepLightAppendMode01() throws IOException {
int total = 1018;
int flushedExpected = 0;
int notReadExpected = 703;
test("releaseDeepLightAppendMode01.pdf", DocMode.APPEND, FlushMode.RELEASE_DEEP, PagesOp.MODIFY_LIGHTLY,
total, flushedExpected, notReadExpected);
}
@Test
public void modifyAnnotationOnlyAppendMode() throws IOException {
String input = sourceFolder + "100pages.pdf";
String output = destinationFolder + "modifyAnnotOnly.pdf";
PdfDocument pdfDoc = new PdfDocument(new PdfReader(input), CompareTool.createTestPdfWriter(output), new StampingProperties().useAppendMode());
PdfPage page = pdfDoc.getPage(1);
PdfIndirectReference pageIndRef = page.getPdfObject().getIndirectReference();
PdfDictionary annotObj = page.getAnnotations().get(0)
.setRectangle(new PdfArray(new Rectangle(0, 0, 300, 300))).setPage(page)
.getPdfObject();
PageFlushingHelper flushingHelper = new PageFlushingHelper(pdfDoc);
flushingHelper.appendModeFlush(1);
// annotation is flushed
Assertions.assertTrue(annotObj.isFlushed());
// page is not flushed
Assertions.assertFalse(pageIndRef.checkState(PdfObject.FLUSHED));
// page is released
Assertions.assertNull(pageIndRef.refersTo);
// exception is not thrown
pdfDoc.close();
}
@Test
public void setLinkDestinationToPageAppendMode() throws IOException {
String input = sourceFolder + "100pages.pdf";
String output = destinationFolder + "setLinkDestinationToPageAppendMode.pdf";
PdfDocument pdfDoc = new PdfDocument(new PdfReader(input), CompareTool.createTestPdfWriter(output), new StampingProperties().useAppendMode());
PdfPage page1 = pdfDoc.getPage(1);
PdfPage page2 = pdfDoc.getPage(2);
PdfIndirectReference page1IndRef = page1.getPdfObject().getIndirectReference();
PdfIndirectReference page2IndRef = page2.getPdfObject().getIndirectReference();
PdfDictionary aDict = ((PdfLinkAnnotation) page1.getAnnotations().get(0)).getAction();
new PdfAction(aDict).put(PdfName.D, PdfExplicitDestination.createXYZ(page2, 300, 400, 1).getPdfObject());
PageFlushingHelper flushingHelper = new PageFlushingHelper(pdfDoc);
flushingHelper.appendModeFlush(2);
flushingHelper.unsafeFlushDeep(1);
// annotation is flushed
Assertions.assertTrue(aDict.isFlushed());
// page is not flushed
Assertions.assertFalse(page1IndRef.checkState(PdfObject.FLUSHED));
// page is released
Assertions.assertNull(page1IndRef.refersTo);
// page is not flushed
Assertions.assertFalse(page2IndRef.checkState(PdfObject.FLUSHED));
// page is released
Assertions.assertNull(page2IndRef.refersTo);
// exception is not thrown
pdfDoc.close();
}
@Test
public void flushSelfContainingObjectsWritingMode() {
PdfDocument pdfDoc = new PdfDocument(new PdfWriter(new ByteArrayOutputStream()));
PdfDictionary pageDict = pdfDoc.addNewPage().getPdfObject();
PdfDictionary dict1 = new PdfDictionary();
pageDict.put(new PdfName("dict1"), dict1);
PdfArray arr1 = new PdfArray();
pageDict.put(new PdfName("arr1"), arr1);
dict1.put(new PdfName("dict1"), dict1);
dict1.put(new PdfName("arr1"), arr1);
arr1.add(arr1);
arr1.add(dict1);
arr1.makeIndirect(pdfDoc);
dict1.makeIndirect(pdfDoc);
PageFlushingHelper flushingHelper = new PageFlushingHelper(pdfDoc);
flushingHelper.unsafeFlushDeep(1);
Assertions.assertTrue(dict1.isFlushed());
Assertions.assertTrue(arr1.isFlushed());
pdfDoc.close();
// exception is not thrown
}
@Test
public void flushingPageResourcesMadeIndependent() throws IOException {
String inputFile = sourceFolder + "100pagesSharedResDict.pdf";
String outputFile = destinationFolder + "flushingPageResourcesMadeIndependent.pdf";
PdfDocument pdf = new PdfDocument(new PdfReader(inputFile), CompareTool.createTestPdfWriter(outputFile));
int numOfAddedXObjectsPerPage = 10;
for (int i = 1; i <= pdf.getNumberOfPages(); ++i) {
PdfPage sourcePage = pdf.getPage(i);
PdfDictionary res = sourcePage.getPdfObject().getAsDictionary(PdfName.Resources);
PdfDictionary resClone = new PdfDictionary();
// clone dictionary manually to ensure this object is direct and is flushed together with the page
for (Map.Entry<PdfName, PdfObject> e : res.entrySet()) {
resClone.put(e.getKey(), e.getValue().clone());
}
sourcePage.getPdfObject().put(PdfName.Resources, resClone);
PdfCanvas pdfCanvas = new PdfCanvas(sourcePage);
pdfCanvas.saveState();
for (int j = 0; j < numOfAddedXObjectsPerPage; ++j) {
PdfImageXObject xObject = new PdfImageXObject(ImageDataFactory.create(sourceFolder + "simple.jpg"));
pdfCanvas.addXObjectFittedIntoRectangle(xObject, new Rectangle(36, 720 - j * 150, 20, 20));
xObject.makeIndirect(pdf).flush();
}
pdfCanvas.restoreState();
pdfCanvas.release();
sourcePage.flush();
}
verifyFlushedObjectsNum(pdf, 1416, 1400, 0);
pdf.close();
printOutputPdfNameAndDir(outputFile);
PdfDocument result = new PdfDocument(CompareTool.createOutputReader(outputFile));
PdfObject page15Res = result.getPage(15).getPdfObject().get(PdfName.Resources, false);
PdfObject page34Res = result.getPage(34).getPdfObject().get(PdfName.Resources, false);
Assertions.assertTrue(page15Res.isDictionary());
Assertions.assertEquals(numOfAddedXObjectsPerPage, ((PdfDictionary)page15Res).getAsDictionary(PdfName.XObject).size());
Assertions.assertTrue(page34Res.isDictionary());
Assertions.assertNotEquals(page15Res, page34Res);
result.close();
}
@Test
public void pageValidationTest() {
try (PdfDocument doc = new PdfDocument(new PdfWriter(new ByteArrayOutputStream()))) {
ValidationContainer container = new ValidationContainer();
CustomValidationChecker checker = new CustomValidationChecker();
container.addChecker(checker);
doc.getDiContainer().register(ValidationContainer.class, container);
Assertions.assertNull(checker.page);
final PdfPage pdfPage = doc.addNewPage();
pdfPage.flush(true);
Assertions.assertSame(pdfPage, checker.page);
}
}
private static class CustomValidationChecker implements IValidationChecker {
public PdfPage page;
@Override
public void validate(IValidationContext validationContext) {
if (validationContext.getType() == ValidationType.PDF_PAGE) {
page = ((PdfPageValidationContext) validationContext).getPage();
}
}
@Override
public boolean isPdfObjectReadyToFlush(PdfObject object) {
return true;
}
}
private static void test(String filename, DocMode docMode, FlushMode flushMode, PagesOp pagesOp,
int total, int flushedExpected, int notReadExpected) throws IOException {
String input = sourceFolder + "100pages.pdf";
String output = destinationFolder + filename;
PdfDocument pdfDoc;
switch (docMode) {
case WRITING:
pdfDoc = new PdfDocument(CompareTool.createTestPdfWriter(output));
break;
case READING:
pdfDoc = new PdfDocument(new PdfReader(input));
break;
case STAMPING:
pdfDoc = new PdfDocument(new PdfReader(input), CompareTool.createTestPdfWriter(output));
break;
case APPEND:
pdfDoc = new PdfDocument(new PdfReader(input), CompareTool.createTestPdfWriter(output), new StampingProperties().useAppendMode());
break;
default:
throw new IllegalStateException();
}
PageFlushingHelper flushingHelper = new PageFlushingHelper(pdfDoc);
PdfFont font = PdfFontFactory.createFont(StandardFonts.HELVETICA);
PdfImageXObject xObject = new PdfImageXObject(ImageDataFactory.create(sourceFolder + "itext.png"));
if (docMode != DocMode.WRITING) {
for (int i = 0; i < 100; ++i) {
PdfPage page = pdfDoc.getPage(i + 1);
switch (pagesOp) {
case READ:
PdfTextExtractor.getTextFromPage(page);
break;
case MODIFY:
addContentToPage(page, font, xObject);
break;
case MODIFY_LIGHTLY:
addBasicContent(page, font);
break;
}
switch (flushMode) {
case UNSAFE_DEEP:
flushingHelper.unsafeFlushDeep(i + 1);
break;
case RELEASE_DEEP:
flushingHelper.releaseDeep(i + 1);
break;
case APPEND_MODE:
flushingHelper.appendModeFlush(i + 1);
break;
case PAGE_FLUSH:
page.flush();
break;
}
}
}
if (docMode != DocMode.READING && pagesOp == PagesOp.MODIFY) {
for (int i = 0; i < 100; ++i) {
PdfPage page = pdfDoc.addNewPage();
addContentToPage(page, font, xObject);
switch (flushMode) {
case UNSAFE_DEEP:
flushingHelper.unsafeFlushDeep(pdfDoc.getNumberOfPages());
break;
case RELEASE_DEEP:
flushingHelper.releaseDeep(pdfDoc.getNumberOfPages());
break;
case APPEND_MODE:
flushingHelper.appendModeFlush(pdfDoc.getNumberOfPages());
break;
case PAGE_FLUSH:
page.flush();
break;
}
}
}
verifyFlushedObjectsNum(pdfDoc, total, flushedExpected, notReadExpected);
pdfDoc.close();
}
private static void verifyFlushedObjectsNum(PdfDocument pdfDoc, int total, int flushedExpected, int notReadExpected) {
int flushedActual = 0;
int notReadActual = 0;
for (int i = 0; i < pdfDoc.getXref().size(); ++i) {
PdfIndirectReference indRef = pdfDoc.getXref().get(i);
if (indRef.checkState(PdfObject.FLUSHED)) {
++flushedActual;
} else if (!indRef.isFree() && indRef.refersTo == null) {
++notReadActual;
}
}
if (pdfDoc.getXref().size() != total || flushedActual != flushedExpected || notReadActual != notReadExpected) {
Assertions.fail(MessageFormatUtil.format("\nExpected total: {0}, flushed: {1}, not read: {2};" +
"\nbut actual was: {3}, flushed: {4}, not read: {5}.",
total, flushedExpected, notReadExpected, pdfDoc.getXref().size(), flushedActual, notReadActual
));
}
Assertions.assertEquals(total, pdfDoc.getXref().size(), "wrong num of total objects");
Assertions.assertEquals(flushedExpected, flushedActual, "wrong num of flushed objects");
Assertions.assertEquals(notReadExpected, notReadActual, "wrong num of not read objects");
}
private static void addContentToPage(PdfPage pdfPage, PdfFont font, PdfImageXObject xObject) throws IOException {
PdfCanvas canvas = addBasicContent(pdfPage, font);
canvas
.saveState()
.rectangle(250, 500, 100, 100)
.fill()
.restoreState();
PdfFont courier = PdfFontFactory.createFont(StandardFonts.COURIER);
courier.makeIndirect(pdfPage.getDocument());
canvas
.saveState()
.beginText()
.moveText(36, 650)
.setFontAndSize(courier, 16)
.showText("Hello Courier!")
.endText()
.restoreState();
canvas
.saveState()
.circle(100, 400, 25)
.fill()
.restoreState();
canvas
.saveState()
.roundRectangle(100, 650, 100, 100, 10)
.fill()
.restoreState();
canvas
.saveState()
.setLineWidth(10)
.roundRectangle(250, 650, 100, 100, 10)
.stroke()
.restoreState();
canvas
.saveState()
.setLineWidth(5)
.arc(400, 650, 550, 750, 0, 180)
.stroke()
.restoreState();
canvas
.saveState()
.setLineWidth(5)
.moveTo(400, 550)
.curveTo(500, 570, 450, 450, 550, 550)
.stroke()
.restoreState();
canvas.addXObjectFittedIntoRectangle(xObject, new Rectangle(100, 500, 400, xObject.getHeight()));
PdfImageXObject xObject2 = new PdfImageXObject(ImageDataFactory.create(sourceFolder + "itext.png"));
xObject2.makeIndirect(pdfPage.getDocument());
canvas.addXObjectFittedIntoRectangle(xObject2, new Rectangle(100, 500, 400, xObject2.getHeight()));
}
private static PdfCanvas addBasicContent(PdfPage pdfPage, PdfFont font) {
Rectangle lineAnnotRect = new Rectangle(0, 0, PageSize.A4.getRight(), PageSize.A4.getTop());
pdfPage.addAnnotation(
new PdfLinkAnnotation(new Rectangle(100, 600, 100, 20))
.setAction(PdfAction.createURI("http://itextpdf.com"))
).addAnnotation(
new PdfLineAnnotation(lineAnnotRect, new float[]{lineAnnotRect.getX(), lineAnnotRect.getY(), lineAnnotRect.getRight(), lineAnnotRect.getTop()})
.setColor(ColorConstants.BLACK)
);
PdfCanvas canvas = new PdfCanvas(pdfPage);
canvas.rectangle(100, 100, 100, 100).fill();
canvas
.saveState()
.beginText()
.setTextMatrix(AffineTransform.getRotateInstance(Math.PI / 4, 36, 350))
.setFontAndSize(font, 72)
.showText("Hello Helvetica!")
.endText()
.restoreState();
return canvas;
}
private enum DocMode {
WRITING,
READING,
STAMPING,
APPEND
}
private enum FlushMode {
NONE,
PAGE_FLUSH,
UNSAFE_DEEP,
RELEASE_DEEP,
APPEND_MODE,
}
private enum PagesOp {
NONE,
READ,
MODIFY,
MODIFY_LIGHTLY,
}
}