Skip to content

Commit b4a29ef

Browse files
committed
Explicitly set built-in fonts on freetext annotations flattening
This logic is closer to how Acrobat displays freetext annotations, and also fixes NPE while processing such annotations with PdfContentStreamProcessor after flattening. DEV-1896
1 parent e79227a commit b4a29ef

File tree

7 files changed

+146
-14
lines changed

7 files changed

+146
-14
lines changed

itext/src/main/java/com/itextpdf/text/pdf/BaseFont.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1496,6 +1496,21 @@ public static ArrayList<Object[]> getDocumentFonts(PdfReader reader, int page) {
14961496
return fonts;
14971497
}
14981498

1499+
static PdfDictionary createBuiltInFontDictionary(String name) {
1500+
return createBuiltInFontDictionary(BuiltinFonts14.get(name));
1501+
}
1502+
1503+
private static PdfDictionary createBuiltInFontDictionary(PdfName name) {
1504+
if (name == null) {
1505+
return null;
1506+
}
1507+
PdfDictionary dictionary = new PdfDictionary();
1508+
dictionary.put(PdfName.TYPE, PdfName.FONT);
1509+
dictionary.put(PdfName.BASEFONT, name);
1510+
dictionary.put(PdfName.SUBTYPE, PdfName.TYPE1);
1511+
return dictionary;
1512+
}
1513+
14991514
/**
15001515
* Gets the smallest box enclosing the character contours. It will return
15011516
* <CODE>null</CODE> if the font has not the information or the character has no

itext/src/main/java/com/itextpdf/text/pdf/PdfStamperImp.java

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,8 @@
5252
import com.itextpdf.text.Version;
5353
import com.itextpdf.text.error_messages.MessageLocalization;
5454
import com.itextpdf.text.exceptions.BadPasswordException;
55+
import com.itextpdf.text.io.RandomAccessSource;
56+
import com.itextpdf.text.io.RandomAccessSourceFactory;
5557
import com.itextpdf.text.log.Counter;
5658
import com.itextpdf.text.log.CounterFactory;
5759
import com.itextpdf.text.log.Logger;
@@ -80,6 +82,7 @@
8082
import java.util.Iterator;
8183
import java.util.List;
8284
import java.util.Map;
85+
import java.util.RandomAccess;
8386

8487
class PdfStamperImp extends PdfWriter {
8588
HashMap<PdfReader, IntHashtable> readers2intrefs = new HashMap<PdfReader, IntHashtable>();
@@ -125,6 +128,9 @@ protected Counter getCounter() {
125128
* then original layers are never read - they are simply copied to the new document with whole original catalog. */
126129
private boolean originalLayersAreRead = false;
127130

131+
//Hash map of standard fonts used in flattening of annotations to prevent fonts duplication
132+
private HashMap<String, PdfIndirectReference> builtInAnnotationFonts = new HashMap<String, PdfIndirectReference>();
133+
128134
private double[] DEFAULT_MATRIX = {1, 0, 0, 1, 0, 0};
129135

130136
/**
@@ -1290,9 +1296,40 @@ private void flattenAnnotations(boolean flattenFreeTextAnnotations) {
12901296
final PdfString freeTextContent = annDic.getAsString(PdfName.CONTENTS);
12911297
final String defaultAppearanceString = defaultAppearancePdfString.toString();
12921298

1293-
app = new PdfAppearance(this);
1299+
//It is not stated in spec, but acrobat seems to support standard font names in DA
1300+
//So we need to check if the font is built-in and specify it explicitly.
1301+
PdfIndirectReference fontReference = null;
1302+
PdfName pdfFontName = null;
1303+
try {
1304+
RandomAccessSource source = new RandomAccessSourceFactory().createSource(defaultAppearancePdfString.getBytes());
1305+
PdfContentParser ps = new PdfContentParser(new PRTokeniser(new RandomAccessFileOrArray(source)));
1306+
ArrayList<PdfObject> operands = new ArrayList<PdfObject>();
1307+
while (ps.parse(operands).size() > 0) {
1308+
PdfLiteral operator = (PdfLiteral)operands.get(operands.size()-1);
1309+
if (operator.toString().equals("Tf")) {
1310+
pdfFontName = (PdfName) operands.get(0);
1311+
String fontName = pdfFontName.toString().substring(1);
1312+
fontReference = builtInAnnotationFonts.get(fontName);
1313+
if (fontReference == null) {
1314+
PdfDictionary dic = BaseFont.createBuiltInFontDictionary(fontName);
1315+
if (dic != null) {
1316+
fontReference = addToBody(dic).getIndirectReference();
1317+
builtInAnnotationFonts.put(fontName, fontReference);
1318+
}
1319+
}
1320+
}
1321+
}
1322+
} catch (Exception any) {
1323+
logger.warn(MessageLocalization.getComposedMessage("error.resolving.freetext.font"));
1324+
break;
1325+
}
12941326

1327+
app = new PdfAppearance(this);
1328+
// it is unclear from spec were referenced from DA font should be (since annotations doesn't have DR), so in case it not built-in
12951329
// quickly and naively flattening the freetext annotation
1330+
if (fontReference != null) {
1331+
app.getPageResources().addFont(pdfFontName, fontReference);
1332+
}
12961333
app.saveState();
12971334
app.beginText();
12981335
app.setLiteral(defaultAppearanceString);

itext/src/main/resources/com/itextpdf/text/l10n/error/en.lng

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,7 @@ error.in.base64.code.reading.stream=Error in Base64 code reading stream.
168168
error.parsing.cmap.beginbfchar.expected.cosstring.or.cosname.and.not.1=Error parsing CMap beginbfchar, expected {COSString or COSName} and not {1}
169169
error.reading.objstm=Error reading ObjStm
170170
error.reading.string=Error reading string
171+
error.resolving.freetext.font=Cannot resolve annotation's font. It won't be flattened
171172
error.with.jp.marker=Error with JP Marker
172173
every.annotation.shall.have.at.least.one.appearance.dictionary=Every annotation shall have at least one appearance dictionary
173174
exactly.one.colour.space.specification.shall.have.the.value.0x01.in.the.approx.field=Exactly one colour space specification shall have the value 0x01 in the APPROX field.

itext/src/main/resources/com/itextpdf/text/l10n/error/nl.lng

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,7 @@ error.in.base64.code.reading.stream=Fout in de Base64 code reading stream.
168168
error.parsing.cmap.beginbfchar.expected.cosstring.or.cosname.and.not.1=Fout bij het parsen van CMap beginbfchar, {COSString or COSName} verwacht in plaats van {1}
169169
error.reading.objstm=Fout tijdens het lezen van ObjStm
170170
error.reading.string=Fout bij het lezen van een string
171+
error.resolving.freetext.font=Kan het lettertype van annotatie niet oplossen. Het wordt niet afgedrukt
171172
error.with.jp.marker=Foute JP Marker
172173
every.annotation.shall.have.at.least.one.appearance.dictionary=Elke annotation moet ten minste 1 appearance dictionary hebben
173174
exactly.one.colour.space.specification.shall.have.the.value.0x01.in.the.approx.field=Exact 1 colour space specificatie moet de waarde 0x01 in het APPROX veld hebben.

itext/src/test/java/com/itextpdf/text/pdf/FreeTextFlatteningTest.java

Lines changed: 91 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -45,46 +45,80 @@ This file is part of the iText (R) project.
4545
import com.itextpdf.testutils.CompareTool;
4646
import com.itextpdf.text.DocumentException;
4747

48-
import java.io.ByteArrayInputStream;
49-
import java.io.ByteArrayOutputStream;
5048
import java.io.File;
5149
import java.io.FileInputStream;
5250
import java.io.FileOutputStream;
5351
import java.io.IOException;
5452
import java.io.InputStream;
5553
import java.io.OutputStream;
5654

55+
import com.itextpdf.text.pdf.parser.ContentByteUtils;
56+
import com.itextpdf.text.pdf.parser.ImageRenderInfo;
57+
import com.itextpdf.text.pdf.parser.PdfContentStreamProcessor;
58+
import com.itextpdf.text.pdf.parser.RenderListener;
59+
import com.itextpdf.text.pdf.parser.TextRenderInfo;
5760
import org.junit.Assert;
61+
import org.junit.BeforeClass;
5862
import org.junit.Test;
5963

6064
public class FreeTextFlatteningTest {
6165

62-
private final String FOLDER = "./src/test/resources/com/itextpdf/text/pdf/FreeTextFlatteningTest/";
66+
private final static String FOLDER = "./src/test/resources/com/itextpdf/text/pdf/FreeTextFlatteningTest/";
67+
private final static String TARGET = "./target/com/itextpdf/test/pdf/FreeTextFlattening/";
6368

69+
@BeforeClass
70+
public static void setUp() {
71+
new File(TARGET).mkdirs();
72+
}
6473

6574
@Test
6675
public void flattenCorrectlyTest() throws IOException, DocumentException, InterruptedException {
67-
String target = "./target/com/itextpdf/test/pdf/FreeTextFlattening/";
68-
new File(target).mkdirs();
69-
String outputFile = target + "freetext-flattened.pdf";
76+
String outputFile = TARGET + "freetext-flattened.pdf";
7077

71-
flattenFreeText(new FileInputStream(FOLDER + "freetext.pdf"), new FileOutputStream(outputFile));
72-
checkFlattenedPdf(new FileInputStream(outputFile), 0);
78+
flattenFreeText(FOLDER + "freetext.pdf", outputFile);
79+
checkAnnotationSize(outputFile, 0);
7380

74-
String errorMessage = new CompareTool().compare(outputFile, FOLDER + "flattened.pdf", target, "diff");
81+
String errorMessage = new CompareTool().compareByContent(outputFile, FOLDER + "flattened.pdf", TARGET, "diff");
7582
if ( errorMessage != null ) {
7683
Assert.fail(errorMessage);
7784
}
7885
}
7986

87+
@Test
88+
public void checkPageContentTest() throws IOException, DocumentException, InterruptedException {
89+
checkPageContent(FOLDER + "flattened.pdf");
90+
}
91+
8092
@Test
8193
public void flattenWithoutDA() throws IOException, DocumentException {
82-
ByteArrayOutputStream baos = new ByteArrayOutputStream();
83-
flattenFreeText(new FileInputStream(FOLDER + "freetext-no-da.pdf"), baos);
84-
checkFlattenedPdf(new ByteArrayInputStream(baos.toByteArray()), 1);
94+
String outputFile = TARGET + "freetext-flattened-no-da.pdf";
95+
96+
flattenFreeText(FOLDER + "freetext-no-da.pdf", outputFile);
97+
checkAnnotationSize(outputFile, 1);
98+
}
99+
100+
@Test
101+
public void flattenAndCheckCourier() throws IOException, DocumentException, InterruptedException {
102+
String inputFile = FOLDER + "freetext-courier.pdf";
103+
String outputFile = TARGET + "freetext-courier-flattened.pdf";
104+
105+
flattenFreeText(inputFile, outputFile);
106+
checkPageContent(outputFile);
107+
}
108+
109+
private void checkAnnotationSize(String path, int expectedAnnotationsSize) throws IOException, DocumentException {
110+
FileInputStream fin = null;
111+
try {
112+
fin = new FileInputStream(path);
113+
checkAnnotationSize(fin, expectedAnnotationsSize);
114+
} finally {
115+
if (fin != null) {
116+
fin.close();
117+
}
118+
}
85119
}
86120

87-
private void checkFlattenedPdf(InputStream inputStream, int expectedAnnotationsSize) throws IOException, DocumentException {
121+
private void checkAnnotationSize(InputStream inputStream, int expectedAnnotationsSize) throws IOException, DocumentException {
88122
PdfReader reader = new PdfReader(inputStream);
89123
PdfDictionary pageDictionary = reader.getPageN(1);
90124
if ( pageDictionary.contains(PdfName.ANNOTS )) {
@@ -93,6 +127,23 @@ private void checkFlattenedPdf(InputStream inputStream, int expectedAnnotationsS
93127
}
94128
}
95129

130+
private void flattenFreeText(String inputPath, String outputPath) throws IOException, DocumentException {
131+
FileInputStream fin = null;
132+
FileOutputStream fout = null;
133+
try {
134+
fin = new FileInputStream(inputPath);
135+
fout = new FileOutputStream(outputPath);
136+
flattenFreeText(fin, fout);
137+
} finally {
138+
if (fin != null) {
139+
fin.close();
140+
}
141+
if (fout != null) {
142+
fout.close();
143+
}
144+
}
145+
}
146+
96147
private void flattenFreeText(final InputStream inputStream, OutputStream outputStream) throws IOException, DocumentException {
97148
PdfReader reader = new PdfReader(inputStream);
98149
PdfStamper stamper = new PdfStamper(reader, outputStream);
@@ -103,4 +154,31 @@ private void flattenFreeText(final InputStream inputStream, OutputStream outputS
103154

104155
stamper.close();
105156
}
157+
158+
private void checkPageContent(String path) throws IOException, DocumentException {
159+
PdfReader pdfReader = new PdfReader(path);
160+
try {
161+
PdfDictionary pageDic = pdfReader.getPageN(1);
162+
163+
RenderListener dummy = new RenderListener() {
164+
public void beginTextBlock() {
165+
}
166+
167+
public void renderText(TextRenderInfo renderInfo) {
168+
}
169+
170+
public void endTextBlock() {
171+
}
172+
173+
public void renderImage(ImageRenderInfo renderInfo) {
174+
}
175+
};
176+
PdfContentStreamProcessor processor = new PdfContentStreamProcessor(dummy);
177+
178+
PdfDictionary resourcesDic = pageDic.getAsDict(PdfName.RESOURCES);
179+
processor.processContent(ContentByteUtils.getContentBytesForPage(pdfReader, 1), resourcesDic);
180+
} finally {
181+
pdfReader.close();
182+
}
183+
}
106184
}
Binary file not shown.

0 commit comments

Comments
 (0)