Mailmerge pdf in nodejs and hummus-recipe - node.js

I'm trying to make a simple mail-merge where recipients information is inserted on top of a template pdf.
The template is 1 page, but the result can be up to several hundred pages.
I have all recipient in array objects, but need to find a way to loop over that and create a unique page for each recipient.
I'm not sure if hummus-recipe is the right tool, so would greatly appreciate any input as to how to do this.
My demo looks like this
const HummusRecipe = require('hummus-recipe')
const recepients = [
{name: "John Doe", address: "My Streetname 23", zip: "23456", city: "My City"},
{name: "Jane Doe", address: "Another Streetname 56 ", zip: "54432", city: "Her City"}
//'.......'
]
const template = 'template.pdf';
const pdfDoc = new HummusRecipe(template, 'output.pdf');
function createPdf(){
pdfDoc
.editPage(1)
.text(recepients[0].name, 30, 60)
.text(recepients[0].address, 30, 75)
.text(recepients[0].zip + ' ' + recepients[0].city, 30, 90)
.endPage()
//. appendpage(template) - tried versions of this in loops etc., but can't get it to work.
.endPDF();
}
createPdf();
This obviously only create a single page with recipient[0]. I have tried all sorts of ways to loop over using .appendpage(template), but I can't append and then edit the same page.
Any advice how to move forward from here?

After conversing with the creator of hummus-recipe and others, the solution became somewhat obvious. Its not possible to append a page and then modify it, and its not possible to modify the same page several times.
The solution then is to make the final pdf in two passes. First create a masterPdf where the template is appended in a for loop, save this file and then edit each of those pages.
I have created a working code as shown below. I have made the functions async as I need to run it on lambda an need som control over returns etc. Also this way its possible to save both the tmp-file and final pdf on AWS S3.
The code below takes about 60 seconds to create a 200 page pdf from a 1,6mb template. Any ideas for optimizations will be greatly appreciated.
const HummusRecipe = require('hummus-recipe');
const template = 'input/input.pdf';
const recipients = [
{name: 'John Doe', address: "My Streetname 23"},
{name: 'Jane Doe', address: "Another Streetname 44"},
//...etc.
]
async function createMasterPdf(recipientsLength) {
const key = `${(Date.now()).toString()}.pdf`;
const filePath = `output/tmp/${key}`;
const pdfDoc = new HummusRecipe(template, filePath)
for (i = 1; i < recipientsLength; i++) {
pdfDoc
.appendPage(template)
.endPage()
}
pdfDoc.endPDF();
return(filePath)
}
async function editMasterPdf(masterPdf, recipientsLength) {
const key = `${(Date.now()).toString()}.pdf`;
const filePath = `output/final/${key}`;
const pdfDoc = new HummusRecipe(masterPdf, filePath)
for (i = 0; i < recipientsLength; i++) {
pdfDoc
.editPage(i+1)
.text(recipients[i].name, 75, 100)
.text(recipients[i].address, 75, 115)
.endPage()
}
pdfDoc.endPDF()
return('Finished file: ' + filePath)
}
const run = async () => {
const masterPdf = await createMasterPdf(recipients.length);
const editMaster = await editMasterPdf(masterPdf, recipients.length)
console.log(editMaster)
}
run()

Related

How can I send image attachment to email

As I have created list of qr codes to scan and then I want to send this images to email by using Google app script.
current result
image is not sent to gmail.
codes
function sendEmail(sheetName = "emails") {
SpreadsheetApp.getActiveSpreadsheet().getSheetByName(sheetName).activate();
var spreedsheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
var lastRow = spreedsheet.getLastRow();
//var ssa = SpreadSheetApp().getSheetByName(sheetName); // spread sheet object
const messageBody = "Dear receiver, We would like to invite you on upcoming seminar please use this link {actual link} and show receiptionis this bar code {actual code}"
const subjectText = "Seminar invitation"
// loop rows or spread sheet
for (var i=2; i<= lastRow; i++) {
var receiverName = spreedsheet.getRange(i, 1).getValue();
var receiverEmail = spreedsheet.getRange(i, 2).getValue();
var seminarLink = spreedsheet.getRange(i, 3).getValue();
var barcode = spreedsheet.getRange(i, 4).getBlob();
var msgBdy = messageBody.replace("receiver",receiverName).replace("{actual link}", seminarLink);
var photoBlob = barcode
MailApp.sendEmail({
to: receiverEmail,
subject: subjectText,
htmlBody: messageBody + "<br /><img src='cid:qrCode'>",
inlineImages: { qrCode: photoBlob }
});
}
}
From your following reply,
images are created from =IMAGE("chart.googleapis.com/…) with that google sheet excel
In this case, I thought that your goal can be achieved using Google Apps Script.
When I saw your script, getValue() is used in a loop. In this case, the process cost will become high. In this modification, this issue is also modified.
Modified script:
function sendEmail(sheetName = "emails") {
var spreedsheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(sheetName);
var lastRow = spreedsheet.getLastRow();
const messageBody = "Dear receiver, We would like to invite you on upcoming seminar please use this link {actual link} and show receiptionis this bar code {actual code}";
const subjectText = "Seminar invitation";
const range = spreedsheet.getRange("A2:D" + lastRow);
const values = range.getValues();
const formulas = range.getFormulas();
values.forEach(([receiverName, receiverEmail, seminarLink], i) => {
const url = formulas[i][3].split('"')[1];
const photoBlob = UrlFetchApp.fetch(url).getBlob();
var msgBdy = messageBody.replace("receiver", receiverName).replace("{actual link}", seminarLink);
MailApp.sendEmail({
to: receiverEmail,
subject: subjectText,
htmlBody: messageBody + "<br /><img src='cid:qrCode'>",
inlineImages: { qrCode: photoBlob }
});
});
}
When this script is run, the values are retrieved from "A2:D" of the sheet. And, each value is retrieved from each row. In the case of the column "D", the URL is retrieved from the formula, and the image blob is retrieved using UrlFetchApp. And, those values are put as the email.
References
forEach()
fetch(url, params)
Update 2
As said in the comments, replace this line:
var barcode = spreedsheet.getRange(i, 4).getBlob();
To:
var barcode = UrlFetchApp.fetch(spreedsheet.getRange(i, 4).getFormula().match(/\"(.*?)\"/)[1]).getAs('image/png')
Update
As you are inserting the image via a IMAGE Formula, you can easily parse the value, obtain the URL and get the Blob via UrlFetchApp
Sample:
const sS = SpreadsheetApp.getActive()
function testSendImage() {
const img = sS.getRange('A1').getFormula()
const urlImg = img.match(/\"(.*?)\"/)[1]
const fetchImage = UrlFetchApp.fetch(urlImg).getAs('image/png')
MailApp.sendEmail({
to: "some#gmail.com",
subject: "QR",
htmlBody: 'Check this QR <img src="cid:fetchImage" />',
inlineImages: { fetchImage: fetchImage } })
}
In the current state, there is no method for extrancting the image from a cell, however there is a Feature Request on Google's Issue Tracker, you can click here to review it.
Remember to click in the top right if you want this feature to be implemented.
In any case, you review this StackOverflow question for workarounds or alternative methods.

discord.js v13 What code would I use to collect the first attachment (image or video) from a MessageCollector?

I've looked everywhere and tried all I could think of but found nothing, everything seemed to fail.
One bit of code I've used before that failed:
Message.author.send({ embeds: [AttachmentEmbed] }).then(Msg => {
var Collector = Msg.channel.createMessageCollector({ MessageFilter, max: 1, time: 300000 });
Collector.on(`collect`, Collected => {
if (Collected.content.toLowerCase() !== `cancel`) {
console.log([Collected.attachments.values()].length);
if ([Collected.attachments.values()].length > 0) {
var Attachment = [Collected.attachments.values()];
var AttachmentType = `Image`;
PostApproval(false, Mode, Title, Description, Pricing, Contact, Attachment[0], AttachmentType);
} else if (Collected.content.startsWith(`https://` || `http://`) && !Collected.content.startsWith(`https://cdn.discordapp.com/attachments/`)) {
var Attachment = Collected.content.split(/[ ]+/)[0];
var AttachmentType = `Link`;
PostApproval(false, Mode, Title, Description, Pricing, Contact, Attachment, AttachmentType);
console.log(Attachment)
} else if (Collected.content.startsWith(`https://cdn.discordapp.com/attachments/`)) {
var Attachment = Collected.content.split(/[ ]+/)[0];
var AttachmentType = `ImageLink`;
PostApproval(false, Mode, Title, Description, Pricing, Contact, Attachment, AttachmentType);
console.log(Attachment)
}
[Collected.attachments.values()].length will always be 1. Why? Well you have these 2 possibilities:
[ [] ] //length 1
[ [someMessageAttachment] ] //length 1
The proper way to check is using the spread operator (...)
[...(Collected.attachments.values())].length //returns amount of attachments in the message

Array list with 2 values and doing it to a top 10 list

im working on a Discord bot and have a reputation system with fs (npm package) and saving peoples reps in a file and doing the file name as they discord id
now im working on a top 10 command and would need some help here, i currently have this as code:
let users = [];
let reps = [];
fs.readdirSync('./data/reps/').forEach(obj => {
users.push(obj.replace('.json', ''))
let file = fs.readFileSync(`./data/reps/${obj}`)
let data = JSON.parse(file)
reps.push(data.reps)
})
let top = [...users, ...reps]
top.sort((a,b) => {a - b})
console.log(top)
the files form the users are like this:
{
"users": [
"437762415275278337"
],
"reps": 1
}
users are the current users that can't rep the persion anymore and don't need to use it in the command
i wan to get the top 10 of reps so that i can get the user id and how many reps they have, how could i do it with the code above?
You could try this
const topTen = fs.readdirSync('./data/reps/').map(obj => {
const file = fs.readFileSync(`./data/reps/${obj}`);
const data = JSON.parse(file);
return { ...data, name: obj.replace('.json', '') };
}).sort((a, b) => a.reps - b.reps).slice(0, 10);
console.log(topTen);
I would change how you push the data
const users = [];
fs.readdirSync('./data/reps/').forEach(obj => {
let file = fs.readFileSync(`./data/reps/${obj}`)
let data = JSON.parse(file)
reps.push({ reps: data.reps, id: obj.replace(".json", "") });
})
That way when you sort the array the id goes along with
//define this after the fs.readdirSync.forEach method
const top = users.sort((a,b)=> a.reps-b.reps).slice(0,10);
If you want an array of top ids
const topIds = top.map(e => e.id);
If you want a quick string of it:
const str = top.map(e => `${e.id}: ${e.reps}`).join("\n");
Also you should probably just have one or two json files, one would be the array of user id's and their reps and then the other could be of user id's and who they can't rep anymore

Gmail to Google Spread Sheet (only date, email and subject)

The code I have cobbled together does work, but it imports the wrong things from my email. I only want the date sent, the sender email address and the subject to import into the google sheet.
Can anyone help?
function onOpen() {
const spreadsheet = SpreadsheetApp.getActive();
let menuItems = [
{name: 'Gather emails', functionName: 'gather'},
];
spreadsheet.addMenu('SP LEGALS', menuItems);
}
function gather() {
let messages = getGmail();
let curSheet = SpreadsheetApp.getActive();
messages.forEach(message => {curSheet.appendRow(parseEmail(message))});
}
function getGmail() {
const query = "to:legals#salisburypost.com";
let threads = GmailApp.search(query,0,10);
let messages = [];
threads.forEach(thread => {
messages.push(thread.getMessages()[0].getPlainBody());
label.addToThread(thread);
});
return messages;
}
function parseEmail(message){
let parsed = message.replace(/,/g,'')
.replace(/\n*.+:/g,',')
.replace(/^,/,'')
.replace(/\n/g,'')
.split(',');
let result = [0,1,2,3,4,6].map(index => parsed[index]);
return result;
}
I believe your goal as follows.
You want to retrieve "the date, sender and subject" from the 1st message in the searched threads to the active sheet of Google Spreadsheet.
For this, how about this answer?
Modification points:
In this case, you can retrieve "the date, sender and subject" using the built-in methods for GmailApp.
When the values are put to the Spreadsheet, when appendRow is used in the loop, the process cost will become high.
It seems that label is not declared in your script.
When above points are reflected to your script, it becomes as follows.
Modified script:
In this modification, I modified gather() and getGmail() for achieving your goal.
function gather() {
let messages = getGmail();
if (messages.length > 0) {
let curSheet = SpreadsheetApp.getActiveSheet();
curSheet.getRange(curSheet.getLastRow() + 1, 1, messages.length, messages[0].length).setValues(messages);
}
}
function getGmail() {
const query = "to:legals#salisburypost.com";
let threads = GmailApp.search(query,0,10);
let messages = [];
threads.forEach(thread => {
const m = thread.getMessages()[0];
messages.push([m.getDate(), m.getFrom(), m.getSubject()]);
// label.addToThread(thread);
});
return messages;
}
When no threads are retrieved with "to:legals#salisburypost.com", the values are not put to the Spreadsheet. Please be careful this.
References:
getDate()
getFrom()
getSubject()
setValues(values)

Why does my for loop only goes through once when i call function inside it?

I got list of videos from API, it has list of urls fo thumbnail and i would like to combine thumbnails of each video to gif. When i loop through videos and don't generate gifs it goes through 5 times as expected, but if i include function that should generate gifs it only goes through once, without any errors. I have no idea what is happening
I'm using node.js, discord.js, get pixels and gif-encoder modules to generate thumbnails.
for(i=0;i<5;i++){
generateThumbnail(data[i].video.video_id,data[i].video.thumbs,function(){
var tags = '';
for(t=0;t<data[i].video.tags.length;t++){
tags = tags + data[i].video.tags[t].tag_name+', ';
}
fields = [
{name:data[i].video.title,
value:value},
{name:'Tags',
value:tags}
]
msg.channel.send({embed: {
color: 3447003,
thumbnail: {
"url": ""
},
fields: fields,
}});
});
}
function generateThumbnail(id,images,fn){
var pics = [];
console.log(id)
var file = require('fs').createWriteStream(id+'.gif');
var gif = new GifEncoder(images[0].width, images[0].height);
gif.pipe(file);
gif.setQuality(20);
gif.setDelay(1000);
gif.setRepeat(0)
gif.writeHeader();
for(i=0;i<images.length;i++){
pics.push(images[i].src)
}
console.log(pics)
addToGif(pics,gif);
fn()
}
var addToGif = function(images,gif, counter = 0) {
getPixels(images[counter], function(err, pixels) {
gif.addFrame(pixels.data);
gif.read();
if (counter === images.length - 1) {
gif.finish();
} else {
addToGif(images,gif, ++counter);
}
})
}
if i dont use GenerateThumbnail function it goes through 5 times as expected and everything works fine, but if i use it it goes through only once, and generated only 1 gif
Use var to declare for vars. Ie for(var i=0....
If you declare vars without var keyword, they are in the global scope. ..... and you are using another i var inside the function but now it is the same var from the outer for loop.

Resources