پی ڈی ایف کٹ کے ساتھ پی ڈی ایف جنریشن: انوائسز، رپورٹس اور سرٹیفکیٹس
پی ڈی ایف جنریشن ان خصوصیات میں سے ایک ہے جو اس وقت تک آسان نظر آتی ہے جب تک کہ آپ اسے نافذ کرنا شروع نہ کریں۔ متحرک میزیں جو صفحات کو درست طریقے سے اوور فلو کرتی ہیں، عربی RTL سپورٹ کے ساتھ کثیر زبان کا متن، صفحہ کی مناسب نمبرنگ، تمام صفحات پر لگاتار ہیڈر اور فوٹر، ایمبیڈڈ لوگوز — ان سب کے لیے ایک ایسے لے آؤٹ انجن کی ضرورت ہوتی ہے جو آپ کے لیے پیچیدگی کو ہینڈل کرے۔ PDFKit ایک نچلی سطح کی لیکن طاقتور Node.js PDF جنریشن لائبریری ہے جو آپ کو ہر عنصر پر قطعی کنٹرول فراہم کرتی ہے۔
یہ گائیڈ NestJS میں پی ڈی ایف جنریشن کے لیے PDFKit کے نمونوں کا احاطہ کرتا ہے: انوائس ٹیمپلیٹس، صفحہ بندی کے ساتھ ڈیٹا ٹیبلز، ملٹی کالم لے آؤٹس، حسب ضرورت فونٹس، امیج ایمبیڈنگ، اور اسٹریمنگ ڈیلیوری — تخلیق شدہ PDF کو بطور منسلکہ ای میل کرنے تک۔
اہم ٹیک ویز
- PDFKit Node.js میں پروگرام کے مطابق پی ڈی ایف تیار کرتا ہے — کوئی براؤزر، کوئی پپیٹیئر، کوئی HTML سے پی ڈی ایف کی تبدیلی نہیں
- بھیجنے سے پہلے ہمیشہ پی ڈی ایف کو مکمل کرنے کے لیے بفر کریں - ایک نامکمل پی ڈی ایف کلائنٹ کو نہ بھیجیں
- مسلسل کراس پلیٹ فارم رینڈرنگ کے لیے
registerFont()کے ذریعے فونٹس ایمبیڈ کریں — کبھی بھی سسٹم فونٹس پر بھروسہ نہ کریں- متعدد صفحات پر مشتمل دستاویزات کے لیے دستی صفحہ کے وقفے کا پتہ لگانے کی ضرورت ہوتی ہے:
doc.page.heightکے خلافdoc.yپوزیشن چیک کریں- عربی اور RTL متن کے لیے، ایک مناسب عربی فونٹ (Noto Sans Arabic) استعمال کریں اور متن کی سمت کو دستی طور پر ہینڈل کریں۔
- PDFs کو AWS S3 میں سٹریم کریں اور پہلے سے دستخط شدہ یو آر ایل واپس کریں - اپنے API کے ذریعے بڑے بفرز کو منتقل نہ کریں
- جانچ کے لیے
doc.pipe(fs.createWriteStream())استعمال کریں؛ HTTP سٹریمنگ کے لیےdoc.pipe(res)؛ ای میل منسلکات کے لیے بفر- محفوظ شدہ دستاویزات کی تعمیل کے لیے PDF/A میٹا ڈیٹا شامل کریں:
Title,Author,Creator,Producerفیلڈز
انسٹالیشن
pnpm add pdfkit
pnpm add -D @types/pdfkit
امیج سپورٹ (PNG، JPEG) اور فونٹ سب سیٹنگ کے لیے، کسی اضافی پیکج کی ضرورت نہیں ہے — PDFKit انہیں مقامی طور پر ہینڈل کرتا ہے۔
انوائس کا بنیادی ڈھانچہ
// src/modules/billing/pdf/invoice.generator.ts
import PDFDocument from 'pdfkit';
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import * as fs from 'fs';
import * as path from 'path';
interface InvoiceData {
invoiceNumber: string;
date: Date;
dueDate: Date;
customer: {
name: string;
email: string;
address: string;
city: string;
country: string;
};
items: Array<{
description: string;
quantity: number;
unitPrice: number;
total: number;
}>;
subtotal: number;
tax: number;
total: number;
currency: string;
notes?: string;
}
@Injectable()
export class InvoiceGenerator {
private readonly primaryColor = '#0f172a';
private readonly accentColor = '#f59e0b';
private readonly mutedColor = '#64748b';
private readonly borderColor = '#e2e8f0';
constructor(private config: ConfigService) {}
async generate(data: InvoiceData): Promise<Buffer> {
return new Promise((resolve, reject) => {
const buffers: Buffer[] = [];
const doc = new PDFDocument({
size: 'A4',
margin: 50,
info: {
Title: `Invoice ${data.invoiceNumber}`,
Author: 'ECOSIRE Private Limited',
Creator: 'ECOSIRE Billing System',
Producer: 'PDFKit',
},
});
// Collect chunks into buffer (do not stream incomplete PDFs)
doc.on('data', (chunk: Buffer) => buffers.push(chunk));
doc.on('end', () => resolve(Buffer.concat(buffers)));
doc.on('error', reject);
this.renderHeader(doc, data);
this.renderCustomerSection(doc, data);
this.renderItemsTable(doc, data);
this.renderTotals(doc, data);
if (data.notes) this.renderNotes(doc, data.notes);
this.renderFooter(doc, data);
doc.end();
});
}
private renderHeader(doc: PDFKit.PDFDocument, data: InvoiceData): void {
// Company logo
const logoPath = path.join(process.cwd(), 'public', 'logo-dark.png');
if (fs.existsSync(logoPath)) {
doc.image(logoPath, 50, 45, { width: 120 });
} else {
doc
.fontSize(18)
.fillColor(this.primaryColor)
.font('Helvetica-Bold')
.text('ECOSIRE', 50, 50);
}
// Invoice label and number
doc
.fontSize(28)
.fillColor(this.accentColor)
.font('Helvetica-Bold')
.text('INVOICE', 0, 50, { align: 'right' });
doc
.fontSize(10)
.fillColor(this.mutedColor)
.font('Helvetica')
.text(`#${data.invoiceNumber}`, 0, 82, { align: 'right' });
// Divider line
doc
.moveTo(50, 110)
.lineTo(545, 110)
.lineWidth(1)
.strokeColor(this.borderColor)
.stroke();
doc.moveDown(2);
}
private renderCustomerSection(doc: PDFKit.PDFDocument, data: InvoiceData): void {
const startY = 130;
// Billed To
doc
.fontSize(8)
.fillColor(this.mutedColor)
.font('Helvetica-Bold')
.text('BILLED TO', 50, startY)
.fontSize(11)
.fillColor(this.primaryColor)
.text(data.customer.name, 50, startY + 16)
.fontSize(9)
.fillColor(this.mutedColor)
.font('Helvetica')
.text(data.customer.email, 50, startY + 32)
.text(data.customer.address, 50, startY + 46)
.text(`${data.customer.city}, ${data.customer.country}`, 50, startY + 60);
// Invoice dates (right column)
const dateRows = [
['Invoice Date:', this.formatDate(data.date)],
['Due Date:', this.formatDate(data.dueDate)],
['Currency:', data.currency],
];
let dateY = startY;
for (const [label, value] of dateRows) {
doc
.fontSize(9)
.fillColor(this.mutedColor)
.font('Helvetica')
.text(label, 350, dateY, { width: 90 });
doc
.fontSize(9)
.fillColor(this.primaryColor)
.font('Helvetica-Bold')
.text(value, 445, dateY, { width: 100, align: 'right' });
dateY += 18;
}
doc.moveDown(5);
}
private renderItemsTable(doc: PDFKit.PDFDocument, data: InvoiceData): void {
const tableTop = 265;
const colWidths = { desc: 230, qty: 60, unitPrice: 90, total: 90 };
const startX = 50;
// Table header background
doc
.rect(startX, tableTop, 495, 22)
.fillColor(this.primaryColor)
.fill();
// Header text
doc
.fontSize(9)
.fillColor('#ffffff')
.font('Helvetica-Bold')
.text('DESCRIPTION', startX + 8, tableTop + 6, { width: colWidths.desc })
.text('QTY', startX + colWidths.desc + 8, tableTop + 6, { width: colWidths.qty, align: 'center' })
.text('UNIT PRICE', startX + colWidths.desc + colWidths.qty + 8, tableTop + 6, { width: colWidths.unitPrice, align: 'right' })
.text('TOTAL', startX + colWidths.desc + colWidths.qty + colWidths.unitPrice + 8, tableTop + 6, { width: colWidths.total, align: 'right' });
let rowY = tableTop + 28;
const fmt = new Intl.NumberFormat('en-US', {
style: 'currency',
currency: data.currency,
});
for (const [index, item] of data.items.entries()) {
// Alternating row background
if (index % 2 === 0) {
doc
.rect(startX, rowY - 4, 495, 24)
.fillColor('#f8fafc')
.fill();
}
doc
.fontSize(9)
.fillColor(this.primaryColor)
.font('Helvetica')
.text(item.description, startX + 8, rowY, { width: colWidths.desc })
.text(String(item.quantity), startX + colWidths.desc + 8, rowY, { width: colWidths.qty, align: 'center' })
.text(fmt.format(item.unitPrice), startX + colWidths.desc + colWidths.qty + 8, rowY, { width: colWidths.unitPrice, align: 'right' })
.text(fmt.format(item.total), startX + colWidths.desc + colWidths.qty + colWidths.unitPrice + 8, rowY, { width: colWidths.total, align: 'right' });
rowY += 24;
// Page break check
if (rowY > doc.page.height - 150) {
doc.addPage();
rowY = 50;
}
}
// Table bottom border
doc
.moveTo(startX, rowY + 4)
.lineTo(545, rowY + 4)
.lineWidth(1)
.strokeColor(this.borderColor)
.stroke();
doc.y = rowY + 20;
}
private renderTotals(doc: PDFKit.PDFDocument, data: InvoiceData): void {
const fmt = new Intl.NumberFormat('en-US', {
style: 'currency',
currency: data.currency,
});
const totalsX = 350;
let currentY = doc.y + 10;
const rows = [
['Subtotal', fmt.format(data.subtotal), false],
[`Tax (${((data.tax / data.subtotal) * 100).toFixed(0)}%)`, fmt.format(data.tax), false],
['Total Due', fmt.format(data.total), true],
];
for (const [label, value, isTotal] of rows) {
if (isTotal) {
doc
.rect(totalsX, currentY - 4, 195, 28)
.fillColor(this.accentColor)
.fill();
doc
.fontSize(11)
.fillColor('#000000')
.font('Helvetica-Bold')
.text(label as string, totalsX + 8, currentY + 2, { width: 90 })
.text(value as string, totalsX + 98, currentY + 2, { width: 89, align: 'right' });
} else {
doc
.fontSize(9)
.fillColor(this.mutedColor)
.font('Helvetica')
.text(label as string, totalsX + 8, currentY, { width: 90 })
.fillColor(this.primaryColor)
.text(value as string, totalsX + 98, currentY, { width: 89, align: 'right' });
}
currentY += 28;
}
doc.y = currentY + 10;
}
private renderNotes(doc: PDFKit.PDFDocument, notes: string): void {
doc.moveDown(2);
doc
.fontSize(9)
.fillColor(this.mutedColor)
.font('Helvetica-Bold')
.text('NOTES', 50, doc.y);
doc
.fontSize(9)
.fillColor(this.primaryColor)
.font('Helvetica')
.text(notes, 50, doc.y + 12, { width: 495 });
}
private renderFooter(doc: PDFKit.PDFDocument, _data: InvoiceData): void {
const footerY = doc.page.height - 60;
doc
.moveTo(50, footerY - 10)
.lineTo(545, footerY - 10)
.lineWidth(1)
.strokeColor(this.borderColor)
.stroke();
doc
.fontSize(8)
.fillColor(this.mutedColor)
.font('Helvetica')
.text(
'ECOSIRE Private Limited | [email protected] | ecosire.com',
50,
footerY,
{ align: 'center', width: 495 }
);
}
private formatDate(date: Date): string {
return new Intl.DateTimeFormat('en-US', {
year: 'numeric', month: 'long', day: 'numeric',
}).format(date);
}
}
NestJS کنٹرولر انٹیگریشن
// src/modules/billing/billing.controller.ts
@Controller('billing')
export class BillingController {
constructor(
private readonly billingService: BillingService,
private readonly invoiceGenerator: InvoiceGenerator,
private readonly emailService: EmailService,
) {}
@Get('invoices/:id/pdf')
async downloadInvoice(
@Param('id') id: string,
@Req() req: AuthenticatedRequest,
@Res() res: Response
): Promise<void> {
const invoice = await this.billingService.getInvoice(id, req.user.organizationId);
if (!invoice) {
throw new NotFoundException('Invoice not found');
}
const pdfBuffer = await this.invoiceGenerator.generate({
invoiceNumber: invoice.number,
date: invoice.createdAt,
dueDate: invoice.dueDate,
customer: {
name: req.user.name,
email: req.user.email,
address: invoice.billingAddress,
city: invoice.billingCity,
country: invoice.billingCountry,
},
items: invoice.lineItems,
subtotal: invoice.subtotal,
tax: invoice.tax,
total: invoice.total,
currency: invoice.currency,
});
res.set({
'Content-Type': 'application/pdf',
'Content-Disposition': `attachment; filename="invoice-${invoice.number}.pdf"`,
'Content-Length': pdfBuffer.length,
'Cache-Control': 'private, no-cache',
});
res.send(pdfBuffer);
}
@Post('invoices/:id/email')
async emailInvoice(
@Param('id') id: string,
@Req() req: AuthenticatedRequest
) {
const invoice = await this.billingService.getInvoice(id, req.user.organizationId);
const pdfBuffer = await this.invoiceGenerator.generate({
invoiceNumber: invoice.number,
// ... populate from invoice data
} as InvoiceData);
// Non-blocking email send
this.emailService.sendInvoiceEmail(
req.user.email,
req.user.name,
invoice.number,
pdfBuffer
).catch((err) =>
this.logger.warn(`Invoice email failed: ${err.message}`)
);
return { message: 'Invoice sent to ' + req.user.email };
}
}
صفحہ نمبروں کے ساتھ ملٹی پیج رپورٹ
// For reports spanning multiple pages
async generateReport(data: ReportData): Promise<Buffer> {
return new Promise((resolve, reject) => {
const buffers: Buffer[] = [];
const doc = new PDFDocument({ size: 'A4', margin: 50 });
doc.on('data', (chunk: Buffer) => buffers.push(chunk));
doc.on('end', () => resolve(Buffer.concat(buffers)));
doc.on('error', reject);
const pageCount = Math.ceil(data.rows.length / 30); // 30 rows per page
let currentPage = 0;
// Add page numbers to every page on finalize
doc.on('pageAdded', () => {
currentPage++;
doc
.fontSize(8)
.fillColor('#94a3b8')
.text(
`Page ${currentPage}`,
0,
doc.page.height - 30,
{ align: 'center', width: doc.page.width }
);
});
// Render content
this.renderReportHeader(doc, data);
let rowIndex = 0;
for (const row of data.rows) {
if (doc.y > doc.page.height - 100) {
doc.addPage(); // Triggers 'pageAdded' for footer
this.renderReportHeader(doc, data); // Re-render header on new page
}
this.renderTableRow(doc, row, rowIndex++);
}
doc.end();
});
}
S3 پر سلسلہ بندی
بڑی رپورٹس کے لیے، میموری میں بفرنگ کے بجائے براہ راست S3 پر جائیں:
import { PassThrough } from 'stream';
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
async function generateAndUploadToS3(
invoiceData: InvoiceData,
s3: S3Client,
bucket: string,
key: string
): Promise<string> {
const passThrough = new PassThrough();
const doc = new PDFDocument({ size: 'A4', margin: 50 });
doc.pipe(passThrough);
// Render invoice content
renderInvoice(doc, invoiceData);
doc.end();
// Upload stream to S3
await s3.send(new PutObjectCommand({
Bucket: bucket,
Key: key,
Body: passThrough,
ContentType: 'application/pdf',
Metadata: {
'invoice-number': invoiceData.invoiceNumber,
},
}));
return `s3://${bucket}/${key}`;
}
سرٹیفکیٹ ٹیمپلیٹ
async generateCertificate(data: CertificateData): Promise<Buffer> {
return new Promise((resolve, reject) => {
const buffers: Buffer[] = [];
const doc = new PDFDocument({
size: [792, 612], // Letter landscape
margin: 60,
layout: 'landscape',
});
doc.on('data', (chunk: Buffer) => buffers.push(chunk));
doc.on('end', () => resolve(Buffer.concat(buffers)));
doc.on('error', reject);
// Background
doc.rect(0, 0, 792, 612).fillColor('#fefce8').fill();
// Gold border (double line)
doc
.rect(20, 20, 752, 572)
.lineWidth(3)
.strokeColor('#f59e0b')
.stroke();
doc
.rect(28, 28, 736, 556)
.lineWidth(1)
.strokeColor('#f59e0b')
.stroke();
// Title
doc
.fontSize(36)
.fillColor('#0f172a')
.font('Helvetica-Bold')
.text('CERTIFICATE OF COMPLETION', 60, 100, { align: 'center' });
// Recipient name
doc
.fontSize(28)
.fillColor('#f59e0b')
.text(data.recipientName, 60, 200, { align: 'center' });
// Course name
doc
.fontSize(16)
.fillColor('#0f172a')
.font('Helvetica')
.text(`has successfully completed`, 60, 260, { align: 'center' })
.fontSize(20)
.font('Helvetica-Bold')
.text(data.courseName, 60, 290, { align: 'center' });
// Date and signature
doc
.fontSize(11)
.fillColor('#64748b')
.font('Helvetica')
.text(`Issued on ${this.formatDate(data.issuedDate)}`, 60, 400, { align: 'center' });
doc.end();
});
}
اکثر پوچھے گئے سوالات
PDFKit بمقابلہ Puppeteer برائے PDF جنریشن — مجھے کون سا استعمال کرنا چاہیے؟
ساختی دستاویزات کے لیے PDFKit: رسیدیں، رپورٹیں، سرٹیفکیٹ، خطوط۔ یہ تیز ہے (کوئی براؤزر اسٹارٹ اپ نہیں)، کم سے کم میموری استعمال کرتا ہے، اور ترتیب پر پکسل پرفیکٹ کنٹرول دیتا ہے۔ ایچ ٹی ایم ایل سے پی ڈی ایف کی تبدیلی کے لیے کٹھ پتلی — بہترین اس وقت جب آپ کے پاس پہلے سے ہی ایک HTML ٹیمپلیٹ ہو اور آپ کو اسے جیسا ہے جیسا پیش کرنے کی ضرورت ہو، بشمول پیچیدہ CSS، ویب فونٹس، اور متحرک JavaScript۔ Puppeteer 5-10x سست ہے اور اسے بغیر سر کے کروم عمل کی ضرورت ہے، لیکن ویب دور کی ترتیب کو بہتر طریقے سے ہینڈل کرتا ہے۔ انوائس اور کاروباری دستاویزات کے لیے، PDFKit صحیح انتخاب ہے۔
میں PDFKit میں متحرک مواد کے لیے صفحہ کے وقفے کو کیسے ہینڈل کروں؟
PDFKit خود بخود صفحہ بندی نہیں کرتا — آپ کو ہر عنصر کو پیش کرنے سے پہلے doc.y کو doc.page.height - margin کے خلاف چیک کرنا ہوگا۔ پیٹرن: if (doc.y > doc.page.height - minSpaceNeeded) { doc.addPage(); renderPageHeader(doc); }۔ میز کی قطاروں کے لیے، ایک قطار کے لیے درکار کم از کم جگہ کا حساب لگائیں اور ہر قطار سے پہلے چیک کریں۔ ملٹی لائن ٹیکسٹ کے لیے، اونچائی کا تخمینہ fontSize * lineCount * 1.2 کے طور پر لگائیں۔
میں PDFKit میں حسب ضرورت فونٹس کو کیسے ایمبیڈ کروں؟
فونٹس کو استعمال کرنے سے پہلے doc.registerFont('FontName', '/path/to/font.ttf') کے ساتھ رجسٹر کریں۔ اسے اپنے سروس کنسٹرکٹر یا سیٹ اپ فنکشن میں کال کریں۔ PDFKit 14 معیاری پی ڈی ایف فونٹس (Helvetica, Times, Courier اور ان کے بولڈ/ Italic variants) کے ساتھ بھیجتا ہے — ان کو رجسٹر کرنے کی ضرورت نہیں ہے۔ یونیکوڈ سپورٹ (عربی، چینی، جاپانی) کے لیے، آپ کو یونیکوڈ سے مطابقت رکھنے والا TTF/OTF فونٹ رجسٹر کرنا ہوگا۔ معیاری پی ڈی ایف فونٹس صرف لاطینی حروف کا احاطہ کرتے ہیں۔
میں عربی یا دیگر RTL زبانوں کے لیے PDFs کیسے بناؤں؟
PDFKit یونیکوڈ کو سپورٹ کرتا ہے لیکن مقامی طور پر دو طرفہ متن کو دوبارہ ترتیب نہیں دیتا ہے۔ عربی کے لیے، نوٹو سنز عربی یا امیری فونٹ رجسٹر کریں۔ متن کو پی ڈی ایف کٹ میں منتقل کرنے سے پہلے اسے دوبارہ ترتیب دینے کے لیے بولی الگورتھم لائبریری (جیسے bidi-js) کا استعمال کریں۔ RTL دستاویزات کے لیے ترتیب کو دستی طور پر ریورس کریں (دائیں طرف سے منسلک متن، عکس والے ٹیبل کالم)۔ اچھی طرح جانچیں — عربی پی ڈی ایف جنریشن کے لیے متن کی شمولیت، ڈائیکرٹکس، اور نمبر ڈائریکشن کو احتیاط سے ہینڈل کرنے کی ضرورت ہے۔
میں ڈیجیٹل دستخط یا PDF/A تعمیل کیسے شامل کروں؟
PDFKit info آپشن کے ذریعے آرکائیول (عنوان، مصنف، مضمون، کلیدی الفاظ) کے لیے بنیادی پی ڈی ایف میٹا ڈیٹا کی حمایت کرتا ہے۔ ڈیجیٹل دستخطوں کے لیے، PDFKit کے ساتھ node-signpdf لائبریری کا استعمال کریں — یہ PKCS#7 دستخط کو سرایت کرنے کے لیے پی ڈی ایف اسٹریم میں ترمیم کرتا ہے۔ PDF/A تعمیل (طویل مدتی آرکائیو) کے لیے، ایمبیڈڈ فونٹس، کوئی شفافیت نہیں، اور رنگ کی جگہ کی مناسب تعریف کو یقینی بنائیں۔ PDFKit v0.13+ نے ان خصوصیات کے لیے سپورٹ کو بہتر بنایا ہے، لیکن تعمیل کے پیچیدہ تقاضے pdf-lib جیسی سرشار لائبریری سے فائدہ اٹھا سکتے ہیں۔
اگلے اقدامات
PDFKit آپ کے Node.js بیک اینڈ میں براہ راست پروفیشنل، پروگرامیٹک پی ڈی ایف جنریشن کو قابل بناتا ہے — کوئی براؤزر اوور ہیڈ، کوئی بیرونی خدمات، فی صفحہ قیمت نہیں ہے۔ انوائس، رپورٹس، سرٹیفکیٹس، اور اسٹیٹمنٹس آپ کے API کے فرسٹ کلاس فیچر بن جاتے ہیں۔
ECOSIRE ہر NestJS پروجیکٹ پر مکمل بلنگ پائپ لائن کے حصے کے طور پر PDF انوائس جنریشن، خودکار ای میل ڈیلیوری، اور S3 آرکائیو کو لاگو کرتا ہے۔ ہماری بیک اینڈ انجینئرنگ سروسز کو دریافت کریں یہ جاننے کے لیے کہ ہم پروڈکشن گریڈ انوائس اور رپورٹنگ سسٹم کیسے بناتے ہیں۔
تحریر
ECOSIRE Research and Development Team
ECOSIRE میں انٹرپرائز گریڈ ڈیجیٹل مصنوعات بنانا۔ Odoo انٹیگریشنز، ای کامرس آٹومیشن، اور AI سے چلنے والے کاروباری حل پر بصیرت شیئر کرنا۔