Javamail multiparts not ordered correctly - image

I have an application that attempts to dynamically build a Javamail message, assembling Mime body parts that are available at mail time. Each image is to have a 'GPC' image at the top, followed by some HTML text(body), a closing constructed by HTML, a closing 'Brand' image, and final part of the closing, also in HTML. File attachments may or may not be included. An error disclaimer(HTML) may precede the first image if applicable.
The disclaimer, body, closing, & attachment(s) are body parts, while the images are nested multiparts, consisting of HTML containers holding CIDs, and the image body parts themselves. When the email is sent, it appears correct in Gmail & Lotus Notes, while in Outlook(Hotmail) the GPC image is overwritten at the top by the Brand image. The order of the body parts is preserved, with the exception of the images, which always seem to revert to the top.
I cannot figure this out. It seems to me that the 'parent' multipart should be subtype 'Mixed' as the order of the body parts is important, and they are relatively unrelated, whereas the nested multiparts are 'Related' as one body part references the other. My code:
public MimeMultipart email_content_builder() throws MessagingException {
MimeMultipart mp = new MimeMultipart();
MimeBodyPart txt_part = new MimeBodyPart(), att_part, err_part, gpc_img_location_part, gpc_img_part, brand_img_location_part, brand_img_part, closing_pt1_part, closing_pt2_part;
DataSource att_source, gpc_img_src, brand_img_src;
String gpc_img_container_html, brand_img_container_html;
//Insert error disclaimer, if applicable:
if (!error_disclaimer.isEmpty()) {
err_part = new MimeBodyPart();
err_part.setText(error_disclaimer, "ISO-8859-1", "html");
mp.addBodyPart(err_part);
}
//Insert GPC logo image, if applicable:
if (GPC_logo.length > 0) {
MimeMultipart gpc_imagery_mp = new MimeMultipart("related");
MimeBodyPart gpc_imagery_part = new MimeBodyPart();
//When resizing the GPC image, the height should be roughly 23% of the width; use this factor: .2331
gpc_img_container_html = "<html><body><img src='cid:gpc_logo' height=\"23px\" width=\"100px\"></body></html>";
gpc_img_location_part = new MimeBodyPart();
gpc_img_location_part.setContent(gpc_img_container_html, "text/html");
gpc_imagery_mp.addBodyPart(gpc_img_location_part);
gpc_img_part = new MimeBodyPart();
gpc_img_src = new ByteArrayDataSource(GPC_logo, "image/jpeg");
gpc_img_part.setDataHandler(new DataHandler(gpc_img_src));
gpc_img_part.setHeader("Content-ID", "<gpc_logo>");
gpc_img_part.setDisposition(MimeBodyPart.INLINE);
gpc_imagery_mp.addBodyPart(gpc_img_part);
gpc_imagery_part.setContent(gpc_imagery_mp);
mp.addBodyPart(gpc_imagery_part);
}
//Insert main body of email:
txt_part.setText(body, "ISO-8859-1", "html");
mp.addBodyPart(txt_part);
//Insert the first part of the closing, if applicable:
if (!Closing_Part1.isEmpty()) {
closing_pt1_part = new MimeBodyPart();
closing_pt1_part.setText(Closing_Part1, "ISO-8859-1", "html");
mp.addBodyPart(closing_pt1_part);
}
//Insert brand logo image, if applicable:
if (brand_logo.length > 0) {
MimeMultipart brand_imagery_mp = new MimeMultipart("related");
MimeBodyPart brand_imagery_part = new MimeBodyPart();
//When resizing the brand image, the height should be roughly 43% of the width; use this factor: .4294
brand_img_container_html = "<html><body><img src='cid:brand_logo' height=\"64px\" width=\"150px\"></body></html>";
brand_img_location_part = new MimeBodyPart();
brand_img_location_part.setContent(brand_img_container_html, "text/html");
brand_imagery_mp.addBodyPart(brand_img_location_part);
brand_img_part = new MimeBodyPart();
brand_img_src = new ByteArrayDataSource(brand_logo, "image/jpeg");
brand_img_part.setDataHandler(new DataHandler(brand_img_src));
brand_img_part.setHeader("Content-ID", "<brand_logo>");
brand_img_part.setDisposition(MimeBodyPart.INLINE);
brand_imagery_mp.addBodyPart(brand_img_part);
brand_imagery_part.setContent(brand_imagery_mp);
mp.addBodyPart(brand_imagery_part);
}
//Insert the second part of the closing, if applicable:
if (!Closing_Part2.isEmpty()) {
closing_pt2_part = new MimeBodyPart();
closing_pt2_part.setText(Closing_Part2, "ISO-8859-1", "html");
mp.addBodyPart(closing_pt2_part);
}
//Insert attachments, if applicable:
if (attachments != null) {
for (int j = 0; j < attachments.size(); j++) {
att_part = new MimeBodyPart();
att_source = new FileDataSource((attachments.get(j)).getPath());
att_part.setDataHandler(new DataHandler(att_source));
att_part.setFileName((attachments.get(j)).getPath());
mp.addBodyPart(att_part);
}
}
return mp;
}

You have very limited control over how different mail readers will display the multiple body parts in your message. You best bet is to put all of the non-attachment content into a single text/html body part, using html to format the message. You can include images in your message using multipart/related, but it's often simpler to reference images on the web.

Related

iText PDF using .getSplitRenderer for Table renderer

In iText PDF 7, I am using the .layout method of the Table renderer to determine whether a table will break across a page.
However, when I add the .getSplitRenderer (returned from the layout result object) as a child of the Documents's renderer, I get this error: "java.lang.IndexOutOfBoundsException".
I'm using iText PDF version 7.1.7 in its Java incarnation. The last three entries in the stacktrace are:
java.util.ArrayList$SubList.rangeCheck(ArrayList.java:1225)
java.util.ArrayList$SubList.get(ArrayList.java:1042)
com.itextpdf.layout.renderer.TableBorders.processAllBordersAndEmptyRows(TableBorders.java:139)
Here is a bare-bones version of the code that triggers the error:
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
PdfWriter pdfWriter = new PdfWriter(outputStream);
PdfDoc pdfDoc = new PdfDocument(pdfWriter);
PageSize pageSize = new PageSize(612, 792);
Document doc = new Document(pdfDoc, pageSize);
Table table = new Table([50, 50, 50]);
for (int i = 0; i < 1000; i++) {
for (int j = 0; j < 3; j++) {
Cell cell = new Cell();
cell.setHeight(100);
table.addCell(cell);
}
}
LayoutContext context = new LayoutContext(doc.getRenderer().getCurrentArea().clone());
TableRenderer tableRenderer = (TableRenderer)table.createRendererSubTree();
LayoutResult result = tableRenderer.setParent(doc.getRenderer()).layout(context);
if (result.getStatus() == result.PARTIAL) {
tableRenderer = (TableRenderer) result.getSplitRenderer();
doc.getRenderer().addChild(tableRenderer); // this is where the error occurs
}
When you add a child to the DocumentRenderer it will layout and draw it automatically. It is not possible to layout a renderer several times in most cases (although what can be improved here is the exception type and message).
If you want to draw the part that fits immediately you can use the following line:
tableRenderer.draw(new DrawContext(pdfDocument, new PdfCanvas(pdfDocument.getPage(doc.getRenderer().getCurrentArea().getPageNumber()))));
Complete if expression:
if (result.getStatus() == LayoutResult.PARTIAL) {
tableRenderer = (TableRenderer) result.getSplitRenderer();
tableRenderer.draw(new DrawContext(pdfDocument, new PdfCanvas(pdfDocument.getPage(doc.getRenderer().getCurrentArea().getPageNumber()))));
}
It might have some drawbacks in complex cases though, so if you are dealing with complex layout or tagged documents I would recommend using binary search to determine the amount of content that still fits and add that content as an element to Document instance still.
An approach that is between those two is adding the table completely and then removing the extra pages from PdfDocument. In this case keep in mind that you will have to recreate the DocumentRenderer because it does not keep track of low level events like page removal from PdfDocument.

Issue scaling the first page of a PDF using iText7 for .NET

I'm trying to scale the first page of a PDF using iText7 for .NET. The rest of the pages should remain untouched.
The method below works if the PDF contains one page, but if there's multiple pages, the first (supposed to be scaled) page is blank, while the remaining pages is added correctly.
What am I missing here?
public byte[] ScaleFirstPagePdf(byte[] pdf)
{
using (var inputStream = new MemoryStream(pdf))
using (var outputStream = new MemoryStream(pdf))
using (var srcPdf = new PdfDocument(new PdfReader(inputStream)))
using (var destPdf = new PdfDocument(new PdfWriter(outputStream)))
{
for (int pageNum = 1; pageNum <= srcPdf.GetNumberOfPages(); pageNum++)
{
var srcPage = srcPdf.GetPage(pageNum);
var srcPageSize = srcPage.GetPageSizeWithRotation();
if (pageNum == 1)
{
var destPage = destPdf.AddNewPage(new PageSize(srcPageSize));
var canvas = new PdfCanvas(destPage);
var transformMatrix = AffineTransform.GetScaleInstance(0.5f, 0.5f);
canvas.ConcatMatrix(transformMatrix);
var pageCopy = srcPage.CopyAsFormXObject(destPdf);
canvas.AddXObject(pageCopy, 0, 0);
}
else
{
destPdf.AddPage(srcPage.CopyTo(destPdf));
}
}
destPdf.Close();
srcPdf.Close();
return outputStream.ToArray();
}
}
I couldn't reproduce the blank page issue with this code, but definitely the files that are generated in this way can be problematic.
The issue is that you are sharing a byte buffer between two memory streams - one used for reading and another one for writing, simultaneously.
Simply using another buffer or relying on the default MemoryStream implementation solved the issue for me, and should do so for you as well because there doesn't seem to be anything suspicious about your code apart from the problem I mentioned.
Here is how you should create the output stream:
using (var inputStream = new MemoryStream(pdf))
using (var outputStream = new MemoryStream())
If you still experience issues even after this tweak then the problem is definitely file-specific and I doubt you could get any help without sharing the file.

How to add multiple Textfields in single or multiple pages in a Loop

I am Using Itext 5 maven and I want to add multiple textfields in multiple pdf pages. like page 1 need 3 fields, page 2 need 4 fields etc.
I have write the below code
public byte[] setupDocument(EditPdfDTO editPdfDTOList, MultipartFile attachment)
{
WritePDF obj = new WritePDF();
Document document = null;
PdfWriter writer = null;
PdfImportedPage page = null;
PdfReader reader = null;
try
{
// Create output PDF
document = new Document(PageSize.A4);
document.setMargins(0, 0, 0, 0);
writer = PdfWriter.getInstance(document,
new FileOutputStream("D:/test.pdf"));
document.open();
PdfContentByte cb = writer.getDirectContent();
// Load existing PDF
reader = new PdfReader(attachment.getBytes());
int totalPages = reader.getNumberOfPages();
for (int i = 0; i < totalPages; i++)
{
page = writer.getImportedPage(reader, i + 1);
document.newPage();
cb.addTemplate(page, 0, 0);
for (int j = 0; j < editPdfDTOList.getPdf().size(); j++)
{
if (i + 1 == editPdfDTOList.getPdf().get(j).getPageNo())
{
BaseFont baseFont = null;
try
{
baseFont = BaseFont.createFont();
}
catch (DocumentException | IOException e1)
{
e1.printStackTrace();
}
int a, b;
a = editPdfDTOList.getPdf().get(j).getxCoordinate();
b = editPdfDTOList.getPdf().get(j).getyCoordinate();
String str = editPdfDTOList.getPdf().get(j).getTextContent();
Rectangle linkLocation =
new Rectangle(a, b + baseFont.getDescentPoint(str, 10),
a + 10 + baseFont.getWidthPoint(str, 10),
b + baseFont.getAscentPoint(str, 10) + 10);
TextField field =
new TextField(writer, linkLocation, "user1" + j+UUID.randomUUID());
field.setFontSize(10);
field.setOptions(TextField.MULTILINE | TextField.READ_ONLY);
field.setTextColor(BaseColor.RED);
field.setText(str);
field.setBorderWidth(1);
cb = writer.getDirectContent();
try
{
cb.addAnnotation(field.getTextField(),false);
}
catch (IOException | DocumentException e)
{
e.printStackTrace();
}
}
}
}
}
catch (DocumentException | IOException e)
{
e.printStackTrace();
}
catch (Exception e)
{
e.printStackTrace();
}
finally
{
document.close();
}
return null;
}
this code is able to add only one Textfield on every expected but not to add 2 or many textfields in a single page.
there is no issue of multiple try--catch block.
The appropriate classes to use
First of, you say you "want to add multiple textfields in multiple pdf pages". When implementing tasks like this, i.e. tasks that take a single document and want to somehow manipulate it while keeping it structurally more or less as before, one should usually work with a PdfReader/PdfStamper couple. This allows you to concentrate on the manipulation and provides a copy of the original PDF with all its properties to work on.
Adding multiple fields to a page of an existing PDF
Adding multiple fields to a single existing page is trivial, e.g.:
PdfReader pdfReader = new PdfReader(resource);
PdfStamper pdfStamper = new PdfStamper(pdfReader, output);
TextField field1 = new TextField(pdfStamper.getWriter(),
new Rectangle(100, 800, 200, 820), "Field1");
field1.setBorderColor(BaseColor.CYAN);
field1.setBorderStyle(PdfBorderDictionary.STYLE_DASHED);
field1.setBorderWidth(BaseField.BORDER_WIDTH_MEDIUM);
field1.setText("Field 1");
pdfStamper.addAnnotation(field1.getTextField(), 1);
TextField field2 = new TextField(pdfStamper.getWriter(),
new Rectangle(300, 800, 400, 820), "Field2");
field2.setBorderColor(BaseColor.RED);
field2.setBorderStyle(PdfBorderDictionary.STYLE_INSET);
field2.setBorderWidth(BaseField.BORDER_WIDTH_THIN);
field2.setText("Field 2");
pdfStamper.addAnnotation(field2.getTextField(), 1);
pdfStamper.close();
(AddField test testAddMultipleFields)
Applied to my example document
the code generates
Thus, there is no conceptual problem adding multiple text fields to the same document page, it works in a very natural manner.
In your case I would switch to using a PdfReader/PdfStamper couple. If some issue still remain, I would inspect your data. Probably they simply contain only a single field dataset per page. Or two textfields have the same coordinates and, therefore, look like one. Or some text fields have off-screen coordinates. Or... Or... Or...
The original answer
Originally the code in the question looked differently. This original answer focused on issues of that code.
You claim your code
is able to add only one Textfield on every expected but not to add 2 or many textfields in a single page
I doubt that because
you have two distinct objects writing to the same file "D:/TemplateFilePDf/" + attachment.getOriginalFilename() concurrently, the PdfWriter writer and the PdfStamper stamper. If you get something sensible as a result of your code, then only by pure luck; and
additionally stamper is instantiated for a null instance of PdfReader. This actually will cause a NullPointerException in the constructor which will keep your textfield adding code from being executed at all.
Thus, either the code you shared is considerably different from the code you run or your test runs actually all throw that NullPointerException and you probably find the outputs of a former, less broken version of your code which happens to have added only a single text field.
After fixing those two issues, some questions still remain (e.g. what is the intention of that cb.fill()? That instruction is only allowed directly after a path definition, the path whose inner area to fill, but I don't see you defining any path).
Furthermore, you access your editPdfDTOList for a lot of relevant values but we don't know those values. Thus, we cannot run your code to try and reproduce the issue. Probably you create only a single textfield because that object contains only values for a single textfield...

Missing font resource dictionary after stamping text in iText 7 [duplicate]

This question already has an answer here:
Why font is not being embedded to the PDF using content stream before in Itext7?
(1 answer)
Closed 4 years ago.
I have an existing PDF that doesn't contain any fonts (image only). I want to stamp some additional text onto the first page using low level canvas operations. When I do this in iText 7, the resulting PDF is missing the Fonts resource dictionary entry (which results in an NPE when parsing the resulting file).
Do I have to do something besides canvas.setFontAndSize() to get the font resource to get added to the output?
Here's a unit test that recreates the issue:
public class CheckFontResourceInclusion {
#Test
public void test() throws Exception {
// create a document to stamp
ByteArrayOutputStream baos = new ByteArrayOutputStream();
try(PdfDocument doc = new PdfDocument(new PdfWriter(baos))){
doc.addNewPage();
}
// stamp it
StampingProperties stampProps = new StampingProperties();
PdfFont font = PdfFontFactory.createFont();
ByteArrayOutputStream resultStream = new ByteArrayOutputStream();
try(PdfDocument doc = new PdfDocument(new PdfReader(new ByteArrayInputStream(baos.toByteArray())), new PdfWriter(resultStream), stampProps)){
PdfPage page = doc.getPage(1);
PdfCanvas canvas = new PdfCanvas(page.newContentStreamAfter(), new PdfResources(), doc);
canvas.beginText();
canvas.setTextRenderingMode(2);
canvas.setFontAndSize(font, 42);
canvas.setTextMatrix(1, 0, 0, -1, 100, 100);
canvas.showText("TEXT TO STAMP");
canvas.endText();
}
// parse text
try(PdfDocument doc = new PdfDocument(new PdfReader(new ByteArrayInputStream(resultStream.toByteArray())))){
LocationTextExtractionStrategy strat = new LocationTextExtractionStrategy();
PdfCanvasProcessor processor = new PdfCanvasProcessor(strat);
processor.processPageContent(doc.getPage(1));
Assert.assertEquals("TEXT TO STAMP", strat.getResultantText());
}
}
}
Here's the resulting failure:
java.lang.NullPointerException
at com.itextpdf.kernel.pdf.canvas.parser.PdfCanvasProcessor$SetTextFontOperator.invoke(PdfCanvasProcessor.java:811)
at com.itextpdf.kernel.pdf.canvas.parser.PdfCanvasProcessor.invokeOperator(PdfCanvasProcessor.java:456)
at com.itextpdf.kernel.pdf.canvas.parser.PdfCanvasProcessor.processContent(PdfCanvasProcessor.java:285)
at com.itextpdf.kernel.pdf.canvas.parser.PdfCanvasProcessor.processPageContent(PdfCanvasProcessor.java:306)
at
The error is the same as in this earlier question: You use a throw-away resources object, so the font resource is missing in the result.
You can fix this by using the actual page resources. Simply replace
PdfCanvas canvas = new PdfCanvas(page.newContentStreamAfter(), new PdfResources(), doc);
by
PdfCanvas canvas = new PdfCanvas(page.newContentStreamAfter(), page.getResources(), doc);

How to get inserted image in a received gmail mail

I want to get the inserted image (not image attachment) in a received Gmail mail. How is this done using Google Apps Script?
I have tried to use urlfetchapp to get the image data from the image link address:
https//mail.google.com/mail/u/0/?ui=2&ik=c348ccba97&view=att&th=13bb892c352c45dc&attid=0.2&disp=emb&realattid=ii_13bb88f3e56aae8c&zw&atsh=1
in the email html body, but can not bypass the google login page. The response content data always is login page.
Edit: While the code below works on email drafts and canned responses, it does not let you access inline images in received email.
This code snippet is from a working mail merge I have that supports inline images (both embedded blobs and external references) and attachments as well. It takes the inline images and attachments from a draft/canned response, adds them to a new email and sends it:
...
//selectedTemplate is a Gmail Message (draft/canned response)
var emailTemplate = selectedTemplate.getBody();
var attachments = selectedTemplate.getAttachments();
if(emailTemplate.search(/<\img/ != -1)){
var inlineImages = {};
var imgVars = emailTemplate.match(/<img[^>]+>/g);
for(i in imgVars){
var title = imgVars[i].match(/title="([^\"]+\")/);
if (title) {
title = title[1].substr(0, title[1].length-1);
var titleEncoded = title.replace(/ /g,"-");
for(j in attachments){
if(attachments[j].getName() == title){
inlineImages[titleEncoded] = attachments[j].copyBlob().setName(titleEncoded);
attachments.splice(j,1);
}
}
var newImg = imgVars[i].replace(/src="[^\"]+\"/,"src=\"cid:"+titleEncoded+"\"");
emailTemplate = emailTemplate.replace(imgVars[i],newImg);
}
}
}
...
GmailApp.sendEmail("test#example.com", "my subject", "my body",
{attachments: attachments,
inlineImages: inlineImages});
Hope this helps.
I don't know how you are accessing your emails, but it doesn't look like you are using the GmailApp from the Gmail service. Once you've found the email you're interested in then examine the html body to find the <img>. Either src will be a url (linked image) or a cid (embedded image) that references an attachment.
A more recent approach to solving this.
The issue
For example, here's an email body retrieved with .getBody()
<div dir="ltr"><div><img src="?view=att&th=1401f70d4881e07f&attid=0.3&disp=emb&realattid=ii_1401f6fc7824ebe1&zw&atsh=1" alt="Inline image 4" width="200" height="180"><br></div><div><br></div><img src="?view=att&th=1401f70d4881e07f&attid=0.2&disp=emb&realattid=ii_1401f6e6c1d46c4b&zw&atsh=1" alt="Inline image 2" width="200" height="65"><div><br></div><div>
jtykuykyu</div><div><br></div><div><img src="?view=att&th=1401f70d4881e07f&attid=0.1&disp=emb&realattid=ii_1401f6e9df3a4b1c&zw&atsh=1" alt="Inline image 3" width="200" height="82"><br><div><br></div><div><br></div></div></div>
And here is the list of attachments for the email (among which are our inline images):
[13-07-30 08:28:08:378 CEST] Screen Shot 2013-07-12 at 1.54.31 PM.png
[13-07-30 08:28:08:379 CEST] Screen Shot 2013-07-23 at 5.38.51 PM.png
[13-07-30 08:28:08:380 CEST] Screen Shot 2013-07-25 at 9.05.15 AM.png
[13-07-30 08:28:08:381 CEST] test2.png
As you can see, there's no link between the name of those images and the information available in the img tags, so there's no safe way to rebuild a correct email with only those information.
The solution
How to solve that ? We can use the method .getRawContent() to get the actual email and parse it to get the information we need. Specifically, this method give us a relationship between the name of an attachment and the 'realattid' available in the email body:
Content-Type: image/png; name="Screen Shot 2013-07-25 at 9.05.15 AM.png"
Content-Transfer-Encoding: base64
Content-ID:
X-Attachment-Id: ii_1401f6e9df3a4b1c
Code snippet
Here's a code snippet to:
-Retrieve the body & attachments of an email
-Get all the img tags inside the body and see which ones are linked to attachments in the email
-Get the 'realattid' of each image and use .getRawContent() to link this 'realattid' to the right attachment
-Replace the img tag to correctly link it to the right attachment
-Indicate that this attachment is no longer a simple attachment but an inline image
-Once all that is done you have all the data you need to send a copy of this email with the correct inline images displayed.
//////////////////////////////////////////////////////////////////////////////
// Get inline images and make sure they stay as inline images
//////////////////////////////////////////////////////////////////////////////
var emailTemplate = selectedTemplate.getBody();
var rawContent = selectedTemplate.getRawContent();
var attachments = selectedTemplate.getAttachments();
var regMessageId = new RegExp(selectedTemplate.getId(), "g");
if (emailTemplate.match(regMessageId) != null) {
var inlineImages = {};
var nbrOfImg = emailTemplate.match(regMessageId).length;
var imgVars = emailTemplate.match(/<img[^>]+>/g);
var imgToReplace = [];
if(imgVars != null){
for (var i = 0; i < imgVars.length; i++) {
if (imgVars[i].search(regMessageId) != -1) {
var id = imgVars[i].match(/realattid=([^&]+)&/);
if (id != null) {
var temp = rawContent.split(id[1])[1];
temp = temp.substr(temp.lastIndexOf('Content-Type'));
var imgTitle = temp.match(/name="([^"]+)"/);
if (imgTitle != null) imgToReplace.push([imgTitle[1], imgVars[i], id[1]]);
}
}
}
}
for (var i = 0; i < imgToReplace.length; i++) {
for (var j = 0; j < attachments.length; j++) {
if(attachments[j].getName() == imgToReplace[i][0]) {
inlineImages[imgToReplace[i][2]] = attachments[j].copyBlob();
attachments.splice(j, 1);
var newImg = imgToReplace[i][1].replace(/src="[^\"]+\"/, "src=\"cid:" + imgToReplace[i][2] + "\"");
emailTemplate = emailTemplate.replace(imgToReplace[i][1], newImg);
}
}
}
}
//////////////////////////////////////////////////////////////////////////////
var message = {
htmlBody: emailTemplate,
subject: selectedTemplate.getSubject(),
attachments: attachments,
inlineImages: inlineImages
}

Resources