Using pdfbox - how to get the font from a COSName? - pdfbox

How to get the font from a COSName?
The solution I'm looking for looks somehow like this:
COSDictionary dict = new COSDictionary();
dict.add(fontname, something); // fontname COSName from below code
PDFontFactory.createFont(dict);
If you need more background, I added the whole story below:
I try to replace some string in a pdf. This succeeds (as long as all text is stored in one token). In order to keep the format I like to re-center the text. As far as I understood I can do this by getting the width of the old string and the new one, do some trivial calculation and setting the new position.
I found some inspiration on stackoverflow for replacing https://stackoverflow.com/a/36404377 (yes it has some issues, but works for my simple pdf's. And How to center a text using PDFBox. Unfortunatly this example uses a font constant.
So using the first link's code I get a handling for operator 'TJ' and one for 'Tj'.
PDFStreamParser parser = new PDFStreamParser(page);
parser.parse();
java.util.List<Object> tokens = parser.getTokens();
for (int j = 0; j < tokens.size(); j++)
{
Object next = tokens.get(j);
if (next instanceof Operator)
{
Operator op = (Operator) next;
// Tj and TJ are the two operators that display strings in a PDF
if (op.getName().equals("Tj"))
{
// Tj takes one operator and that is the string to display so lets
// update that operator
COSString previous = (COSString) tokens.get(j - 1);
String string = previous.getString();
String replaced = prh.getReplacement(string);
if (!string.equals(replaced))
{ // if changes are there, replace the content
previous.setValue(replaced.getBytes());
float xpos = getPosX(tokens, j);
//if (true) // center the text
if (6 * xpos > page.getMediaBox().getWidth()) // check if text starts right from 1/xth page width
{
float fontsize = getFontSize(tokens, j);
COSName fontname = getFontName(tokens, j);
// TODO
PDFont font = ?getFont?(fontname);
// TODO
float widthnew = getStringWidth(replaced, font, fontsize);
setPosX(tokens, j, page.getMediaBox().getWidth() / 2F - (widthnew / 2F));
}
replaceCount++;
}
}
Considering the code between the TODO tags, I will get the required values from the token list. (yes this code is awful, but for now it let's me concentrate on the main issue)
Having the string, the size and the font I should be able to call the getWidth(..) method from the sample code.
Unfortunatly I run into trouble to create a font from the COSName variable.
PDFont doesn't provide a method to create a font by name.
PDFontFactory looks fine, but requests a COSDictionary. This is the point I gave up and request help from you.

The names are associated with font objects in the page resources.
Assuming you use PDFBox 2.0.x and that page is a PDPage instance, you can resolve the name fontname using:
PDFont font = page.getResources().getFont(fontname);
But the warning from the comments to the questions you reference remain: This approach will work only for very simple PDFs and might even damage other ones.

try {
//Loading an existing document
File file = new File("UKRSICH_Mo6i-Spikyer_z1560-FAV.pdf");
PDDocument document = PDDocument.load(file);
PDPage page = document.getPage(0);
PDResources pageResources = page.getResources();
System.out.println(pageResources.getFontNames() );
for (COSName key : pageResources.getFontNames())
{
PDFont font = pageResources.getFont(key);
System.out.println("Font: " + font.getName());
}
document.close();
}

Related

Copying pages from one PDF to another gives an error on save

I'm trying to take pages from one PDF, scale them down, and put them side-by-side in another PDF. To do this I make an intermediate PDF that has all of the pages from the source scaled down to the size I need to place them side-by-side. Then I go thought the scaled PDF and copy the pages two at a time to the final PDF. My thinking is that I'm down with the scaled PDF so I can close it but when I do that I get an error trying to save the final PDF that says
COSStream has been closed and cannot be read. Perhaps its enclosing PDDocument has been closed?
I'm not sure why the intermediate doc should matter when I try to save the final doc. It could be that I'm doing something wrong in the copying of pages? Here's the code I use for that:
private PDDocument sideBySide(PaperSize paperSize, PaperSize pageSize) throws IOException {
PDRectangle targetPaperSize = getRect(paperSize);
PDRectangle targetPageSize = getRect(pageSize);
PDDocument scaledDoc = scaleDoc(pageSize, doc);
PDDocument outputDoc = new PDDocument();
final double theta = Math.PI / 2;
for (int offset = 0; offset < scaledDoc.getNumberOfPages() - 1; offset+=2) {
PDPage twoUp = new PDPage(targetPaperSize);
twoUp.setRotation(90);
twoUp.setResources(new PDResources());
outputDoc.addPage(twoUp);
PDPage leftPage = scaledDoc.getPage(offset);
PDPage rightPage = scaledDoc.getPage(offset + 1);
PDFormXObject leftObject = importAsXObject(outputDoc, leftPage);
twoUp.getResources().add(leftObject);
PDFormXObject rightObject = importAsXObject(outputDoc, rightPage);
twoUp.getResources().add(rightObject);
PDPageContentStream content = new PDPageContentStream(outputDoc, twoUp);
AffineTransform leftTrans = AffineTransform.getRotateInstance(theta);
leftTrans.concatenate(AffineTransform.getTranslateInstance(0, -targetPageSize.getHeight()));
AffineTransform rightTrans = AffineTransform.getRotateInstance(theta);
rightTrans.concatenate(AffineTransform.getTranslateInstance(targetPageSize.getWidth(), -targetPageSize.getHeight()));
leftObject.setMatrix(leftTrans);
rightObject.setMatrix(rightTrans);
content.drawForm(leftObject);
content.drawForm(rightObject);
content.close();
}
scaledDoc.close();
return outputDoc;
}

Printing to pdf from Google Apps Script HtmlOutput

For years, I have been using Google Cloud Print to print labels in our laboratories on campus (to standardize) using a Google Apps Script custom HtmlService form.
Now that GCP is becoming depreciated, I am in on a search for a solution. I have found a few options but am struggling to get the file to convert to a pdf as would be needed with these other vendors.
Currently, when you submit a text/html blob to the GCP servers in GAS, the backend converts the blob to application/pdf (as evidenced by looking at the job details in the GCP panel on Chrome under 'content type').
That said, because these other cloud print services require pdf printing, I have tried for some time now to have GAS change the file to pdf format before sending to GCP and I always get a strange result. Below, I'll show some of the strategies that I have used and include pictures of one of our simple labels generated with the different functions.
The following is the base code for the ticket and payload that has worked for years with GCP
//BUILD PRINT JOB FOR NARROW TAPES
var ticket = {
version: "1.0",
print: {
color: {
type: "STANDARD_COLOR",
vendor_id: "Color"
},
duplex: {
type: "NO_DUPLEX"
},
copies: {copies: parseFloat(quantity)},
media_size: {
width_microns: 27940,
height_microns:40960
},
page_orientation: {
type: "LANDSCAPE"
},
margins: {
top_microns:0,
bottom_microns:0,
left_microns:0,
right_microns:0
},
page_range: {
interval:
[{start:1,
end:1}]
},
}
};
var payload = {
"printerid" : QL710,
"title" : "Blank Template Label",
"content" : HtmlService.createHtmlOutput(html).getBlob(),
"contentType": 'text/html',
"ticket" : JSON.stringify(ticket)
};
This generates the expected following printout:
When trying to convert to pdf using the following code:
The following is the code used to transform to pdf:
var blob = HtmlService.createTemplate(html).evaluate().getContent();
var newBlob = Utilities.newBlob(html, "text/html", "text.html");
var pdf = newBlob.getAs("application/pdf").setName('tempfile');
var file = DriveApp.getFolderById("FOLDER ID").createFile(pdf);
var payload = {
"printerid" : QL710,
"title" : "Blank Template Label",
"content" : pdf,//HtmlService.createHtmlOutput(html).getBlob(),
"contentType": 'text/html',
"ticket" : JSON.stringify(ticket)
};
an unexpected result occurs:
This comes out the same way for direct coding in the 'content' field with and without .getBlob():
"content" : HtmlService.createHtmlOutput(html).getAs('application/pdf'),
note the createFile line in the code above used to test the pdf. This file is created as expected, of course with the wrong dimensions for label printing (not sure how to convert to pdf with the appropriate margins and page size?): see below
I have now tried to adopt Yuri's ideas; however, the conversion from html to document loses formatting.
var blob = HtmlService.createHtmlOutput(html).getBlob();
var docID = Drive.Files.insert({title: 'temp-label'}, blob, {convert: true}).id
var file = DocumentApp.openById(docID);
file.getBody().setMarginBottom(0).setMarginLeft(0).setMarginRight(0).setMarginTop(0).setPageHeight(79.2).setPageWidth(172.8);
This produces a document looks like this (picture also showing expected output in my hand).
Does anyone have insights into:
How to format the converted pdf to contain appropriate height, width
and margins.
How to convert to pdf in a way that would print correctly.
Here is a minimal code to get a better sense of context https://script.google.com/d/1yP3Jyr_r_FIlt6_aGj_zIf7HnVGEOPBKI0MpjEGHRFAWztGzcWKCJrD0/edit?usp=sharing
I've made the template (80 x 40 mm -- sorry, I don't know your size):
https://docs.google.com/document/d/1vA93FxGXcWLIEZBuQwec0n23cWGddyLoey-h0WR9weY/edit?usp=sharing
And there is the script:
function myFunction() {
// input data
var matName = '<b>testing this to <u>see</u></b> if it <i>actually</i> works <i>e.coli</i>'
var disposeWeek = 'end of semester'
var prepper = 'John Ruppert';
var className = 'Cell and <b>Molecular</b> Biology <u>Fall 2020</u> a few exercises a few exercises a few exercises a few exercises';
var hazards = 'Lots of hazards';
// make a temporary Doc from the template
var copyFile = DriveApp.getFileById('1vA93FxGXcWLIEZBuQwec0n23cWGddyLoey-h0WR9weY').makeCopy();
var doc = DocumentApp.openById(copyFile.getId());
var body = doc.getBody();
// replace placeholders with data
body.replaceText('{matName}', matName);
body.replaceText('{disposeWeek}', disposeWeek);
body.replaceText('{prepper}', prepper);
body.replaceText('{className}', className);
body.replaceText('{hazards}', hazards);
// make Italics, Bold and Underline
handle_tags(['<i>', '</i>'], body);
handle_tags(['<b>', '</b>'], body);
handle_tags(['<u>', '</u>'], body);
// save the temporary Doc
doc.saveAndClose();
// make a PDF
var docblob = doc.getBlob().setName('Label.pdf');
DriveApp.createFile(docblob);
// delete the temporary Doc
copyFile.setTrashed(true);
}
// this function applies formatting to text inside the tags
function handle_tags(tags, body) {
var start_tag = tags[0].toLowerCase();
var end_tag = tags[1].toLowerCase();
var found = body.findText(start_tag);
while (found) {
var elem = found.getElement();
var start = found.getEndOffsetInclusive();
var end = body.findText(end_tag, found).getStartOffset()-1;
switch (start_tag) {
case '<b>': elem.setBold(start, end, true); break;
case '<i>': elem.setItalic(start, end, true); break;
case '<u>': elem.setUnderline(start, end, true); break;
}
found = body.findText(start_tag, found);
}
body.replaceText(start_tag, ''); // remove tags
body.replaceText(end_tag, '');
}
The script just changes the {placeholders} with the data and saves the result as a PDF file (Label.pdf). The PDF looks like this:
There is one thing, I'm not sure if it's possible -- to change a size of the texts dynamically to fit them into the cells, like it's done in your 'autosize.html'. Roughly, you can take a length of the text in the cell and, in case it is bigger than some number, to make the font size a bit smaller. Probably you can use the jquery texfill function from the 'autosize.html' to get an optimal size and apply the size in the document.
I'm not sure if I got you right. Do you need make PDF and save it on Google Drive? You can do in Google Docs.
As example:
Make a new document with your table and text. Something like this
Add this script into your doc:
function myFunction() {
var copyFile = DriveApp.getFileById(ID).makeCopy();
var newFile = DriveApp.createFile(copyFile.getAs('application/pdf'));
newFile.setName('label');
copyFile.setTrashed(true);
}
Every time you run this script it makes the file 'label.pdf' on your Google Drive.
The size of this pdf will be the same as the page size of your Doc. You can make any size of page with add-on: Page Sizer https://webapps.stackexchange.com/questions/129617/how-to-change-the-size-of-paper-in-google-docs-to-custom-size
If you need to change the text in your label before generate pdf or/and you need change the name of generated file, you can do it via script as well.
Here is a variant of the script that changes a font size in one of the cells if the label doesn't fit into one page.
function main() {
// input texts
var text = {};
text.matName = '<b>testing this to <u>see</u></b> if it <i>actually</i> works <i>e.coli</i>';
text.disposeWeek = 'end of semester';
text.prepper = 'John Ruppert';
text.className = 'Cell and <b>Molecular</b> Biology <u>Fall 2020</u> a few exercises a few exercises a few exercises a few exercises';
text.hazards = 'Lots of hazards';
// initial max font size for the 'matName'
var size = 10;
var doc_blob = set_text(text, size);
// if we got more than 1 page, reduce the font size and repeat
while ((size > 4) && (getNumPages(doc_blob) > 1)) {
size = size-0.5;
doc_blob = set_text(text, size);
}
// save pdf
DriveApp.createFile(doc_blob);
}
// this function takes texts and a size and put the texts into fields
function set_text(text, size) {
// make a copy
var copyFile = DriveApp.getFileById('1vA93FxGXcWLIEZBuQwec0n23cWGddyLoey-h0WR9weY').makeCopy();
var doc = DocumentApp.openById(copyFile.getId());
var body = doc.getBody();
// replace placeholders with data
body.replaceText('{matName}', text.matName);
body.replaceText('{disposeWeek}', text.disposeWeek);
body.replaceText('{prepper}', text.prepper);
body.replaceText('{className}', text.className);
body.replaceText('{hazards}', text.hazards);
// set font size for 'matName'
body.findText(text.matName).getElement().asText().setFontSize(size);
// make Italics, Bold and Underline
handle_tags(['<i>', '</i>'], body);
handle_tags(['<b>', '</b>'], body);
handle_tags(['<u>', '</u>'], body);
// save the doc
doc.saveAndClose();
// delete the copy
copyFile.setTrashed(true);
// return blob
return docblob = doc.getBlob().setName('Label.pdf');
}
// this function formats the text beween html tags
function handle_tags(tags, body) {
var start_tag = tags[0].toLowerCase();
var end_tag = tags[1].toLowerCase();
var found = body.findText(start_tag);
while (found) {
var elem = found.getElement();
var start = found.getEndOffsetInclusive();
var end = body.findText(end_tag, found).getStartOffset()-1;
switch (start_tag) {
case '<b>': elem.setBold(start, end, true); break;
case '<i>': elem.setItalic(start, end, true); break;
case '<u>': elem.setUnderline(start, end, true); break;
}
found = body.findText(start_tag, found);
}
body.replaceText(start_tag, '');
body.replaceText(end_tag, '');
}
// this funcion takes saved doc and returns the number of its pages
function getNumPages(doc) {
var blob = doc.getAs('application/pdf');
var data = blob.getDataAsString();
var pages = parseInt(data.match(/ \/N (\d+) /)[1], 10);
Logger.log("pages = " + pages);
return pages;
}
It looks rather awful and hopeless. It turned out that Google Docs has no page number counter. You need to convert your document into a PDF and to count pages of the PDF file. Gross!
Next problem, even if you managed somehow to count the pages, you have no clue which of the cells was overflowed. This script takes just one cell, changes its font size, counts pages, changes the font size again, etc. But it doesn't granted a success, because there can be another cell with long text inside. You can reduce font size of all the texts, but it doesn't look like a great idea as well.

allow arabic text in pdf table using itext7 (xamarin android)

I have to put my list data in a table in a pdf file. My data has some Arabic words. When my pdf is generated, the Arabic words don't appear. I searched and found that I need itext7.pdfcalligraph so I installed it in my app. I found this code too https://itextpdf.com/en/blog/technical-notes/displaying-text-different-languages-single-pdf-document and tried to do something similar to allow Arabic words in my table but I couldn't figure it out.
This is a trial code before I apply it to my real list:
var path2 = global::Android.OS.Environment.ExternalStorageDirectory.AbsolutePath;
filePath = System.IO.Path.Combine(path2.ToString(), "myfile2.pdf");
stream = new FileStream(filePath, FileMode.Create);
PdfWriter writer = new PdfWriter(stream);
PdfDocument pdf2 = new iText.Kernel.Pdf.PdfDocument(writer);
Document document = new Document(pdf2, PageSize.A4);
FontSet set = new FontSet();
set.AddFont("ARIAL.TTF");
document.SetFontProvider(new FontProvider(set));
document.SetProperty(Property.FONT, "Arial");
string[] sources = new string[] { "يوم","شهر 2020" };
iText.Layout.Element.Table table = new iText.Layout.Element.Table(2, false);
foreach (string source in sources)
{
Paragraph paragraph = new Paragraph();
Bidi bidi = new Bidi(source, Bidi.DirectionDefaultLeftToRight);
if (bidi.BaseLevel != 0)
{
paragraph.SetTextAlignment(iText.Layout.Properties.TextAlignment.RIGHT);
}
paragraph.Add(source);
table.AddCell(new Cell(1, 1).SetTextAlignment(iText.Layout.Properties.TextAlignment.CENTER).Add(paragraph));
}
document.Add(table);
document.Close();
I updated my code and added the arial.ttf to my assets folder . i'm getting the following exception:
System.InvalidOperationException: 'FontProvider and FontSet are empty. Cannot resolve font family name (see ElementPropertyContainer#setFontFamily) without initialized FontProvider (see RootElement#setFontProvider).'
and I still can't figure it out. any ideas?
thanks in advance
- C #
I have a similar situation for Turkish characters, and I've followed these
steps :
Create a folder under projects root folder which is : /wwwroot/Fonts
Add OpenSans-Regular.ttf under the Fonts folder
Path for font is => ../wwwroot/Fonts/OpenSans-Regular.ttf
Create font like below :
public static PdfFont CreateOpenSansRegularFont()
{
var path = "{Your absolute path for FONT}";
return PdfFontFactory.CreateFont(path, PdfEncodings.IDENTITY_H, true);
}
and use it like :
paragraph.Add(source)
.SetFont(FontFactory.CreateOpenSansRegularFont()); //set font in here
table.AddCell(new Cell(1, 1)
.SetTextAlignment(iText.Layout.Properties.TextAlignment.CENTER)
.Add(paragraph));
This is how I used font factory for Turkish characters ex: "ü,i,ç,ş,ö"
For Xamarin-Android, you could try
string documentsPath = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);
var path = Path.Combine(documentsPath, "Fonts/Arial.ttf");
Look i fixed it in java,this may help you:
String font = "your Arabic font";
//the magic is in the next 4 lines:
PdfFontFactory.register(font);
FontProgram fontProgram = FontProgramFactory.createFont(font, true);
PdfFont f = PdfFontFactory.createFont(fontProgram, PdfEncodings.IDENTITY_H);
LanguageProcessor languageProcessor = new ArabicLigaturizer();
//and look here how i used setBaseDirection and don't use TextAlignment ,it will work without it
com.itextpdf.kernel.pdf.PdfDocument tempPdfDoc = new com.itextpdf.kernel.pdf.PdfDocument(new PdfReader(pdfFile.getPath()), TempWriter);
com.itextpdf.layout.Document TempDoc = new com.itextpdf.layout.Document(tempPdfDoc);
com.itextpdf.layout.element.Paragraph paragraph0 = new com.itextpdf.layout.element.Paragraph(languageProcessor.process("الاستماره الالكترونية--الاستماره الالكترونية--الاستماره الالكترونية--الاستماره الالكترونية"))
.setFont(f).setBaseDirection(BaseDirection.RIGHT_TO_LEFT)
.setFontSize(15);

When using iText to generate a PDF, if I need to switch fonts many times the file size becomes too large

I have a section of my PDF in which I need to use one font for its unicode symbol and the rest of the paragraph should be a different font. (It is something like "1. a 2. b 3. c" where "1." is the unicode symbol/font and "a" is another font) I have followed the method Bruno describes here: iText 7: How to build a paragraph mixing different fonts? and it works fine to generate the PDF. The issue is that the file size of the PDF goes from around 20MB to around 100MB compared to using only one font and one Text element. This section is used repeatedly in the document thousands of times. I am wondering if there is a way to reduce the impact of switching fonts or to reduce the file size of the entire document in some way.
Style creation pseudocode:
Style style1 = new Style();
Style style2 = new Style();
PdfFont font1 = PdfFontFactory.createFont(FontProgramFactory.createFont(fontFile1), PdfEncodings.IDENTITY_H, true);
style1.setFont(font1).setFontSize(8f).setFontColor(Color.DARK_GRAY);
PdfFont font2 = PdfFontFactory.createFont(FontProgramFactory.createFont(fontFile2), "", false);
style2.setFont(font2).setFontSize(8f).setFontColor(Color.DARK_GRAY);
Writing text/paragraph pseudocode:
Div div = new Div().setPaddingLeft(3).setMarginBottom(0).setKeepTogether(true);
Paragraph paragraph = new Paragraph();
loop up to 25 times: {
Text unicodeText = new Text(unicodeSymbol + " ").addStyle(style1);
paragraph.add(unicodeText);
Text plainText = new Text(plainText + " ").addStyle(style2);
paragraph.add(plainText);
}
div.add(paragraph);
This writing of text/paragraph is done thousands of times and makes up most of the document. Basically the document consists of thousands of "buildings" that have corresponding codes and the codes have categories. I need to have the index for the category as the unicode symbol and then all of the corresponding codes within the paragraph for the building.
Here is reproducable code:
float offSet = 50;
Integer leading = 10;
DateFormat format = new SimpleDateFormat("yyyy_MM_dd_kkmmss");
String formattedDate = format.format(new Date());
String path = "/tmp/testing_pdf_"+formattedDate + ".pdf";
File targetPdfFile = new File(path);
PdfWriter writer = new PdfWriter(path, new WriterProperties().addXmpMetadata());
PdfDocument pdf = new PdfDocument(writer);
pdf.setTagged();
PageSize pageSize = PageSize.LETTER;
Document document = new Document(pdf, pageSize);
document.setMargins(offSet, offSet, offSet, offSet);
byte[] font1file = IOUtils.toByteArray(FileUtility.getInputStreamFromClassPath("fonts/Garamond-Premier-Pro-Regular.ttf"));
byte[] font2file = IOUtils.toByteArray(FileUtility.getInputStreamFromClassPath("fonts/Quivira.otf"));
PdfFont font1 = PdfFontFactory.createFont(FontProgramFactory.createFont(font1file), "", true);
PdfFont font2 = PdfFontFactory.createFont(FontProgramFactory.createFont(font2file), PdfEncodings.IDENTITY_H, true);
Style style1 = new Style().setFont(font1).setFontSize(8f).setFontColor(Color.DARK_GRAY);
Style style2 = new Style().setFont(font2).setFontSize(8f).setFontColor(Color.DARK_GRAY);
float columnGap = 5;
float columnWidth = (pageSize.getWidth() - offSet * 2 - columnGap * 2) / 3;
float columnHeight = pageSize.getHeight() - offSet * 2;
Rectangle[] columns = {
new Rectangle(offSet, offSet, columnWidth, columnHeight),
new Rectangle(offSet + columnWidth + columnGap, offSet, columnWidth, columnHeight),
new Rectangle(offSet + columnWidth * 2 + columnGap * 2, offSet, columnWidth, columnHeight)};
document.setRenderer(new ColumnDocumentRenderer(document, columns));
for (int j = 0; j < 5000; j++) {
Div div = new Div().setPaddingLeft(3).setMarginBottom(0).setKeepTogether(true);
Paragraph paragraph = new Paragraph().setFixedLeading(leading);
// StringBuilder stringBuilder = new StringBuilder();
for (int i = 0; i < 26; i++) {
paragraph.add(new Text("\u3255 ").addStyle(style2));
paragraph.add(new Text("test ").addStyle(style1));
// stringBuilder.append("\u3255 ").append(" test ");
}
// paragraph.add(stringBuilder.toString()).addStyle(style2);
div.add(paragraph);
document.add(div);
}
document.close();
In creating the reproducible code I have found this this is related to the document being tagged. If you remove the line that marks it as tagged it reduces the file size greatly.
You can also reduce the file size by using the commented out string builder with one font instead of two. (Comment out the two "paragraph.add"s in the for-loop) This mirrors the issue I have in my code.
The problem is not in fonts themselves. The issues comes from the fact that you are creating a tagged PDF. Tagged documents have a lot of PDF objects in them that need a lot of space in the file.
I wasn't able to reproduce your 20MB vs 100MB results. On my machine whether with one font or with two fonts, but with two Text elements, the resultant file size is ~44MB.
To compress file when creating large tagged documents, you should use full compression mode which compresses all PDF objects, not only streams.
To activate full compression mode, create a PdfWriter instance with WriterProperties:
PdfWriter writer = new PdfWriter(outFileName,
new WriterProperties().setFullCompressionMode(true));
This setting reduced the file size for me from >40MB to ~5MB.
Please note that you are using iText 7.0.x while 7.1.x line has already been released and is now the main line of iText, so I recommend that you update to the latest version.

PDFBox inserting data into multi-page doc

I'm trying to create a simple pdf multi-page document with fields in it. To do that I have a template pdf which in the code I clone this template as many times as needed in order to create the document itself.
The problem comes with inserting some data in it. The type of data I try to insert to the document is not supposed to change across the pages. Rather than that, it stays static in all pages, Like the "Pages" digit that represents the number of the pages that this document contains.
Now, Inside my template pdf I have some text fields like, for instance, "Shipper1" and "Pages". I want to be able to insert my data into this text fields so that all the pages in the document will have this values in their "Shipper1" and "Pages" fields.
My code currently does that only on the first page. It shows the data perfectly. On the other hand, when I go to another page, the data isn't shown there. It's just displays an empty field.
Here is the code where I initiate the pdf document:
static void initiatePdf() {
// Initiate a new PDF Box object and get the acro form from it
File file = new File(Constants.Paths.EMPTY_DOC)
PDDocument tempDoc
Evaluator evaluator = new Evaluator(metaHolder)
int numPages = evaluator.getNumOfPagesRequired(objects)
FieldRenamer renamer = new FieldRenamer()
PDResources res = new PDResources()
COSDictionary acroFormDict = new COSDictionary()
List<PDField> fields = []
Closure isFieldExist = {List<PDField> elements, String fieldName ->
elements.findAll{it.getFullyQualifiedName() == fieldName}.size() > 0
}
for(int i = 0; i < numPages; i++) {
tempDoc = new PDDocument().load(file)
PDDocumentCatalog docCatalog = tempDoc.getDocumentCatalog()
PDAcroForm acroForm = docCatalog.acroForm
PDPage page = (PDPage) docCatalog.getPages().get(0)
renamer.setCurrentForm(acroForm)
if(i == 0) {
res = acroForm.getDefaultResources()
acroFormDict.mergeInto(acroForm.getCOSObject())
renamer.renameFields(1)
} else
renamer.renameFields(i*10+1)
List<PDField> newFields = acroForm.fields.findAll { PDField newField ->
isFieldExist(fields, newField.getFullyQualifiedName()) == false
}
fields.addAll(newFields)
document.addPage(page)
}
PDAcroForm acroForm = new PDAcroForm(document, acroFormDict);
acroForm.setFields(fields)
acroForm.setDefaultResources(res);
document.documentCatalog.setAcroForm(acroForm)
}
A couple of things first:
metaHolder instance holds the information about all
the fields that reside inside the acro form. the info is: Field Name, Field Widget Width, Field Font and Font size
evaluator is just and instance of the Evaluator class. Its purpose is to analyze the dynamic data and decide how many pages will take to contain all that text data.
Here is where I try to populate the fields with text:
static void populateData() {
def properties = ["$Constants.Fields.SHIPPER" : "David"]
FieldPopulater populater = new FieldPopulater(document, metaHolder)
populater.populateStaticFields(properties)
}
FieldPopulater class:
package app.components
import app.StringUtils
import app.components.entities.DGObject
import app.components.entities.FieldMeta
import org.apache.pdfbox.pdmodel.PDDocument
import org.apache.pdfbox.pdmodel.interactive.form.PDAcroForm
import org.apache.pdfbox.pdmodel.interactive.form.PDField
/**
* Created by David on 18/10/2016.
*/
class FieldPopulater {
PDAcroForm acroForm
FormMetaHolder metaHolder
FieldPopulater(PDDocument document, FormMetaHolder metaHolder) {
this.acroForm = document.getDocumentCatalog().acroForm
this.metaHolder = metaHolder
}
void populateStaticFields(properties) {
List<PDField> fields = []
properties.each {fieldName, data ->
FieldMeta fieldMeta = metaHolder.getMetaData(fieldName)
fields = acroForm.fields.findAll { PDField field ->
String currentName = field.getFullyQualifiedName()
char lastChar = currentName[-1]
if(Character.isDigit(lastChar)) {
currentName = currentName.substring(0,currentName.size()-1)
}
currentName == fieldName
}
if(fields.size() > 1) {
int counter = 1
String tempData = data
String currentFitData
while(tempData.isEmpty() != true) {
int maxWords = Utils.getMaxWords(tempData, fieldMeta)
currentFitData = StringUtils.getTextByWords(tempData, maxWords)
tempData = StringUtils.chopTextByWords(tempData, maxWords)
PDField field = fields.find{it.getFullyQualifiedName()[-1] == "$counter"}
field?.setValue(currentFitData)
counter++
}
} else {
PDField tempField = fields[0]
tempField.setValue(data)
}
}
}
}
The result is, in the first page, the field "Shipper" has a value of "David"
In the second page, the field "Shipper" is empty.
Here is an image. First page:
Second page:
What is the problem here?
UPDATE: I tried to add the widgets of every new acro form to the current page so that every field will a few kids widgets that will represent the field, but it still doesn't work.
// All the widgets that are associated with the fields
List<PDAnnotationWidget> widgets = acroForm.fields.collect {PDField field -> field.getWidgets().get(0)}
page.annotations.addAll(widgets)
UPDATE: I also tried to add the current widget of a field to the parent field's collection of widgets. Here is the code:
List<PDAnnotationWidget> widgets = []
// All the widgets that are associated with the fields
acroForm.fields.each {PDField field ->
PDAnnotationWidget widget = field.widgets.get(0)
// Adding the following widget to the page and to the field's list of annotation widgets
widgets.add(widget)
fields.find {it.getFullyQualifiedName() == field.getFullyQualifiedName()}?.widgets.add(widget)
}
page.annotations.addAll(widgets)
What you want is to have sereval visual representations of the same field. This is done by having several annotation widgets for such a field.
In PDF, when a field has only one annotation widget, they share a common dictionary. When it has several, the annotation widgets are in a child list of the field.
When you want several annotation widgets for one field, you need to create the annotation widgets with new PDAnnotationWidget() instead of calling field.getWidgets().get(0) and using that one. These widgets must be added to a list, and this list must be assigned to the field with setWidgets(). And for each widget you must call setRectangle() and setPage() and setParent().
An example for this is in the new CreateMultiWidgetsForm.java example. The setParent() method is not yet available in 2.0.3 (but will be in 2.0.4). In this answer, it is replaced with a call that does the same thing in a less elegant way.
public final class CreateMultiWidgetsForm
{
private CreateMultiWidgetsForm()
{
}
public static void main(String[] args) throws IOException
{
// Create a new document with 2 empty pages.
PDDocument document = new PDDocument();
PDPage page1 = new PDPage(PDRectangle.A4);
document.addPage(page1);
PDPage page2 = new PDPage(PDRectangle.A4);
document.addPage(page2);
// Adobe Acrobat uses Helvetica as a default font and
// stores that under the name '/Helv' in the resources dictionary
PDFont font = PDType1Font.HELVETICA;
PDResources resources = new PDResources();
resources.put(COSName.getPDFName("Helv"), font);
// Add a new AcroForm and add that to the document
PDAcroForm acroForm = new PDAcroForm(document);
document.getDocumentCatalog().setAcroForm(acroForm);
// Add and set the resources and default appearance at the form level
acroForm.setDefaultResources(resources);
// Acrobat sets the font size on the form level to be
// auto sized as default. This is done by setting the font size to '0'
String defaultAppearanceString = "/Helv 0 Tf 0 g";
acroForm.setDefaultAppearance(defaultAppearanceString);
// Add a form field to the form.
PDTextField textBox = new PDTextField(acroForm);
textBox.setPartialName("SampleField");
// Acrobat sets the font size to 12 as default
// This is done by setting the font size to '12' on the
// field level.
// The text color is set to blue in this example.
// To use black, replace "0 0 1 rg" with "0 0 0 rg" or "0 g".
defaultAppearanceString = "/Helv 12 Tf 0 0 1 rg";
textBox.setDefaultAppearance(defaultAppearanceString);
// add the field to the AcroForm
acroForm.getFields().add(textBox);
// Specify 1st annotation associated with the field
PDAnnotationWidget widget1 = new PDAnnotationWidget();
PDRectangle rect = new PDRectangle(50, 750, 250, 50);
widget1.setRectangle(rect);
widget1.setPage(page1);
widget1.getCOSObject().setItem(COSName.PARENT, textBox);
// Specify 2nd annotation associated with the field
PDAnnotationWidget widget2 = new PDAnnotationWidget();
PDRectangle rect2 = new PDRectangle(200, 650, 100, 50);
widget2.setRectangle(rect2);
widget2.setPage(page2);
widget2.getCOSObject().setItem(COSName.PARENT, textBox);
// set green border and yellow background for 1st widget
// if you prefer defaults, just delete this code block
PDAppearanceCharacteristicsDictionary fieldAppearance1
= new PDAppearanceCharacteristicsDictionary(new COSDictionary());
fieldAppearance1.setBorderColour(new PDColor(new float[]{0,1,0}, PDDeviceRGB.INSTANCE));
fieldAppearance1.setBackground(new PDColor(new float[]{1,1,0}, PDDeviceRGB.INSTANCE));
widget1.setAppearanceCharacteristics(fieldAppearance1);
// set red border and green background for 2nd widget
// if you prefer defaults, just delete this code block
PDAppearanceCharacteristicsDictionary fieldAppearance2
= new PDAppearanceCharacteristicsDictionary(new COSDictionary());
fieldAppearance2.setBorderColour(new PDColor(new float[]{1,0,0}, PDDeviceRGB.INSTANCE));
fieldAppearance2.setBackground(new PDColor(new float[]{0,1,0}, PDDeviceRGB.INSTANCE));
widget2.setAppearanceCharacteristics(fieldAppearance2);
List <PDAnnotationWidget> widgets = new ArrayList<PDAnnotationWidget>();
widgets.add(widget1);
widgets.add(widget2);
textBox.setWidgets(widgets);
// make sure the annotations are visible on screen and paper
widget1.setPrinted(true);
widget2.setPrinted(true);
// Add the annotations to the pages
page1.getAnnotations().add(widget1);
page2.getAnnotations().add(widget2);
// set the field value
textBox.setValue("Sample field");
document.save("MultiWidgetsForm.pdf");
document.close();
}
}