feat: Add local logo image and improve PDF generation layout

This commit is contained in:
Alaguraj0361 2026-05-18 22:46:10 +05:30
parent 1b2ee461a6
commit 20977e00bd
2 changed files with 291 additions and 54 deletions

BIN
logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

View File

@ -1,14 +1,51 @@
const PDFDocument = require('pdfkit');
/**
* Generates a PDF invoice based on Shopify order data.
* Helper to draw a mock barcode in the top right corner
*/
function drawMockBarcode(doc, x, y, width, height) {
doc.save();
let currentX = x;
const endX = x + width;
// Draw a series of random thin/thick black lines to look like a barcode
while (currentX < endX) {
const lineWidth = Math.random() * 2 + 0.8; // 0.8 to 2.8 pixels wide
const spacing = Math.random() * 3 + 1.2; // 1.2 to 4.2 pixels spacing
if (currentX + lineWidth > endX) break;
doc.rect(currentX, y, lineWidth, height).fill('#000000');
currentX += lineWidth + spacing;
}
doc.restore();
}
/**
* Fetch image buffer from URL safely
*/
async function fetchImageBuffer(url) {
if (!url) return null;
try {
const response = await fetch(url);
if (!response.ok) return null;
const arrayBuffer = await response.arrayBuffer();
return Buffer.from(arrayBuffer);
} catch (err) {
console.error(`Failed to fetch image from ${url}:`, err.message);
return null;
}
}
/**
* Generates a PDF invoice matching the user's custom layout.
* @param {Object} orderData - The Shopify order payload
* @returns {Promise<Buffer>} - A promise that resolves to a Buffer containing the PDF data
*/
const generateInvoicePDF = (orderData) => {
return new Promise((resolve, reject) => {
const generateInvoicePDF = async (orderData) => {
return new Promise(async (resolve, reject) => {
try {
const doc = new PDFDocument({ margin: 50 });
const doc = new PDFDocument({ margin: 40, size: 'A4' });
let buffers = [];
doc.on('data', buffers.push.bind(buffers));
@ -17,68 +54,268 @@ const generateInvoicePDF = (orderData) => {
resolve(pdfData);
});
// Invoice Header
doc.fontSize(20).text('Invoice', { align: 'right' });
doc.moveDown();
// Colors
const primaryColor = '#1e293b'; // Slate dark gray
const textColor = '#334155';
const lightGray = '#cbd5e1';
const rowAltColor = '#f8fafc';
doc.fontSize(12)
.text(`Order Number: ${orderData.name || orderData.order_number}`)
.text(`Date: ${new Date(orderData.created_at).toLocaleDateString()}`)
.moveDown();
// --- 1. TITLE & BARCODE ---
// Title (Left)
const isQuote = orderData.gateway === 'Request Quote' || orderData.payment_gateway_names?.includes('Request Quote');
const titleText = isQuote ? 'Request Quote' : 'Invoice';
// Store Info (You can customize this)
const shopName = process.env.SHOP_NAME || 'Your Store Name';
doc.fontSize(14).text(shopName);
doc.moveDown();
doc.fontSize(28)
.font('Helvetica-Bold')
.fillColor(primaryColor)
.text(titleText, 40, 40);
// Customer Info
doc.fontSize(14).text('Billed To:');
doc.fontSize(12).text(`${orderData.billing_address?.first_name || ''} ${orderData.billing_address?.last_name || ''}`);
doc.text(`${orderData.billing_address?.address1 || ''}`);
if (orderData.billing_address?.address2) {
doc.text(`${orderData.billing_address.address2}`);
}
doc.text(`${orderData.billing_address?.city || ''}, ${orderData.billing_address?.province || ''} ${orderData.billing_address?.zip || ''}`);
doc.text(`${orderData.billing_address?.country || ''}`);
// Barcode (Right)
drawMockBarcode(doc, 380, 40, 175, 40);
doc.moveDown(2);
// Line Items Table Header
doc.fontSize(12).font('Helvetica-Bold');
doc.text('Item', 50, doc.y, { continued: true });
doc.text('Qty', 300, doc.y, { continued: true, width: 50, align: 'right' });
doc.text('Price', 350, doc.y, { continued: true, width: 100, align: 'right' });
doc.text('Total', 450, doc.y, { width: 100, align: 'right' });
// --- 2. LOGO & FROM DETAILS ---
const fromY = 110;
doc.moveTo(50, doc.y).lineTo(550, doc.y).stroke();
doc.moveDown(0.5);
// Logo (Left)
let logoLoaded = false;
const fs = require('fs');
const path = require('path');
const localLogoPath = path.join(__dirname, 'logo.png');
// Line Items Table Rows
doc.font('Helvetica');
let currentY = doc.y;
if (fs.existsSync(localLogoPath)) {
try {
doc.image(localLogoPath, 40, fromY, { width: 130 });
logoLoaded = true;
} catch (e) {
console.error('Error rendering local logo image:', e);
}
}
if (!logoLoaded) {
const logoUrl = process.env.SHOP_LOGO_URL;
const logoBuffer = await fetchImageBuffer(logoUrl);
if (logoBuffer) {
try {
doc.image(logoBuffer, 40, fromY, { width: 130 });
logoLoaded = true;
} catch (e) {
console.error('Error rendering logo image from URL:', e);
}
}
}
if (!logoLoaded) {
// Stylish text fallback if image is missing
doc.fontSize(20)
.font('Helvetica-Bold')
.fillColor('#db2777') // Pink accent
.text('RAY AARI SHOP', 40, fromY);
}
// From Address (Right)
doc.fontSize(10)
.font('Helvetica-Bold')
.fillColor(primaryColor)
.text('From', 380, fromY)
.font('Helvetica')
.fillColor(textColor)
.text('Ray Aari Shop', 380, fromY + 15)
.text('Sivanatham Salai', 380, fromY + 28)
.text('Arappalayam Cross Rd, Near : Madura Coats pvt', 380, fromY + 41, { width: 175 })
.text('Madurai 625016', 380, fromY + 67)
.text('Tamil Nadu, India', 380, fromY + 80)
.text('9994333548', 380, fromY + 93);
// --- 3. BILL TO & ORDER DETAILS ---
const detailsY = 240;
// Bill To (Left)
const billing = orderData.billing_address || {};
doc.fontSize(11)
.font('Helvetica-Bold')
.fillColor(primaryColor)
.text('Bill to', 40, detailsY);
doc.fontSize(10)
.font('Helvetica')
.fillColor(textColor);
let billTextY = detailsY + 18;
if (billing.first_name || billing.last_name) {
doc.text(`${billing.first_name || ''} ${billing.last_name || ''}`, 40, billTextY);
billTextY += 13;
}
if (billing.address1) {
doc.text(billing.address1, 40, billTextY);
billTextY += 13;
}
if (billing.address2) {
doc.text(billing.address2, 40, billTextY);
billTextY += 13;
}
if (billing.city || billing.province || billing.zip) {
doc.text(`${billing.city || ''} ${billing.province || ''} ${billing.zip || ''}`, 40, billTextY);
billTextY += 13;
}
if (billing.country) {
doc.text(billing.country, 40, billTextY);
billTextY += 13;
}
if (orderData.email || orderData.contact_email) {
doc.text(orderData.email || orderData.contact_email, 40, billTextY);
billTextY += 13;
}
if (billing.phone || orderData.phone) {
doc.text(billing.phone || orderData.phone, 40, billTextY);
}
// Order Details (Right)
const orderDateStr = new Date(orderData.created_at).toLocaleDateString('en-GB'); // DD-MM-YYYY
const gatewayName = orderData.gateway || 'Request Quote';
doc.fontSize(14)
.font('Helvetica-Bold')
.fillColor(primaryColor)
.text(`Order no: ${orderData.order_number}`, 380, detailsY)
.fontSize(10)
.font('Helvetica-Bold')
.text('Order date: ', 380, detailsY + 22, { continued: true })
.font('Helvetica')
.fillColor(textColor)
.text(orderDateStr)
.font('Helvetica-Bold')
.fillColor(primaryColor)
.text('Payment method: ', 380, detailsY + 37, { continued: true })
.font('Helvetica')
.fillColor(textColor)
.text(gatewayName);
// --- 4. PRODUCT TABLE ---
let tableY = 380;
// Table Headers
doc.fontSize(9)
.font('Helvetica-Bold')
.fillColor(primaryColor);
doc.text('S.No', 40, tableY, { width: 35 });
doc.text('Image', 85, tableY, { width: 60 });
doc.text('Product', 155, tableY, { width: 200 });
doc.text('Quantity', 365, tableY, { width: 50, align: 'right' });
doc.text('Unit price', 425, tableY, { width: 60, align: 'right' });
doc.text('Total price', 495, tableY, { width: 60, align: 'right' });
// Line below header
doc.moveTo(40, tableY + 12)
.lineTo(555, tableY + 12)
.lineWidth(0.8)
.strokeColor(lightGray)
.stroke();
tableY += 20;
// Currency formatting symbol
const currencySymbol = orderData.currency === 'INR' ? 'Rs. ' : `${orderData.currency} `;
// Table Rows
if (orderData.line_items && orderData.line_items.length > 0) {
orderData.line_items.forEach(item => {
doc.text(item.name, 50, currentY, { width: 250 });
doc.text(item.quantity.toString(), 300, currentY, { width: 50, align: 'right' });
doc.text(`${orderData.currency} ${parseFloat(item.price).toFixed(2)}`, 350, currentY, { width: 100, align: 'right' });
const total = parseFloat(item.price) * item.quantity;
doc.text(`${orderData.currency} ${total.toFixed(2)}`, 450, currentY, { width: 100, align: 'right' });
for (let i = 0; i < orderData.line_items.length; i++) {
const item = orderData.line_items[i];
doc.moveDown(0.5);
currentY = doc.y;
});
// Fetch product image buffer
let imageBuffer = null;
if (item.image) {
imageBuffer = await fetchImageBuffer(item.image);
} else if (item.featured_image && item.featured_image.url) {
imageBuffer = await fetchImageBuffer(item.featured_image.url);
}
doc.moveTo(50, currentY).lineTo(550, currentY).stroke();
doc.moveDown(1);
// Totals
doc.text(`Subtotal: ${orderData.currency} ${orderData.subtotal_price}`, { align: 'right' });
if (orderData.total_tax > 0) {
doc.text(`Tax: ${orderData.currency} ${orderData.total_tax}`, { align: 'right' });
// Background color for alternating rows
if (i % 2 === 1) {
doc.rect(40, tableY - 4, 515, 38).fill(rowAltColor);
}
doc.text(`Shipping: ${orderData.currency} ${orderData.total_shipping_price_set?.shop_money?.amount || '0.00'}`, { align: 'right' });
doc.font('Helvetica-Bold').text(`Total: ${orderData.currency} ${orderData.total_price}`, { align: 'right' });
doc.fontSize(9)
.font('Helvetica')
.fillColor(textColor);
// S.No
doc.text((i + 1).toString(), 40, tableY);
// Image
if (imageBuffer) {
try {
doc.image(imageBuffer, 85, tableY - 2, { fit: [35, 30] });
} catch (err) {
console.error('Failed to draw item image:', err);
}
}
// Product Name and Variant/Quantity details
const productText = item.title || item.name;
doc.text(productText, 155, tableY, { width: 200, height: 15, ellipsis: true });
// Display quantity/variant info if available
if (item.variant_title) {
doc.fontSize(8)
.fillColor('#64748b')
.text(item.variant_title, 155, tableY + 12, { width: 200 });
}
// Reset font size
doc.fontSize(9).fillColor(textColor);
// Quantity
doc.text(item.quantity.toString(), 365, tableY, { width: 50, align: 'right' });
// Unit Price
const unitPrice = parseFloat(item.price).toFixed(2);
doc.text(`${currencySymbol}${unitPrice}`, 425, tableY, { width: 60, align: 'right' });
// Total Price
const totalPrice = (parseFloat(item.price) * item.quantity).toFixed(2);
doc.text(`${currencySymbol}${totalPrice}`, 495, tableY, { width: 60, align: 'right' });
// Divider Line
doc.moveTo(40, tableY + 28)
.lineTo(555, tableY + 28)
.lineWidth(0.5)
.strokeColor('#f1f5f9')
.stroke();
tableY += 34;
}
}
// --- 5. TOTALS SECTION ---
tableY += 10;
doc.fontSize(10)
.font('Helvetica')
.fillColor(textColor);
// Subtotal
doc.text('Subtotal', 365, tableY, { width: 100, align: 'right' });
doc.font('Helvetica-Bold')
.text(`${currencySymbol}${parseFloat(orderData.subtotal_price).toFixed(2)}`, 480, tableY, { width: 75, align: 'right' });
// Divider
doc.moveTo(365, tableY + 15)
.lineTo(555, tableY + 15)
.lineWidth(0.5)
.strokeColor(lightGray)
.stroke();
tableY += 22;
// Total
doc.fontSize(11)
.font('Helvetica-Bold')
.fillColor(primaryColor);
doc.text('Total', 365, tableY, { width: 100, align: 'right' });
doc.text(`${currencySymbol}${parseFloat(orderData.total_price).toFixed(2)}`, 480, tableY, { width: 75, align: 'right' });
doc.end();
} catch (error) {