Skip to content
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
8 changes: 6 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@

FROM node:20 as dependencies
WORKDIR /app

# Install required system dependencies for Puppeteer
RUN apt-get update && apt-get install -y libnss3 libatk1.0-0 libatk-bridge2.0-0 libcups2 libdrm2 libxkbcommon-x11-0 libgbm1 libasound2 libxshmfence1 fonts-liberation fonts-noto fonts-noto-core fonts-noto-ui-core fonts-deva fonts-indic gconf-service libappindicator3-1 libnspr4 libx11-xcb1 xdg-utils --no-install-recommends && rm -rf /var/lib/apt/lists/*

COPY . ./
RUN npm cache clean --force
RUN npm i typeorm
RUN npm i cache-manager
RUN npm i --legacy-peer-deps

# Install required system dependencies for Puppeteer
RUN apt-get update && apt-get install -y libnss3 libatk1.0-0 libatk-bridge2.0-0 libcups2 libdrm2 libxkbcommon-x11-0 libgbm1 libasound2 libxshmfence1 fonts-liberation gconf-service libappindicator3-1 libnspr4 libx11-xcb1 xdg-utils --no-install-recommends
# Install Chrome for Puppeteer
RUN npx puppeteer browsers install chrome

EXPOSE 3000
CMD ["npm", "start"]
15 changes: 12 additions & 3 deletions src/modules/certificate/certificate.contoller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,12 +104,21 @@ export class CertificateController {
@Post('render-PDF')
async renderCertificatePDFFromHTML(
@Body() renderCertificateDto: RenderCertificateDTO,
@Res({ passthrough: true }) response,
): Promise<string | StreamableFile> {
return await this.certificateService.renderPDFFromHTML(
@Res({ passthrough: true }) response: Response,
): Promise<StreamableFile> {
const streamableFile = await this.certificateService.renderPDFFromHTML(
renderCertificateDto.credentialId,
renderCertificateDto.templateId,
response,
);
Comment on lines +107 to 113
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Remove unused response parameter from service call.

The response parameter is passed to renderPDFFromHTML but is no longer used in the service (as noted in the service file review). Update the service signature and this call site accordingly.

♻️ Suggested fix
-  async renderCertificatePDFFromHTML(
-    `@Body`() renderCertificateDto: RenderCertificateDTO,
-    `@Res`({ passthrough: true }) response: Response,
-  ): Promise<StreamableFile> {
-    const streamableFile = await this.certificateService.renderPDFFromHTML(
-      renderCertificateDto.credentialId,
-      renderCertificateDto.templateId,
-      response,
-    );
+  async renderCertificatePDFFromHTML(
+    `@Body`() renderCertificateDto: RenderCertificateDTO,
+    `@Res`({ passthrough: true }) response: Response,
+  ): Promise<StreamableFile> {
+    const streamableFile = await this.certificateService.renderPDFFromHTML(
+      renderCertificateDto.credentialId,
+      renderCertificateDto.templateId,
+    );
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@Res({ passthrough: true }) response: Response,
): Promise<StreamableFile> {
const streamableFile = await this.certificateService.renderPDFFromHTML(
renderCertificateDto.credentialId,
renderCertificateDto.templateId,
response,
);
`@Res`({ passthrough: true }) response: Response,
): Promise<StreamableFile> {
const streamableFile = await this.certificateService.renderPDFFromHTML(
renderCertificateDto.credentialId,
renderCertificateDto.templateId,
);
🤖 Prompt for AI Agents
In `@src/modules/certificate/certificate.contoller.ts` around lines 107 - 113, The
controller is passing a now-unused Response object into
CertificateService.renderPDFFromHTML; remove the response argument from the call
and update the service method signature to no longer accept Response. Edit the
controller method that currently injects `@Res`({ passthrough: true }) response:
Response to stop forwarding response into certificateService.renderPDFFromHTML
(and remove the response parameter entirely if it's not used elsewhere), and
update CertificateService.renderPDFFromHTML to accept only (credentialId,
templateId) and adjust any internal usages accordingly.


// Set proper headers for PDF download
response.setHeader('Content-Type', 'application/pdf');
response.setHeader(
'Content-Disposition',
`attachment; filename="certificate-${renderCertificateDto.credentialId}.pdf"`,
);

return streamableFile;
}
}
81 changes: 63 additions & 18 deletions src/modules/certificate/certificate.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -412,7 +412,53 @@ export class CertificateService {
Accept: 'text/html',
},
});
response.data = response?.data?.replace('**Id**', credentialId);
let htmlContent = response?.data?.replace('**Id**', credentialId);

// Inject font support for Devanagari script (Marathi/Hindi) if not already present
if (!htmlContent.includes('fonts.googleapis.com') && !htmlContent.includes('@font-face')) {
const fontInjection = `
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+Devanagari:wght@400;500;600;700&family=Noto+Serif+Devanagari:wght@400;500;600;700&display=swap" rel="stylesheet">
<style>
* {
font-family: 'Noto Sans Devanagari', 'Noto Serif Devanagari', 'DejaVu Sans', 'Liberation Sans', Arial, sans-serif !important;
}
</style>
`;

// Ensure charset is set first
if (!htmlContent.includes('charset')) {
if (htmlContent.includes('</head>')) {
htmlContent = htmlContent.replace('</head>', '<meta charset="UTF-8"></head>');
} else if (htmlContent.includes('<head>')) {
htmlContent = htmlContent.replace('<head>', '<head><meta charset="UTF-8">');
}
}

// Inject fonts into head if head exists, otherwise wrap content
if (htmlContent.includes('</head>')) {
htmlContent = htmlContent.replace('</head>', `${fontInjection}</head>`);
} else if (htmlContent.includes('<head>')) {
htmlContent = htmlContent.replace('<head>', `<head>${fontInjection}`);
} else {
// If no head tag, wrap content and add head
if (!htmlContent.includes('<html>')) {
htmlContent = `<html><head><meta charset="UTF-8">${fontInjection}</head><body>${htmlContent}</body></html>`;
} else {
htmlContent = htmlContent.replace('<html>', `<html><head><meta charset="UTF-8">${fontInjection}</head>`);
}
}
} else {
// Ensure charset is set even if fonts are already present
if (!htmlContent.includes('charset')) {
if (htmlContent.includes('</head>')) {
htmlContent = htmlContent.replace('</head>', '<meta charset="UTF-8"></head>');
} else if (htmlContent.includes('<head>')) {
htmlContent = htmlContent.replace('<head>', '<head><meta charset="UTF-8">');
}
}
}

// Launch Puppeteer
const browser = await puppeteer.launch({
Expand All @@ -423,18 +469,28 @@ export class CertificateService {
'--disable-gpu',
'--disable-dev-shm-usage',
'--disable-software-rasterizer',
'--font-render-hinting=none',
],
});

const page = await browser.newPage();

// Set HTML content
await page.setContent(response.data, { waitUntil: 'load' });
// Set viewport for better rendering
await page.setViewport({ width: 1200, height: 1600 });

// Set HTML content and wait for fonts to load
await page.setContent(htmlContent, {
waitUntil: 'networkidle0', // Wait for all network requests to finish (including fonts)
});

// Additional wait to ensure fonts are fully loaded
await page.evaluateHandle(() => document.fonts.ready);

// Generate PDF
const pdfBuffer = await page.pdf({
format: 'A4',
printBackground: true,
preferCSSPageSize: false,
margin: {
top: '10mm',
bottom: '10mm',
Expand All @@ -445,23 +501,12 @@ export class CertificateService {

await browser.close();

res.setHeader('Content-Type', 'application/pdf');
res.setHeader(
'Content-Disposition',
'attachment; filename="generated.pdf"',
);
res.setHeader('Content-Length', pdfBuffer.length);
res.end(pdfBuffer);
// Return StreamableFile instead of calling res.end()
return new StreamableFile(pdfBuffer);
} catch (error) {
console.log('error: ', error);
this.loggerService.error('Error fetching credentials:', error);
return APIResponse.error(
res,
apiId,
'Error fetching credentials',
'INTERNAL_SERVER_ERROR',
HttpStatus.INTERNAL_SERVER_ERROR,
);
this.loggerService.error('Error generating PDF:', error);
throw error;
}
}
}