Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: allow PDF upload for logo on campaign #1009

Merged
merged 4 commits into from
Nov 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 61 additions & 12 deletions packages/draft/src/pdf.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import async from 'async';
import { ReadableStream } from 'node:stream/web';
import * as handlebars from 'handlebars';
import path from 'node:path';
import { PDFDocument, PDFImage } from 'pdf-lib';
import { PDFDocument, PDFEmbeddedPage, PDFImage } from 'pdf-lib';
import puppeteer from 'puppeteer';
import { match } from 'ts-pattern';
import { Logger } from '@zerologementvacant/utils';
Expand Down Expand Up @@ -89,13 +89,17 @@ function createTransformer(opts: TransformerOptions) {
width: width,
height: height,
x: x,
y: y
y: y,
type: (image as Element)?.classList.contains('header__image') ? 'logo' : 'signature'
};
});
});
// Retrieve images from the HTML only once
if (images.length === 0) {
images.push(...elements);
images.push(...elements.map(element => ({
...element,
type: element.type as 'logo' | 'signature'
})));
}
// Save image positions
elements.forEach((element) => {
Expand Down Expand Up @@ -147,10 +151,29 @@ function createTransformer(opts: TransformerOptions) {
/**
* Embed an image into the PDF.
* @param pdf
* @param image A JPEG or PNG image encoded in base64
* @param image A PDF, JPEG or PNG image encoded in base64
*/
async embed(pdf: PDFDocument, image: Image): Promise<PDFImage> {
async embed(pdf: PDFDocument, image: Image): Promise<PDFImage | PDFEmbeddedPage> {
return match(image)
.when(
(image) => image.content.startsWith('data:application/pdf'),
async (image) => {
const content = image.content.substring(
image.content.indexOf('JVBER')
);

const binaryString = atob(content);
const len = binaryString.length;
const bytes = new Uint8Array(len);

for (let i = 0; i < len; i++) {
bytes[i] = binaryString.charCodeAt(i);
}

const [embeddedPage] = await pdf.embedPdf(bytes.buffer, [0]);
return embeddedPage;
}
)
.when(
(image) => image.content.startsWith('data:image/jpeg'),
async (image) => {
Expand Down Expand Up @@ -180,22 +203,47 @@ function createTransformer(opts: TransformerOptions) {
* @param pdf
*/
async save(pdf: PDFDocument): Promise<Buffer> {
const firstImageHeight = { 'logo': 0, 'signature': 0 };
const firstImageWidth = { 'logo': 0, 'signature': 0 };

// Embed images into the PDF
await async.forEach(images, async (image) => {
const embed = await this.embed(pdf, image);

let imageHeight = 0;
if (embed instanceof PDFImage) {
imageHeight = image.height;
} else if (embed instanceof PDFEmbeddedPage) {
// PDFs are vector images, so we use the max width defined for the header__image CSS class
imageHeight = 140 / (embed.width / embed.height); // Keep the aspect ratio as image.height is incorrect
}

pdf.getPages().forEach((page, i) => {
const position = imagePositions.get(image.id)?.at(i);
if (!position) {
throw new Error('Image position not found');
}
page.drawImage(embed, {
x: toPoints(position.x),
// The Y-axis is inverted in the PDF specification
y: page.getHeight() - toPoints(position.y) - toPoints(image.height),
width: toPoints(image.width),
height: toPoints(image.height)
});
if (embed instanceof PDFImage) {
page.drawImage(embed, {
x: toPoints(position.x),
// The Y-axis is inverted in the PDF specification
y: page.getHeight() - toPoints(image.height) - (image.type === 'signature' ? toPoints(position.y) : toPoints(40) + toPoints(firstImageHeight[image.type])),
width: toPoints(image.width),
height: toPoints(image.height)
});
} else if (embed instanceof PDFEmbeddedPage) {
page.drawPage(embed, {
x: toPoints(position.x) - (image.type === 'signature' ? toPoints(40) : 0),
// The Y-axis is inverted in the PDF specification
y: page.getHeight() - toPoints(imageHeight) - (image.type === 'signature' ? toPoints(position.y) : toPoints(40) + toPoints(firstImageHeight[image.type])),
width: toPoints(140),
height: toPoints(imageHeight)
});

}
});
firstImageHeight[image.type] = firstImageHeight[image.type] === 0 ? imageHeight : 0;
firstImageWidth[image.type] = firstImageWidth[image.type] === 0 ? image.width : 0;
});
const final = await pdf.save();
return Buffer.from(final);
Expand All @@ -222,6 +270,7 @@ interface Image {
content: string;
width: number;
height: number;
type: 'logo' | 'signature';
}

interface Position {
Expand Down
2 changes: 1 addition & 1 deletion packages/draft/src/templates/draft/draft.css
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ footer {

.footer__signature {
margin-left: 4rem;
width: 200px;
width: 280px;
}

.footer__signature p {
Expand Down
Loading