A few weeks ago I posted a tutorial on using PDFs with Eleventy. In that post I described how to use a data file to scan a directory of PDFs and make them available to a Liquid template. I then followed up that post with another, where I described using Adobe's PDF Tools API to generate thumbnail images from PDFs. I thought it would be nice to combine the two so I could have my Eleventy site both list the PDFs as well as generate thumbnails. Here's how that looks with me spending about five seconds on layout:
So how did I do it? Keep in mind I described most of the process in my earlier post ("Using the Adobe PDF Tools API to Generate Thumbnails"). The process boils down to:
- Use Adobe's PDF Tools API to generate a zip of images for each page of the PDF
- Extract the first file from the zip
- Resize
I took that logic and combined it with the code from the first demo ("Using PDFs with the Jamstack"). That process was:
- Use a glob pattern to get PDFs
- Create an array of those PDFs with names and such to make them easier to use in Liquid
- Use Eleventy pagination to generate an HTML page per PDF
- Use the Adobe PDF Embed API to render the PDF in the HTML layout
Here's the updated data file (named pdfs.js
):
require('dotenv').config()
const globby = require('globby');
const PDFToolsSdk = require('@adobe/documentservices-pdftools-node-sdk');
const nanoid = require('nanoid').nanoid;
const StreamZip = require('node-stream-zip');
const Jimp = require('jimp');
const fs = require('fs');
let creds = {
clientId:process.env.ADOBE_CLIENT_ID,
clientSecret:process.env.ADOBE_CLIENT_SECRET,
privateKey:process.env.ADOBE_KEY,
organizationId:process.env.ADOBE_ORGANIZATION_ID,
accountId:process.env.ADOBE_ACCOUNT_ID
}
const outputPath = './tmp/';
const thumbPath = "images/thumbs/";
module.exports = async function() {
let result = [];
let files = await globby('./pdfs/**/*.pdf');
for(let i=0; i < files.length; i++) {
let pdf = files[i];
//name safe for a directory
let name = pdf.split('/').pop().replace('.pdf', '');
//do we have a thumb, if so, its /path/foo.pdf => /path/foo.jpg
let thumb = pdf.replace('.pdf', '.jpg');
if(!fs.existsSync(thumb)) {
console.log('need to generate '+thumb);
let zip = await generateImageZip(pdf, creds, outputPath);
console.log(`image generated from source ${pdf} at ${zip}`);
let dest = await extractFirstFile(zip, outputPath);
console.log(`image extracted to ${dest}`);
await makeThumbnail(dest, 200, 80);
console.log('Done resizing image.');
//move to a new filename based on nanoid
fs.renameSync(dest, thumb);
//cleanup
fs.unlinkSync(zip);
}
result.push({
path:files[i],
name,
thumb
});
}
return result;
};
async function generateImageZip(pdfPath, credsPath, outputPath) {
return new Promise((resolve, reject) => {
let output = outputPath + nanoid() + '.zip';
const credentials = PDFToolsSdk.Credentials.serviceAccountCredentialsBuilder()
.withClientId(creds.clientId)
.withClientSecret(creds.clientSecret)
.withPrivateKey(creds.privateKey)
.withOrganizationId(creds.organizationId)
.withAccountId(creds.accountId)
.build();
const executionContext = PDFToolsSdk.ExecutionContext.create(credentials),
exportPDF = PDFToolsSdk.ExportPDF,
exportPdfOperation = exportPDF.Operation.createNew(exportPDF.SupportedTargetFormats.JPEG);
const input = PDFToolsSdk.FileRef.createFromLocalFile(pdfPath);
exportPdfOperation.setInput(input);
exportPdfOperation.execute(executionContext)
.then(result => result.saveAsFile(output))
.then(r => {
resolve(output);
})
.catch(err => {
if(err instanceof PDFToolsSdk.Error.ServiceApiError
|| err instanceof PDFToolsSdk.Error.ServiceUsageError) {
console.log('Exception encountered while executing operation', err);
} else {
console.log('Exception encountered while executing operation', err);
}
});
});
}
async function extractFirstFile(zip, outputPath) {
return new Promise(async (resolve, reject) => {
// Read the zip and extract the first file
let zipFile = new StreamZip.async({file: zip });
const entries = await zipFile.entries();
let first = Object.values(entries)[0];
let dest = outputPath + nanoid() + '.' + first.name.split('.').pop();
await zipFile.extract(first.name, dest );
await zipFile.close();
resolve(dest);
});
}
async function makeThumbnail(path, width, quality) {
const image = await Jimp.read(path);
await image.resize(width, Jimp.AUTO);
await image.quality(quality);
await image.writeAsync(path);
return true;
}
That's a bit long, but let me point out the highlights. First off, I modified my use of Adobe's Node SDK to use variables instead of files. This let me store everything in a .env
file that would be regular environment variables in production. That makes the initial setup a few more lines of code, but the code is safer to check into source control now:
const credentials = PDFToolsSdk.Credentials.serviceAccountCredentialsBuilder()
.withClientId(creds.clientId)
.withClientSecret(creds.clientSecret)
.withPrivateKey(creds.privateKey)
.withOrganizationId(creds.organizationId)
.withAccountId(creds.accountId)
.build();
I still use a glob to get my PDFs, but now I look for a corresponding filename with the .jpg
extension. If it doesn't exist, I generate the thumbnail. This makes it quite a bit more performant. In my initial version I simply regenerated it everytime, but while the API was pretty fast, that's still a lot of work I don't need to do more than once.
The other change was to include the thumb filename in the result data:
result.push({
path:files[i],
name,
thumb
});
And really, that's it. As I said, I did modify the homepage to show the thumbnails and used a bit of CSS, so if you're curious, you can peruse the entire codebase here: https://github.com/cfjedimaster/eleventy-demos/tree/master/pdftest2