Top 5 Automated PDF & Document Generation Tool Ideas for Developers for Independent Web Developers and Indie Hackers
Automated Invoice Generation with Dynamic Data Binding
For e-commerce businesses, timely and accurate invoicing is critical. Automating this process not only saves time but also reduces errors. A robust solution involves a backend service that can dynamically generate PDF invoices based on order data, customer information, and product details. We’ll leverage a PHP-based approach using the popular TCPDF library for PDF generation and a simple data structure to represent order information.
First, ensure you have TCPDF installed. If using Composer, add it to your `composer.json`:
composer require tecnickcom/tcpdf
Next, let’s define a sample order data structure. In a real application, this would come from your database or an API.
Here’s a PHP class to represent an invoice:
Invoice.php
<?php
require_once('vendor/autoload.php'); // Assuming Composer autoload
use TCPDF;
class Invoice {
private $orderData;
private $companyInfo;
private $pdf;
public function __construct(array $orderData, array $companyInfo) {
$this->orderData = $orderData;
$this->companyInfo = $companyInfo;
$this->pdf = new TCPDF(PDF_PAGE_ORIENTATION, PDF_UNIT, PDF_PAGE_FORMAT, true, 'UTF-8', false);
// Set document information
$this->pdf->SetCreator(PDF_CREATOR);
$this->pdf->SetAuthor($companyInfo['name']);
$this->pdf->SetTitle('Invoice #' . $orderData['order_id']);
$this->pdf->SetSubject('Invoice for Order ' . $orderData['order_id']);
// Set default header data
$this->pdf->SetHeaderData(null, 0, $companyInfo['name'] . ' - Invoice', "Order ID: " . $orderData['order_id']);
// Set header and footer fonts
$this->pdf->setHeaderFont(Array(PDF_FONT_NAME_MAIN, '', PDF_FONT_SIZE_MAIN));
$this->pdf->setFooterFont(Array(PDF_FONT_NAME_DATA, '', PDF_FONT_SIZE_DATA));
// Set default monospaced font
$this->pdf->SetDefaultMonospacedFont(PDF_FONT_MONOSPACED);
// Set margins
$this->pdf->SetMargins(PDF_MARGIN_LEFT, PDF_MARGIN_TOP, PDF_MARGIN_RIGHT);
$this->pdf->SetHeaderMargin(PDF_MARGIN_HEADER);
$this->pdf->SetFooterMargin(PDF_MARGIN_FOOTER);
// Set auto page breaks
$this->pdf->SetAutoPageBreak(TRUE, PDF_MARGIN_BOTTOM);
// Set image scale factor
$this->pdf->setImageScale(PDF_IMAGE_SCALE_RATIO);
// Add a page
$this->pdf->AddPage();
}
public function generate() {
// Company Info Section
$this->pdf->SetFont('helvetica', 'B', 16);
$this->pdf->Cell(0, 10, $this->companyInfo['name'], 0, 1, 'C');
$this->pdf->SetFont('helvetica', '', 10);
$this->pdf->Cell(0, 5, $this->companyInfo['address'], 0, 1, 'C');
$this->pdf->Cell(0, 5, $this->companyInfo['city'] . ', ' . $this->companyInfo['zip'], 0, 1, 'C');
$this->pdf->Cell(0, 5, 'Phone: ' . $this->companyInfo['phone'], 0, 1, 'C');
$this->pdf->Ln(10);
// Invoice Details Section
$this->pdf->SetFont('helvetica', 'B', 12);
$this->pdf->Cell(0, 10, 'INVOICE', 0, 1, 'L');
$this->pdf->SetFont('helvetica', '', 10);
$this->pdf->Cell(40, 7, 'Invoice Number:', 0, 0, 'L');
$this->pdf->Cell(0, 7, $this->orderData['order_id'], 0, 1, 'L');
$this->pdf->Cell(40, 7, 'Invoice Date:', 0, 0, 'L');
$this->pdf->Cell(0, 7, date('Y-m-d'), 0, 1, 'L');
$this->pdf->Cell(40, 7, 'Due Date:', 0, 0, 'L');
$this->pdf->Cell(0, 7, date('Y-m-d', strtotime('+30 days')), 0, 1, 'L');
$this->pdf->Ln(5);
// Customer Info Section
$this->pdf->SetFont('helvetica', 'B', 11);
$this->pdf->Cell(0, 10, 'Bill To:', 0, 1, 'L');
$this->pdf->SetFont('helvetica', '', 10);
$this->pdf->Cell(0, 5, $this->orderData['customer']['name'], 0, 1, 'L');
$this->pdf->Cell(0, 5, $this->orderData['customer']['address'], 0, 1, 'L');
$this->pdf->Cell(0, 5, $this->orderData['customer']['city'] . ', ' . $this->orderData['customer']['zip'], 0, 1, 'L');
$this->pdf->Ln(10);
// Items Table Header
$this->pdf->SetFont('helvetica', 'B', 10);
$this->pdf->SetFillColor(220, 220, 220);
$this->pdf->Cell(80, 7, 'Description', 1, 0, 'C', 1);
$this->pdf->Cell(30, 7, 'Quantity', 1, 0, 'C', 1);
$this->pdf->Cell(30, 7, 'Unit Price', 1, 0, 'C', 1);
$this->pdf->Cell(30, 7, 'Total', 1, 1, 'C', 1);
// Items Table Rows
$this->pdf->SetFont('helvetica', '', 10);
$subtotal = 0;
foreach ($this->orderData['items'] as $item) {
$itemTotal = $item['quantity'] * $item['unit_price'];
$subtotal += $itemTotal;
$this->pdf->Cell(80, 7, $item['name'], 1, 0, 'L');
$this->pdf->Cell(30, 7, $item['quantity'], 1, 0, 'C');
$this->pdf->Cell(30, 7, '$' . number_format($item['unit_price'], 2), 1, 0, 'R');
$this->pdf->Cell(30, 7, '$' . number_format($itemTotal, 2), 1, 1, 'R');
}
// Totals Section
$tax = $subtotal * ($this->orderData['tax_rate'] ?? 0);
$shipping = $this->orderData['shipping_cost'] ?? 0;
$grandTotal = $subtotal + $tax + $shipping;
$this->pdf->Ln(5);
$this->pdf->SetFont('helvetica', '', 10);
$this->pdf->Cell(140, 7, 'Subtotal:', 0, 0, 'R');
$this->pdf->Cell(30, 7, '$' . number_format($subtotal, 2), 0, 1, 'R');
if (isset($this->orderData['tax_rate']) && $this->orderData['tax_rate'] > 0) {
$this->pdf->Cell(140, 7, 'Tax (' . ($this->orderData['tax_rate'] * 100) . '%):', 0, 0, 'R');
$this->pdf->Cell(30, 7, '$' . number_format($tax, 2), 0, 1, 'R');
}
if (isset($this->orderData['shipping_cost'])) {
$this->pdf->Cell(140, 7, 'Shipping:', 0, 0, 'R');
$this->pdf->Cell(30, 7, '$' . number_format($shipping, 2), 0, 1, 'R');
}
$this->pdf->SetFont('helvetica', 'B', 12);
$this->pdf->Cell(140, 10, 'Grand Total:', 0, 0, 'R');
$this->pdf->Cell(30, 10, '$' . number_format($grandTotal, 2), 0, 1, 'R');
// Output the PDF
return $this->pdf->Output('invoice_' . $this->orderData['order_id'] . '.pdf', 'S'); // 'S' returns as string
}
}
?>
Now, an example of how to use this class:
<?php
require_once 'Invoice.php';
$companyDetails = [
'name' => 'Your Awesome E-commerce Store',
'address' => '123 Main Street',
'city' => 'Anytown',
'zip' => '12345',
'phone' => '555-123-4567'
];
$order = [
'order_id' => 'ORD789012',
'customer' => [
'name' => 'John Doe',
'address' => '456 Oak Avenue',
'city' => 'Otherville',
'zip' => '67890'
],
'items' => [
['name' => 'Product A', 'quantity' => 2, 'unit_price' => 25.50],
['name' => 'Product B', 'quantity' => 1, 'unit_price' => 75.00],
['name' => 'Product C', 'quantity' => 5, 'unit_price' => 5.25]
],
'tax_rate' => 0.08, // 8% tax
'shipping_cost' => 10.00
];
try {
$invoiceGenerator = new Invoice($order, $companyDetails);
$pdfOutput = $invoiceGenerator->generate();
// To save the file on the server:
// file_put_contents('invoices/invoice_' . $order['order_id'] . '.pdf', $pdfOutput);
// To send directly to the browser:
header('Content-Type: application/pdf');
header('Content-Disposition: inline; filename="invoice_' . $order['order_id'] . '.pdf"');
header('Content-Length: ' . strlen($pdfOutput));
echo $pdfOutput;
exit;
} catch (Exception $e) {
// Log error or display a user-friendly message
error_log('Error generating invoice: ' . $e->getMessage());
echo "An error occurred while generating the invoice.";
}
?>
This setup provides a foundational, production-ready invoice generation system. For more complex layouts, consider using HTML templates with libraries like Dompdf or mPDF, which offer more flexibility in design.
Automated Packing Slip Generation
Similar to invoices, packing slips are essential for order fulfillment. They typically contain less financial information and focus on product details, quantities, and shipping addresses. This can be generated using the same principles as invoice generation but with a simplified template.
We can adapt the `Invoice` class or create a new `PackingSlip` class. For simplicity, let’s outline the key differences and a potential implementation snippet.
PackingSlip.php (Conceptual Snippet)
<?php
require_once('vendor/autoload.php'); // Assuming Composer autoload
use TCPDF;
class PackingSlip {
private $orderData;
private $companyInfo;
private $pdf;
public function __construct(array $orderData, array $companyInfo) {
$this->orderData = $orderData;
$this->companyInfo = $companyInfo;
$this->pdf = new TCPDF(PDF_PAGE_ORIENTATION, PDF_UNIT, PDF_PAGE_FORMAT, true, 'UTF-8', false);
// Configure TCPDF for packing slip (e.g., different header/footer, no financial details)
$this->pdf->SetTitle('Packing Slip - Order ' . $orderData['order_id']);
$this->pdf->SetHeaderData(null, 0, $companyInfo['name'] . ' - Packing Slip', "Order ID: " . $orderData['order_id']);
// ... other TCPDF configurations ...
$this->pdf->AddPage();
}
public function generate() {
// Company Info Section (simplified)
$this->pdf->SetFont('helvetica', 'B', 14);
$this->pdf->Cell(0, 10, $this->companyInfo['name'], 0, 1, 'C');
$this->pdf->SetFont('helvetica', '', 9);
$this->pdf->Cell(0, 5, $this->companyInfo['address'], 0, 1, 'C');
$this->pdf->Ln(5);
// Shipping Address Section
$this->pdf->SetFont('helvetica', 'B', 11);
$this->pdf->Cell(0, 10, 'Ship To:', 0, 1, 'L');
$this->pdf->SetFont('helvetica', '', 10);
$this->pdf->Cell(0, 5, $this->orderData['customer']['name'], 0, 1, 'L');
$this->pdf->Cell(0, 5, $this->orderData['customer']['address'], 0, 1, 'L');
$this->pdf->Cell(0, 5, $this->orderData['customer']['city'] . ', ' . $this->orderData['customer']['zip'], 0, 1, 'L');
$this->pdf->Ln(10);
// Items Table Header
$this->pdf->SetFont('helvetica', 'B', 10);
$this->pdf->SetFillColor(220, 220, 220);
$this->pdf->Cell(100, 7, 'Item Description', 1, 0, 'C', 1);
$this->pdf->Cell(40, 7, 'Quantity', 1, 1, 'C', 1);
// Items Table Rows
$this->pdf->SetFont('helvetica', '', 10);
foreach ($this->orderData['items'] as $item) {
$this->pdf->Cell(100, 7, $item['name'], 1, 0, 'L');
$this->pdf->Cell(40, 7, $item['quantity'], 1, 1, 'C');
}
// Optional: Add a barcode for tracking
// $this->pdf->write1DBarcode($this->orderData['order_id'], 'C128', '', '', '', 15, 0.4, $style, 'N');
return $this->pdf->Output('packing_slip_' . $this->orderData['order_id'] . '.pdf', 'S');
}
}
?>
The generation logic would be similar to the invoice example, but the `generate()` method would focus on displaying only the necessary information for packing and shipping.
Automated Report Generation (e.g., Sales Summaries)
For business owners, regular sales reports are invaluable for tracking performance. Automating the generation of these reports, perhaps on a weekly or monthly basis, can provide actionable insights without manual effort. This often involves querying a database, aggregating data, and presenting it in a clear, readable format.
Let’s consider generating a weekly sales summary report. We’ll use Python with the `reportlab` library for PDF generation and `SQLAlchemy` for database interaction.
First, install the necessary libraries:
pip install reportlab SQLAlchemy
Assume you have a database table `orders` with columns like `order_date`, `total_amount`, and `status`.
from reportlab.lib.pagesizes import letter
from reportlab.pdfgen import canvas
from reportlab.lib import colors
from reportlab.platypus import SimpleDocTemplate, Table, TableStyle, Paragraph
from reportlab.lib.styles import getSampleStyleSheet
from sqlalchemy import create_engine, text
from datetime import datetime, timedelta
def generate_weekly_sales_report(db_connection_string, output_filename="weekly_sales_report.pdf"):
engine = create_engine(db_connection_string)
styles = getSampleStyleSheet()
doc = SimpleDocTemplate(output_filename, pagesize=letter)
story = []
# --- Report Header ---
story.append(Paragraph("Weekly Sales Report", styles['h1']))
end_date = datetime.now()
start_date = end_date - timedelta(days=7)
report_period = f"{start_date.strftime('%Y-%m-%d')} to {end_date.strftime('%Y-%m-%d')}"
story.append(Paragraph(f"Period: {report_period}", styles['h2']))
story.append(Paragraph("
", styles['Normal'])) # Spacer
# --- Fetch Data ---
with engine.connect() as connection:
query = text("""
SELECT
DATE(order_date) as sale_date,
COUNT(*) as order_count,
SUM(total_amount) as total_revenue
FROM orders
WHERE order_date BETWEEN :start_date AND :end_date AND status = 'completed'
GROUP BY DATE(order_date)
ORDER BY sale_date;
""")
result = connection.execute(query, {'start_date': start_date, 'end_date': end_date})
sales_data = result.fetchall()
# --- Prepare Table Data ---
data = [['Date', 'Orders', 'Revenue']]
grand_total_orders = 0
grand_total_revenue = 0.0
for row in sales_data:
date_str = row.sale_date.strftime('%Y-%m-%d')
orders = row.order_count
revenue = f"${row.total_revenue:,.2f}"
data.append([date_str, str(orders), revenue])
grand_total_orders += orders
grand_total_revenue += row.total_revenue
# Add summary row
data.append(['', '', '']) # Spacer row
data.append(['Grand Totals', str(grand_total_orders), f"${grand_total_revenue:,.2f}"])
# --- Create Table ---
table = Table(data)
style = TableStyle([
('BACKGROUND', (0, 0), (-1, 0), colors.grey),
('TEXTCOLOR', (0, 0), (-1, 0), colors.whitesmoke),
('ALIGN', (0, 0), (-1, -1), 'CENTER'),
('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
('BOTTOMPADDING', (0, 0), (-1, 0), 12),
('BACKGROUND', (0, 1), (-1, -2), colors.beige), # Data rows
('GRID', (0, 0), (-1, -1), 1, colors.black),
('ALIGN', (2, 1), (-1, -2), 'RIGHT'), # Align revenue to the right
('FONTNAME', (0, -1), (-1, -1), 'Helvetica-Bold'), # Grand Totals row
('BACKGROUND', (0, -1), (-1, -1), colors.lightgreen),
])
table.setStyle(style)
story.append(table)
# --- Build PDF ---
try:
doc.build(story)
print(f"Report generated successfully: {output_filename}")
except Exception as e:
print(f"Error building PDF: {e}")
# --- Example Usage ---
# Replace with your actual database connection string
# Example for PostgreSQL: "postgresql://user:password@host:port/database"
# Example for MySQL: "mysql+mysqlconnector://user:password@host:port/database"
# Example for SQLite: "sqlite:///path/to/your/database.db"
DATABASE_URL = "sqlite:///your_database.db" # Replace with your DB URL
if __name__ == "__main__":
generate_weekly_sales_report(DATABASE_URL)
This script connects to a database, queries completed orders within the last 7 days, aggregates the data, and formats it into a PDF report. You can schedule this script to run automatically using cron jobs (on Linux/macOS) or Task Scheduler (on Windows).
Automated Certificate Generation for Online Courses/Webinars
For platforms offering online courses or webinars, automatically issuing certificates of completion is a common requirement. This involves generating personalized certificates with the participant’s name, course title, completion date, and a unique certificate ID. A template-based approach is often best here.
We can use Python with `Pillow` (PIL fork) for image manipulation to overlay text onto a certificate template image.
Install Pillow:
pip install Pillow
Assume you have a certificate template image (e.g., `certificate_template.png`) and a list of participants with their completion data.
from PIL import Image, ImageDraw, ImageFont
import datetime
import os
def generate_certificate(template_path, output_dir, participant_name, course_title, completion_date, cert_id):
try:
img = Image.open(template_path).convert("RGB")
draw = ImageDraw.Draw(img)
# Define font paths (ensure these fonts are available or provide full paths)
# You might need to download and place these fonts in your project directory
try:
title_font = ImageFont.truetype("arialbd.ttf", 48) # Bold Arial
name_font = ImageFont.truetype("arial.ttf", 64) # Regular Arial
details_font = ImageFont.truetype("arial.ttf", 24)
except IOError:
print("Error: Font file not found. Using default PIL font.")
title_font = ImageFont.load_default()
name_font = ImageFont.load_default()
details_font = ImageFont.load_default()
# --- Text Placement (Coordinates will depend on your template image) ---
# These are example coordinates. You'll need to adjust them based on your template.
img_width, img_height = img.size
# Course Title (centered horizontally, specific vertical position)
title_text = f"Certificate of Completion for"
title_bbox = draw.textbbox((0, 0), title_text, font=title_font)
title_width = title_bbox[2] - title_bbox[0]
title_x = (img_width - title_width) / 2
draw.text((title_x, 180), title_text, fill="black", font=title_font)
# Participant Name (larger, centered)
name_bbox = draw.textbbox((0, 0), participant_name, font=name_font)
name_width = name_bbox[2] - name_bbox[0]
name_x = (img_width - name_width) / 2
draw.text((name_x, 250), participant_name, fill="black", font=name_font)
# Course Title (below name)
course_title_bbox = draw.textbbox((0, 0), course_title, font=title_font)
course_title_width = course_title_bbox[2] - course_title_bbox[0]
course_title_x = (img_width - course_title_width) / 2
draw.text((course_title_x, 330), course_title, fill="black", font=title_font)
# Completion Date and Certificate ID (smaller, often bottom-aligned)
date_str = completion_date.strftime("%B %d, %Y")
date_text = f"Date: {date_str}"
cert_id_text = f"Certificate ID: {cert_id}"
# Position date on the left side
date_bbox = draw.textbbox((0, 0), date_text, font=details_font)
date_width = date_bbox[2] - date_bbox[0]
draw.text((100, 450), date_text, fill="black", font=details_font)
# Position cert ID on the right side
cert_id_bbox = draw.textbbox((0, 0), cert_id_text, font=details_font)
cert_id_width = cert_id_bbox[2] - cert_id_bbox[0]
draw.text((img_width - cert_id_width - 100, 450), cert_id_text, fill="black", font=details_font)
# --- Save Certificate ---
if not os.path.exists(output_dir):
os.makedirs(output_dir)
output_filename = os.path.join(output_dir, f"certificate_{cert_id}.png")
img.save(output_filename)
print(f"Generated certificate: {output_filename}")
return output_filename
except FileNotFoundError:
print(f"Error: Template file not found at {template_path}")
return None
except Exception as e:
print(f"An error occurred: {e}")
return None
# --- Example Usage ---
TEMPLATE = "certificate_template.png" # Path to your template image
OUTPUT_DIRECTORY = "certificates"
participants = [
{"name": "Alice Wonderland", "course": "Advanced Python", "date": datetime.date(2023, 10, 26), "id": "CERT001"},
{"name": "Bob The Builder", "course": "Web Development Fundamentals", "date": datetime.date(2023, 10, 25), "id": "CERT002"},
]
# Create a dummy template if it doesn't exist for testing
if not os.path.exists(TEMPLATE):
print(f"Creating a dummy template: {TEMPLATE}")
dummy_img = Image.new('RGB', (1000, 700), color = (255, 255, 255))
d = ImageDraw.Draw(dummy_img)
try:
fnt = ImageFont.truetype("arial.ttf", 30)
except IOError:
fnt = ImageFont.load_default()
d.text((50, 50), "Certificate Template Placeholder", fill=(0,0,0), font=fnt)
d.text((50, 150), "Name: [PARTICIPANT_NAME]", fill=(0,0,0), font=fnt)
d.text((50, 250), "Course: [COURSE_TITLE]", fill=(0,0,0), font=fnt)
d.text((50, 350), "Date: [COMPLETION_DATE]", fill=(0,0,0), font=fnt)
d.text((50, 450), "ID: [CERT_ID]", fill=(0,0,0), font=fnt)
dummy_img.save(TEMPLATE)
for p in participants:
generate_certificate(
TEMPLATE,
OUTPUT_DIRECTORY,
p["name"],
p["course"],
p["date"],
p["id"]
)
This script takes a template image and overlays participant-specific text. The coordinates for text placement are crucial and must be fine-tuned based on the exact dimensions and layout of your `certificate_template.png`. This approach is highly customizable and can be integrated into a web application’s backend to issue certificates automatically upon course completion.
Automated Welcome/Onboarding Document Generation
For SaaS products or subscription services, a well-crafted welcome document or onboarding guide can significantly improve user retention. Automating the generation of these documents, personalized with the user’s name and perhaps specific service details, makes the onboarding process feel more engaging and professional.
We can use a templating engine like Jinja2 (Python) or Twig (PHP) combined with a PDF generation library. For this example, let’s use Python with Jinja2 and