user6839234 user6839234 - 1 month ago 22
Groovy Question

iText7 Java adding text issue

I'm using iText 7 to duplicate pdf pages and numbering these pages. So I don't need to numbering them manually. But there's a problem with the numbers in the generated pdf file. Here's what it looks like :

Issue image

And I have think about it for many hours, still can't figure it out.

My code :

import com.itextpdf.io.font.FontConstants;
import com.itextpdf.kernel.font.PdfFontFactory;
import com.itextpdf.kernel.pdf.PdfDocument;
import com.itextpdf.kernel.pdf.PdfPage;
import com.itextpdf.kernel.pdf.PdfReader;
import com.itextpdf.kernel.pdf.PdfWriter;
import com.itextpdf.kernel.pdf.canvas.PdfCanvas;

import java.io.File;
import java.io.IOException;

public class NumberingInJava {

public static final String SRC = "D:/temp/num_src.pdf";
public static final String DEST = "D:/temp/edited_numbering.pdf";

public static final String[] NUM4SAMPLE = {"02A", "03A", "03B", "03C", "04A", "08A"};

public static final double XCOOR = 230;
public static final double YCOOR = 795;//755

public static void main(String[] args) throws IOException {
File file = new File(DEST);
file.getParentFile().mkdirs();
new NumberingInJava().manipulatePdf(SRC, DEST, NUM4SAMPLE);
}

private void manipulatePdf(String src, String dest, String[] numbering4what) throws IOException {

//Initialize PDF document
PdfDocument pdfDocToRead = new PdfDocument(new PdfReader(src));
PdfDocument pdfDocToWrite = new PdfDocument(new PdfWriter(dest));

for(String s : numbering4what) {
println(s);
}

String number = null;
PdfPage tempPage = null;
for (int i=0; i<numbering4what.length; i++) {
pdfDocToRead.copyPagesTo(1, 2, pdfDocToWrite);
number = numbering4what[i];
println(number);
tempPage = pdfDocToWrite.getPage(2*(i+1)-1);

numberingPage(tempPage, number);

println("pdfDocToWrite.numberOfPages : "+pdfDocToWrite.getNumberOfPages());
}

pdfDocToRead.close();
pdfDocToWrite.close();

println("\nNumber added!");
}

private void numberingPage(PdfPage pdfPage, String number) throws IOException {
println(pdfPage);
PdfCanvas canvas = new PdfCanvas(pdfPage);
canvas.beginText().setFontAndSize(PdfFontFactory.createFont(FontConstants.HELVETICA), 22)
.moveText(XCOOR, YCOOR)
.showText(number)
.endText();

println("number: "+number);

}

private void println(Object obj) {
System.out.println(obj);
}
}


The console output:

02A
03A
03B
03C
04A
08A
02A
com.itextpdf.kernel.pdf.PdfPage@cf768c
17:58:07,457 |-INFO in ch.qos.logback.classic.LoggerContext[default] - Could NOT find resource [logback.groovy]
17:58:07,458 |-INFO in ch.qos.logback.classic.LoggerContext[default] - Could NOT find resource [logback-test.xml]
17:58:07,458 |-INFO in ch.qos.logback.classic.LoggerContext[default] - Found resource [logback.xml] at [jar:file:/D:/Java_Packages/ext_lib/iText/itext7-7.0.1/itext7-itext-rups-7.0.1.jar!/logback.xml]
17:58:07,459 |-WARN in ch.qos.logback.classic.LoggerContext[default] - Resource [logback.xml] occurs multiple times on the classpath.
17:58:07,459 |-WARN in ch.qos.logback.classic.LoggerContext[default] - Resource [logback.xml] occurs at [jar:file:/D:/Java_Packages/ext_lib/iText/itext7-7.0.1/itext7-itext-rups-7.0.1-jar-with-dependencies.jar!/logback.xml]
17:58:07,459 |-WARN in ch.qos.logback.classic.LoggerContext[default] - Resource [logback.xml] occurs at [jar:file:/D:/Java_Packages/ext_lib/iText/itext7-7.0.1/itext7-itext-rups-7.0.1-sources.jar!/logback.xml]
17:58:07,459 |-WARN in ch.qos.logback.classic.LoggerContext[default] - Resource [logback.xml] occurs at [jar:file:/D:/Java_Packages/ext_lib/iText/itext7-7.0.1/itext7-itext-rups-7.0.1.jar!/logback.xml]
17:58:07,501 |-INFO in ch.qos.logback.core.joran.spi.ConfigurationWatchList@8080bb - URL [jar:file:/D:/Java_Packages/ext_lib/iText/itext7-7.0.1/itext7-itext-rups-7.0.1.jar!/logback.xml] is not of type file
17:58:07,662 |-INFO in ch.qos.logback.classic.joran.action.ConfigurationAction - debug attribute not set
17:58:07,829 |-INFO in ch.qos.logback.core.joran.action.AppenderAction - About to instantiate appender of type [com.itextpdf.rups.view.DebugAppender]
17:58:07,851 |-INFO in ch.qos.logback.core.joran.action.AppenderAction - Naming appender as [DEFAULT_APP]
17:58:07,940 |-INFO in ch.qos.logback.core.joran.action.NestedComplexPropertyIA - Assuming default type [ch.qos.logback.classic.encoder.PatternLayoutEncoder] for [encoder] property
17:58:08,045 |-INFO in ch.qos.logback.core.joran.action.AppenderAction - About to instantiate appender of type [com.itextpdf.rups.view.StyleAppender]
17:58:08,046 |-INFO in ch.qos.logback.core.joran.action.AppenderAction - Naming appender as [INFO_APP]
17:58:08,062 |-INFO in ch.qos.logback.core.joran.action.NestedComplexPropertyIA - Assuming default type [ch.qos.logback.classic.encoder.PatternLayoutEncoder] for [encoder] property
17:58:08,063 |-INFO in ch.qos.logback.core.joran.action.AppenderAction - About to instantiate appender of type [com.itextpdf.rups.view.DebugAppender]
17:58:08,063 |-INFO in ch.qos.logback.core.joran.action.AppenderAction - Naming appender as [DEBUG_APP]
17:58:08,065 |-INFO in ch.qos.logback.core.joran.action.NestedComplexPropertyIA - Assuming default type [ch.qos.logback.classic.encoder.PatternLayoutEncoder] for [encoder] property
17:58:08,066 |-INFO in ch.qos.logback.core.joran.action.AppenderAction - About to instantiate appender of type [com.itextpdf.rups.view.DebugAppender]
17:58:08,066 |-INFO in ch.qos.logback.core.joran.action.AppenderAction - Naming appender as [TRACE_APP]
17:58:08,067 |-INFO in ch.qos.logback.core.joran.action.NestedComplexPropertyIA - Assuming default type [ch.qos.logback.classic.encoder.PatternLayoutEncoder] for [encoder] property
17:58:08,069 |-INFO in ch.qos.logback.core.joran.action.AppenderAction - About to instantiate appender of type [com.itextpdf.rups.view.StyleAppender]
17:58:08,069 |-INFO in ch.qos.logback.core.joran.action.AppenderAction - Naming appender as [IMPORTANT_APP]
17:58:08,075 |-INFO in ch.qos.logback.core.joran.action.NestedComplexPropertyIA - Assuming default type [ch.qos.logback.classic.encoder.PatternLayoutEncoder] for [encoder] property
17:58:08,076 |-INFO in ch.qos.logback.classic.joran.action.LoggerAction - Setting additivity of logger [com.itextpdf] to false
17:58:08,077 |-INFO in ch.qos.logback.core.joran.action.AppenderRefAction - Attaching appender named [IMPORTANT_APP] to Logger[com.itextpdf]
17:58:08,078 |-INFO in ch.qos.logback.core.joran.action.AppenderRefAction - Attaching appender named [INFO_APP] to Logger[com.itextpdf]
17:58:08,078 |-INFO in ch.qos.logback.core.joran.action.AppenderRefAction - Attaching appender named [DEBUG_APP] to Logger[com.itextpdf]
17:58:08,078 |-INFO in ch.qos.logback.core.joran.action.AppenderRefAction - Attaching appender named [TRACE_APP] to Logger[com.itextpdf]
17:58:08,078 |-INFO in ch.qos.logback.classic.joran.action.RootLoggerAction - Setting level of ROOT logger to TRACE
17:58:08,078 |-INFO in ch.qos.logback.core.joran.action.AppenderRefAction - Attaching appender named [DEFAULT_APP] to Logger[ROOT]
17:58:08,078 |-INFO in ch.qos.logback.classic.joran.action.ConfigurationAction - End of configuration.
17:58:08,082 |-INFO in ch.qos.logback.classic.joran.JoranConfigurator@1c24521 - Registering current configuration as safe fallback point

number: 02A
pdfDocToWrite.numberOfPages : 2
03A
com.itextpdf.kernel.pdf.PdfPage@11aa95a
number: 03A
pdfDocToWrite.numberOfPages : 4
03B
com.itextpdf.kernel.pdf.PdfPage@bc05a6
number: 03B
pdfDocToWrite.numberOfPages : 6
03C
com.itextpdf.kernel.pdf.PdfPage@ef309d
number: 03C
pdfDocToWrite.numberOfPages : 8
04A
com.itextpdf.kernel.pdf.PdfPage@1fc609f
number: 04A
pdfDocToWrite.numberOfPages : 10
08A
com.itextpdf.kernel.pdf.PdfPage@173813a
number: 08A
pdfDocToWrite.numberOfPages : 12

Number added!

Process finished with exit code 0


EDIT:
I have uploaded a simulated document and an edited document on dropbox, here it is:
Simulated doc

edited Simulated doc

mkl mkl
Answer

The cause

The problem is caused iText trying to make the result PDF small:

When you copy the pages of pdfDocToRead multiple times to pdfDocToWrite, the actual page content stream, page resources, etc. are copied only once, merely a small object referencing these data is generated once for each copy and page.

This optimization is not yet the problem, but a further micro-optimization is, in

PdfCanvas canvas = new PdfCanvas(pdfPage);

this PdfCanvas helper method is used to retrieve the content stream to which content will be added:

private static PdfStream getPageStream(PdfPage page) {
    PdfStream stream = page.getContentStream(page.getContentStreamCount() - 1);
    return stream == null || stream.getOutputStream() == null || stream.containsKey(PdfName.Filter) ? page.newContentStreamAfter() : stream;
}

As you see, usually a new content stream is added to the page (page.newContentStreamAfter()); only if there already is a content stream and this stream has no filter (e.g. for compression), this existing content stream is used to append data to.

In case of your document the single content stream copied for each source page is not compressed. Thus, those two optimizations result in all your PdfCanvas canvas instances appending to the same single content stream.

A work-around

The obvious work-around consists of circumventing the latter optimization: Replace the line

PdfCanvas canvas = new PdfCanvas(pdfPage);

by

PdfCanvas canvas = new PdfCanvas(pdfPage.newContentStreamAfter(), pdfPage.getResources(), pdfPage.getDocument());

(in StampPageNumbers.java method numberingPage)

This essentially is what also would have happened if the original content stream was compressed.

(In general one might actually have to add a new content stream before the current with a save-graphics-state instruction and initially add a restore-graphics-state instruction to the new content stream after the current; in case of your sample document, though, this is not necessary because the graphics state is not adversely changed by the current content.)

Comments