Skip to content

Commit

Permalink
Explicitly set built-in fonts on freetext annotations flattening
Browse files Browse the repository at this point in the history
This logic is closer to how Acrobat displays freetext annotations,
and also fixes NPE while processing such annotations with
PdfContentStreamProcessor after flattening.

DEV-1896
  • Loading branch information
IdamkinI committed Aug 11, 2017
1 parent e79227a commit b4a29ef
Show file tree
Hide file tree
Showing 7 changed files with 146 additions and 14 deletions.
15 changes: 15 additions & 0 deletions itext/src/main/java/com/itextpdf/text/pdf/BaseFont.java
Original file line number Diff line number Diff line change
Expand Up @@ -1496,6 +1496,21 @@ public static ArrayList<Object[]> getDocumentFonts(PdfReader reader, int page) {
return fonts;
}

static PdfDictionary createBuiltInFontDictionary(String name) {
return createBuiltInFontDictionary(BuiltinFonts14.get(name));
}

private static PdfDictionary createBuiltInFontDictionary(PdfName name) {
if (name == null) {
return null;
}
PdfDictionary dictionary = new PdfDictionary();
dictionary.put(PdfName.TYPE, PdfName.FONT);
dictionary.put(PdfName.BASEFONT, name);
dictionary.put(PdfName.SUBTYPE, PdfName.TYPE1);
return dictionary;
}

/**
* Gets the smallest box enclosing the character contours. It will return
* <CODE>null</CODE> if the font has not the information or the character has no
Expand Down
39 changes: 38 additions & 1 deletion itext/src/main/java/com/itextpdf/text/pdf/PdfStamperImp.java
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@
import com.itextpdf.text.Version;
import com.itextpdf.text.error_messages.MessageLocalization;
import com.itextpdf.text.exceptions.BadPasswordException;
import com.itextpdf.text.io.RandomAccessSource;
import com.itextpdf.text.io.RandomAccessSourceFactory;
import com.itextpdf.text.log.Counter;
import com.itextpdf.text.log.CounterFactory;
import com.itextpdf.text.log.Logger;
Expand Down Expand Up @@ -80,6 +82,7 @@
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.RandomAccess;

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

//Hash map of standard fonts used in flattening of annotations to prevent fonts duplication
private HashMap<String, PdfIndirectReference> builtInAnnotationFonts = new HashMap<String, PdfIndirectReference>();

private double[] DEFAULT_MATRIX = {1, 0, 0, 1, 0, 0};

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

app = new PdfAppearance(this);
//It is not stated in spec, but acrobat seems to support standard font names in DA
//So we need to check if the font is built-in and specify it explicitly.
PdfIndirectReference fontReference = null;
PdfName pdfFontName = null;
try {
RandomAccessSource source = new RandomAccessSourceFactory().createSource(defaultAppearancePdfString.getBytes());
PdfContentParser ps = new PdfContentParser(new PRTokeniser(new RandomAccessFileOrArray(source)));
ArrayList<PdfObject> operands = new ArrayList<PdfObject>();
while (ps.parse(operands).size() > 0) {
PdfLiteral operator = (PdfLiteral)operands.get(operands.size()-1);
if (operator.toString().equals("Tf")) {
pdfFontName = (PdfName) operands.get(0);
String fontName = pdfFontName.toString().substring(1);
fontReference = builtInAnnotationFonts.get(fontName);
if (fontReference == null) {
PdfDictionary dic = BaseFont.createBuiltInFontDictionary(fontName);
if (dic != null) {
fontReference = addToBody(dic).getIndirectReference();
builtInAnnotationFonts.put(fontName, fontReference);
}
}
}
}
} catch (Exception any) {
logger.warn(MessageLocalization.getComposedMessage("error.resolving.freetext.font"));
break;
}

app = new PdfAppearance(this);
// 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
// quickly and naively flattening the freetext annotation
if (fontReference != null) {
app.getPageResources().addFont(pdfFontName, fontReference);
}
app.saveState();
app.beginText();
app.setLiteral(defaultAppearanceString);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,7 @@ error.in.base64.code.reading.stream=Error in Base64 code reading stream.
error.parsing.cmap.beginbfchar.expected.cosstring.or.cosname.and.not.1=Error parsing CMap beginbfchar, expected {COSString or COSName} and not {1}
error.reading.objstm=Error reading ObjStm
error.reading.string=Error reading string
error.resolving.freetext.font=Cannot resolve annotation's font. It won't be flattened
error.with.jp.marker=Error with JP Marker
every.annotation.shall.have.at.least.one.appearance.dictionary=Every annotation shall have at least one appearance dictionary
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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,7 @@ error.in.base64.code.reading.stream=Fout in de Base64 code reading stream.
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}
error.reading.objstm=Fout tijdens het lezen van ObjStm
error.reading.string=Fout bij het lezen van een string
error.resolving.freetext.font=Kan het lettertype van annotatie niet oplossen. Het wordt niet afgedrukt
error.with.jp.marker=Foute JP Marker
every.annotation.shall.have.at.least.one.appearance.dictionary=Elke annotation moet ten minste 1 appearance dictionary hebben
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.
Expand Down
104 changes: 91 additions & 13 deletions itext/src/test/java/com/itextpdf/text/pdf/FreeTextFlatteningTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -45,46 +45,80 @@ This file is part of the iText (R) project.
import com.itextpdf.testutils.CompareTool;
import com.itextpdf.text.DocumentException;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;

import com.itextpdf.text.pdf.parser.ContentByteUtils;
import com.itextpdf.text.pdf.parser.ImageRenderInfo;
import com.itextpdf.text.pdf.parser.PdfContentStreamProcessor;
import com.itextpdf.text.pdf.parser.RenderListener;
import com.itextpdf.text.pdf.parser.TextRenderInfo;
import org.junit.Assert;
import org.junit.BeforeClass;
import org.junit.Test;

public class FreeTextFlatteningTest {

private final String FOLDER = "./src/test/resources/com/itextpdf/text/pdf/FreeTextFlatteningTest/";
private final static String FOLDER = "./src/test/resources/com/itextpdf/text/pdf/FreeTextFlatteningTest/";
private final static String TARGET = "./target/com/itextpdf/test/pdf/FreeTextFlattening/";

@BeforeClass
public static void setUp() {
new File(TARGET).mkdirs();
}

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

flattenFreeText(new FileInputStream(FOLDER + "freetext.pdf"), new FileOutputStream(outputFile));
checkFlattenedPdf(new FileInputStream(outputFile), 0);
flattenFreeText(FOLDER + "freetext.pdf", outputFile);
checkAnnotationSize(outputFile, 0);

String errorMessage = new CompareTool().compare(outputFile, FOLDER + "flattened.pdf", target, "diff");
String errorMessage = new CompareTool().compareByContent(outputFile, FOLDER + "flattened.pdf", TARGET, "diff");
if ( errorMessage != null ) {
Assert.fail(errorMessage);
}
}

@Test
public void checkPageContentTest() throws IOException, DocumentException, InterruptedException {
checkPageContent(FOLDER + "flattened.pdf");
}

@Test
public void flattenWithoutDA() throws IOException, DocumentException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
flattenFreeText(new FileInputStream(FOLDER + "freetext-no-da.pdf"), baos);
checkFlattenedPdf(new ByteArrayInputStream(baos.toByteArray()), 1);
String outputFile = TARGET + "freetext-flattened-no-da.pdf";

flattenFreeText(FOLDER + "freetext-no-da.pdf", outputFile);
checkAnnotationSize(outputFile, 1);
}

@Test
public void flattenAndCheckCourier() throws IOException, DocumentException, InterruptedException {
String inputFile = FOLDER + "freetext-courier.pdf";
String outputFile = TARGET + "freetext-courier-flattened.pdf";

flattenFreeText(inputFile, outputFile);
checkPageContent(outputFile);
}

private void checkAnnotationSize(String path, int expectedAnnotationsSize) throws IOException, DocumentException {
FileInputStream fin = null;
try {
fin = new FileInputStream(path);
checkAnnotationSize(fin, expectedAnnotationsSize);
} finally {
if (fin != null) {
fin.close();
}
}
}

private void checkFlattenedPdf(InputStream inputStream, int expectedAnnotationsSize) throws IOException, DocumentException {
private void checkAnnotationSize(InputStream inputStream, int expectedAnnotationsSize) throws IOException, DocumentException {
PdfReader reader = new PdfReader(inputStream);
PdfDictionary pageDictionary = reader.getPageN(1);
if ( pageDictionary.contains(PdfName.ANNOTS )) {
Expand All @@ -93,6 +127,23 @@ private void checkFlattenedPdf(InputStream inputStream, int expectedAnnotationsS
}
}

private void flattenFreeText(String inputPath, String outputPath) throws IOException, DocumentException {
FileInputStream fin = null;
FileOutputStream fout = null;
try {
fin = new FileInputStream(inputPath);
fout = new FileOutputStream(outputPath);
flattenFreeText(fin, fout);
} finally {
if (fin != null) {
fin.close();
}
if (fout != null) {
fout.close();
}
}
}

private void flattenFreeText(final InputStream inputStream, OutputStream outputStream) throws IOException, DocumentException {
PdfReader reader = new PdfReader(inputStream);
PdfStamper stamper = new PdfStamper(reader, outputStream);
Expand All @@ -103,4 +154,31 @@ private void flattenFreeText(final InputStream inputStream, OutputStream outputS

stamper.close();
}

private void checkPageContent(String path) throws IOException, DocumentException {
PdfReader pdfReader = new PdfReader(path);
try {
PdfDictionary pageDic = pdfReader.getPageN(1);

RenderListener dummy = new RenderListener() {
public void beginTextBlock() {
}

public void renderText(TextRenderInfo renderInfo) {
}

public void endTextBlock() {
}

public void renderImage(ImageRenderInfo renderInfo) {
}
};
PdfContentStreamProcessor processor = new PdfContentStreamProcessor(dummy);

PdfDictionary resourcesDic = pageDic.getAsDict(PdfName.RESOURCES);
processor.processContent(ContentByteUtils.getContentBytesForPage(pdfReader, 1), resourcesDic);
} finally {
pdfReader.close();
}
}
}
Binary file not shown.
Binary file not shown.

0 comments on commit b4a29ef

Please sign in to comment.