Generate a professional invoice PDF directly from a dynamic web form using PHP, Dompdf, and AJAX for seamless downloads without page reloads. This tutorial leverages your attached scripts for a production-ready implementation with CSRF protection, logo uploads, and real-time calculations.
This tutorial explains how to generate a professional Invoice PDF using PHP and AJAX without page reload. The invoice is created dynamically from a web form, supports multiple line items, tax calculation, optional logo upload, and automatically downloads the generated PDF. Let’s dive into the implementation of the invoice PDF generator using PHP and AJAX!
Install Dompdf via Composer by running (in your project root):
composer require dompdf/dompdf
Note that: You don’t need to download the Dompdf library separately, all the required library files are included in our source code package.
Organize files as follows for clean deployment:
generate_invoice_pdf/ βββ index.php βββ generate_pdf.php βββ assets/ β βββ css/ β β βββ style.css β βββ js/ β βββ app.js βββ vendor/ βββ autoload.php
Create a file named index.php and define HTML elements to build an invoice form with organized sections for customer/seller details, dynamic items table, tax summary, and notes. The invoice form collects:
At the beginning, start a PHP session and generate a CSRF token for security.
<?php
// Start session
session_start();
// Generate CSRF token
if (empty($_SESSION['csrf_token'])) {
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}
$csrf = $_SESSION['csrf_token'];
?>
<form id="invoice-form" enctype="multipart/form-data" novalidate>
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($csrf) ?>">
<section class="cols">
<div class="card">
<h2>Customer Information</h2>
<div class="row">
<input name="customer_first" placeholder="First name" required>
</div>
<div class="row">
<input name="customer_last" placeholder="Last name" required>
</div>
<div class="row">
<input name="customer_business" placeholder="Business name">
</div>
<div class="row">
<input name="customer_phone" placeholder="Phone">
</div>
<div class="row">
<input name="customer_address" placeholder="Address">
</div>
<div class="row">
<input name="customer_city" placeholder="City">
</div>
<div class="row">
<input name="customer_state" placeholder="State">
</div>
<div class="row">
<input name="customer_zip" placeholder="Zip Code">
</div>
<div class="row">
<input name="customer_country" placeholder="Country">
</div>
</div>
<div class="card">
<h2>Seller Information</h2>
<div class="row">
<input name="seller_business" placeholder="Business name" required>
</div>
<div class="row">
<input name="seller_email" placeholder="Email" type="email" required>
</div>
<div class="row">
<input name="seller_phone" placeholder="Phone">
</div>
<div class="row">
<input name="seller_website" placeholder="Website">
</div>
<div class="row">
<input name="seller_address" placeholder="Address">
</div>
<div class="row">
<input name="seller_city" placeholder="City">
</div>
<div class="row">
<input name="seller_state" placeholder="State">
</div>
<div class="row">
<input name="seller_zip" placeholder="Zip Code">
</div>
<div class="row">
<input name="seller_country" placeholder="Country">
</div>
<div class="row logo-row">
<label>Logo (optional)</label>
<div class="logo-input-wrap">
<input id="logo-input" name="logo" type="file" accept="image/*">
<div id="logo-preview" class="logo-preview" style="display:none">
<img id="logo-preview-img" src="" alt="Logo preview">
<button type="button" id="remove-logo" class="remove-logo">Remove</button>
</div>
</div>
</div>
</div>
</section>
<section class="card">
<h2>Invoice Details</h2>
<div class="row field-inline">
<label>
Invoice #
<input name="invoice_number" placeholder="Invoice #" value="<?= htmlspecialchars('INV-001') ?>">
</label>
<label>
Invoice Date
<input name="issue_date" type="date" required value="<?= date('Y-m-d') ?>">
</label>
<label>
Due Date
<input name="due_date" type="date" required>
</label>
</div>
</section>
<section class="card">
<h2>Items</h2>
<table id="items-table">
<thead>
<tr>
<th>Item</th>
<th>Description</th>
<th>Qty</th>
<th>Price</th>
<th></th>
</tr>
</thead>
<tbody></tbody>
</table>
<button type="button" class="btn-info" id="add-item">+ Add Item</button>
</section>
<section class="card">
<div class="summary">
<div>
<label>Tax (%)</label>
<input id="tax" name="tax" type="number" value="0" min="0" step="0.01">
</div>
<div>
<label>Subtotal:</label>
<span id="subtotal">0.00</span>
</div>
<div>
<label>Tax:</label>
<span id="tax-amount">0.00</span>
</div>
<div class="total">
<label>Total:</label>
<span id="total">0.00</span>
</div>
</div>
</section>
<section class="card">
<h2>Notes</h2>
<textarea name="notes" rows="4" placeholder="Add any notes or terms for this invoice..."></textarea>
</section>
<div class="actions">
<a href="index.php" class="btn btn-secondary">Reset</a>
<button type="submit" class="btn-primary" id="create-btn">Create Invoice (Download PDF)</button>
</div>
</form>
Include JavaScript handler script (app.js) for dynamic invoice items, real-time subtotal/tax/total updates, and logo preview.
<script src="assets/js/app.js"></script>
The app.js file contains JavaScript code to manage dynamic invoice items, calculate subtotals, taxes, and totals in real-time, and handle logo preview functionality. This file manages all client-side interactions.
Features Implemented:
subtotal = sum(qty * price), tax_amount = subtotal * (tax/100), total = subtotal + tax_amount#logo-preview-imgInclude error handling for validation failures (e.g., missing items) with user-friendly alerts.
(function(){
const addItemBtn = document.getElementById('add-item');
const itemsTbody = document.querySelector('#items-table tbody');
const taxInput = document.getElementById('tax');
const subtotalEl = document.getElementById('subtotal');
const taxAmountEl = document.getElementById('tax-amount');
const totalEl = document.getElementById('total');
const form = document.getElementById('invoice-form');
const logoInput = document.getElementById('logo-input');
const logoPreviewWrap = document.getElementById('logo-preview');
const logoPreviewImg = document.getElementById('logo-preview-img');
const removeLogoBtn = document.getElementById('remove-logo');
// Function for dynamic item row creation
function createRow(item={name:'',desc:'',qty:1,price:0}){
const tr = document.createElement('tr');
tr.innerHTML = `
<td><input name="items_name[]" required value="${escapeHtml(item.name)}"></td>
<td><input name="items_desc[]" value="${escapeHtml(item.desc)}"></td>
<td><input name="items_qty[]" class="small" type="number" min="0" step="1" required value="${item.qty}"></td>
<td><input name="items_price[]" class="small" type="number" min="0" step="0.01" required value="${item.price}"></td>
<td><button type="button" class="remove-item">Remove</button></td>
`;
itemsTbody.appendChild(tr);
tr.querySelector('.remove-item').addEventListener('click', ()=>{tr.remove(); computeTotals();});
tr.querySelectorAll('input').forEach(i=>i.addEventListener('input', computeTotals));
}
function escapeHtml(s){ return String(s).replace(/[&<>\"]/g, function(c){return {'&':'&','<':'<','>':'>','"':'"'}[c];}); }
addItemBtn.addEventListener('click', ()=>createRow());
taxInput.addEventListener('input', computeTotals);
// Function for real-time total calculation
function computeTotals(){
let subtotal=0;
const qtys = Array.from(document.getElementsByName('items_qty[]'));
const prices = Array.from(document.getElementsByName('items_price[]'));
for(let i=0;i<qtys.length;i++){
const q = parseFloat(qtys[i].value)||0;
const p = parseFloat(prices[i].value)||0;
subtotal += q*p;
}
const taxPerc = parseFloat(taxInput.value)||0;
const taxAmt = subtotal * (taxPerc/100);
const total = subtotal + taxAmt;
subtotalEl.textContent = '$'+subtotal.toFixed(2);
taxAmountEl.textContent = '$'+taxAmt.toFixed(2);
totalEl.textContent = '$'+total.toFixed(2);
}
// initial one item
createRow({name:'',desc:'',qty:1,price:0});
computeTotals();
// Logo preview handling
if(logoInput){
logoInput.addEventListener('change', function(){
const f = this.files && this.files[0];
if(!f){ logoPreviewWrap.style.display='none'; logoPreviewImg.src=''; return; }
if(f.size > 2*1024*1024){ alert('Logo must be under 2 MB'); this.value=''; return; }
const allowed = ['image/png','image/jpeg','image/gif'];
if(!allowed.includes(f.type)) { alert('Only PNG/JPEG/GIF allowed'); this.value=''; return; }
const reader = new FileReader();
reader.onload = function(ev){ logoPreviewImg.src = ev.target.result; logoPreviewWrap.style.display='flex'; };
reader.readAsDataURL(f);
});
removeLogoBtn.addEventListener('click', function(){
logoInput.value = '';
logoPreviewImg.src = '';
logoPreviewWrap.style.display = 'none';
});
}
// Form submission and PDF generation
form.addEventListener('submit', async function(e){
e.preventDefault();
const btn = document.getElementById('create-btn');
if(!form.checkValidity()){
form.reportValidity();
return;
}
btn.disabled = true; btn.textContent = 'Generating...';
const data = new FormData(form);
try{
const resp = await fetch('generate_pdf.php', {method:'POST', body:data});
if(!resp.ok){
const text = await resp.text();
throw new Error(text || 'Server error');
}
const blob = await resp.blob();
const filename = getFileNameFromDisposition(resp.headers.get('Content-Disposition')) || 'invoice.pdf';
const url = URL.createObjectURL(blob);
const a = document.createElement('a'); a.href = url; a.download = filename; document.body.appendChild(a); a.click(); a.remove();
URL.revokeObjectURL(url);
}catch(err){
alert('Error: '+err.message);
}finally{ btn.disabled=false; btn.textContent='Create Invoice (Download PDF)'; }
});
// Helper to extract filename from Content-Disposition header
function getFileNameFromDisposition(disposition){
if(!disposition) return null;
const match = /filename\*?=([^;]+)/i.exec(disposition);
if(!match) return null;
let fn = match[1].trim();
fn = fn.replace(/UTF-8''/i,'');
fn = fn.replace(/"/g,'');
return decodeURIComponent(fn);
}
})();
The generate_pdf.php file processes the submitted form data, generates a PDF invoice using the Dompdf library, and returns it for download using PHP. It includes CSRF protection, input sanitization, and process POST data securely and output binary PDF.
hash_equals($_SESSION['csrftoken'], $_POST['csrftoken'])floatval_safe() for numerics$dompdf->loadHtml($html); $dompdf->setPaper('A4', 'portrait'); $pdf = $dompdf->output();Content-Type: application/pdf, Content-Disposition: attachmentUse number_format($value, 2) for currency formatting and nl2br() for notes.
<?php
// Start session
session_start();
// Load Dompdf library
require __DIR__ . '/vendor/autoload.php';
use Dompdf\Dompdf;
use Dompdf\Options;
// Only accept POST
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
echo 'Method not allowed';
exit;
}
// Validate CSRF token
$csrf = $_POST['csrf_token'] ?? '';
if (empty($_SESSION['csrf_token']) || !hash_equals($_SESSION['csrf_token'], $csrf)) {
http_response_code(400);
echo 'Invalid CSRF token';
exit;
}
// Helper sanitizers
function clean($s){ return htmlspecialchars(trim($s), ENT_QUOTES, 'UTF-8'); }
function floatval_safe($v){ return is_numeric($v) ? (float)$v : 0.0; }
// Collect and sanitize fields
$customer = [
'first'=>clean($_POST['customer_first'] ?? ''),
'last'=>clean($_POST['customer_last'] ?? ''),
'business'=>clean($_POST['customer_business'] ?? ''),
'phone'=>clean($_POST['customer_phone'] ?? ''),
'address'=>clean($_POST['customer_address'] ?? ''),
'city'=>clean($_POST['customer_city'] ?? ''),
'state'=>clean($_POST['customer_state'] ?? ''),
'zip'=>clean($_POST['customer_zip'] ?? ''),
'country'=>clean($_POST['customer_country'] ?? ''),
];
$seller = [
'business'=>clean($_POST['seller_business'] ?? ''),
'email'=>filter_var($_POST['seller_email'] ?? '', FILTER_SANITIZE_EMAIL),
'phone'=>clean($_POST['seller_phone'] ?? ''),
'website'=>filter_var($_POST['seller_website'] ?? '', FILTER_SANITIZE_URL),
'address'=>clean($_POST['seller_address'] ?? ''),
'city'=>clean($_POST['seller_city'] ?? ''),
'state'=>clean($_POST['seller_state'] ?? ''),
'zip'=>clean($_POST['seller_zip'] ?? ''),
'country'=>clean($_POST['seller_country'] ?? ''),
];
$invoice = [
'number'=>clean($_POST['invoice_number'] ?? ''),
'issue'=>clean($_POST['issue_date'] ?? ''),
'due'=>clean($_POST['due_date'] ?? ''),
];
$notes = clean($_POST['notes'] ?? '');
$tax = floatval_safe($_POST['tax'] ?? 0);
// Collect invoice items
$items = [];
$names = $_POST['items_name'] ?? [];
$descs = $_POST['items_desc'] ?? [];
$qtys = $_POST['items_qty'] ?? [];
$prices = $_POST['items_price'] ?? [];
for($i=0;$i<count($names);$i++){
$n = clean($names[$i] ?? '');
$d = clean($descs[$i] ?? '');
$q = intval($qtys[$i] ?? 0);
$p = floatval_safe($prices[$i] ?? 0);
if($n === '' && $d === '' && $q<=0 && $p==0) continue;
$items[] = ['name'=>$n,'desc'=>$d,'qty'=>$q,'price'=>$p];
}
if(empty($items)){
http_response_code(400);
echo 'At least one invoice item is required.';
exit;
}
// Handle logo upload (optional).
$tempLogo = '';
if (!empty($_FILES['logo']) && $_FILES['logo']['error'] !== UPLOAD_ERR_NO_FILE) {
// Check for upload errors
if ($_FILES['logo']['error'] !== UPLOAD_ERR_OK) {
http_response_code(400);
exit('Logo upload error');
}
// File type validation
$allowed = ['image/png', 'image/jpeg', 'image/gif'];
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$mime = finfo_file($finfo, $_FILES['logo']['tmp_name']);
finfo_close($finfo);
if (!in_array($mime, $allowed)) {
http_response_code(400);
exit('Invalid logo type');
}
// File size limit (2MB)
if ($_FILES['logo']['size'] > 2 * 1024 * 1024) {
http_response_code(400);
exit('Logo too large');
}
// Temp file path
$tmpPath = $_FILES['logo']['tmp_name'];
// Build base64 data URI
$imageData = file_get_contents($tmpPath);
$base64 = 'data:' . $mime . ';base64,' . base64_encode($imageData);
$tempLogo = str_replace(' ', '+', $base64);
}
// Build HTML for PDF
function money($v){ return number_format((float)$v,2); }
$subtotal = 0;
$itemsHtml = '';
foreach($items as $it){
$line = $it['qty'] * $it['price'];
$subtotal += $line;
$itemsHtml .= "<tr>\n".
"<td style=\"border:1px solid #ccc;padding:8px;\">". $it['name'] ."</td>\n".
"<td style=\"border:1px solid #ccc;padding:8px;\">". $it['desc'] ."</td>\n".
"<td style=\"border:1px solid #ccc;padding:8px;text-align:right;\">". $it['qty'] ."</td>\n".
"<td style=\"border:1px solid #ccc;padding:8px;text-align:right;\">". money($it['price']) ."</td>\n".
"<td style=\"border:1px solid #ccc;padding:8px;text-align:right;\">". money($line) ."</td>\n".
"</tr>\n";
}
$taxAmt = $subtotal * ($tax/100);
$total = $subtotal + $taxAmt;
$html = '<!doctype html><html><head><meta charset="utf-8"><style>' .
'body{font-family:Arial,Helvetica,sans-serif;font-size:12px;color:#222} .header{display:flex;justify-content:space-between;align-items:center} .seller{font-weight:700}' .
'table{width:100%;border-collapse:collapse;margin-top:12px}' .
'</style></head><body>';
$html .= '<div class="header">';
$html .= '<div class="left">';
if (!empty($tempLogo)) {
$html .= '<img src="' . $tempLogo . '" height="80" style="margin-bottom:10px;">';
}
$html .= '<div class="seller">'. $seller['business'] .'</div>';
$html .= '<div>'. $seller['address'] .'<br>'. $seller['city'].' '. $seller['state'].' '. $seller['zip'].'<br>'. $seller['country'] .'</div>';
$html .= '</div>';
$html .= '<div class="right">';
$html .= '<h2>Invoice</h2>';
$html .= '<div><strong>Invoice #:</strong> '. $invoice['number'] .'</div>';
$html .= '<div><strong>Issue:</strong> '. $invoice['issue'] .'</div>';
$html .= '<div><strong>Due:</strong> '. $invoice['due'] .'</div>';
$html .= '</div></div>';
$html .= '<hr>';
$html .= '<div style="display:flex;justify-content:space-between">';
$html .= '<div><strong>Bill To:</strong><br>'. $customer['first'].' '. $customer['last'] .'<br>'. $customer['business'] .'<br>'. $customer['address'] .'<br>'. $customer['city'].' '. $customer['state'].' '. $customer['zip'] .'<br>'. $customer['country'] .'</div>';
$html .= '</div>';
$html .= '<table><thead><tr><th style="border:1px solid #ccc;padding:8px">Item</th><th style="border:1px solid #ccc;padding:8px">Description</th><th style="border:1px solid #ccc;padding:8px">Qty</th><th style="border:1px solid #ccc;padding:8px">Price</th><th style="border:1px solid #ccc;padding:8px">Line Total</th></tr></thead><tbody>';
$html .= $itemsHtml;
$html .= '</tbody></table>';
$html .= '<div style="width:300px;margin-left:auto;margin-top:12px">';
$html .= '<table style="width:100%">';
$html .= '<tr><td style="padding:6px">Subtotal:</td><td style="padding:6px;text-align:right">$'.money($subtotal).'</td></tr>';
$html .= '<tr><td style="padding:6px">Tax ('.htmlspecialchars($tax).'%):</td><td style="padding:6px;text-align:right">$'.money($taxAmt).'</td></tr>';
$html .= '<tr><td style="padding:6px;font-weight:700">Total:</td><td style="padding:6px;text-align:right;font-weight:700">$'.money($total).'</td></tr>';
$html .= '</table></div>';
if($notes){ $html .= '<h3>Notes</h3><p>'. nl2br($notes) .'</p>'; }
$html .= '</body></html>';
// Generate PDF via Dompdf
$options = new Options();
$options->set('isRemoteEnabled', true);
$dompdf = new Dompdf($options);
$dompdf->loadHtml($html);
$dompdf->setPaper('A4','portrait');
$dompdf->render();
// Render PDF to string (avoid direct streaming which can be corrupted by prior output)
$pdf = $dompdf->output();
// Ensure no accidental output (warnings/notices) remain β clean all output buffers
while (ob_get_level() > 0) {
@ob_end_clean();
}
// Turn off display_errors so PHP won't inject warnings into the binary stream
ini_set('display_errors', '0');
// Send proper binary headers
$filename = 'invoice_'.time().'.pdf';
header('Content-Type: application/pdf');
header('Content-Disposition: attachment; filename="' . rawurlencode($filename) . '"; filename*=UTF-8\'\'' . rawurlencode($filename));
header('Content-Transfer-Encoding: binary');
header('Accept-Ranges: bytes');
header('Content-Length: ' . strlen($pdf));
echo $pdf;
exit;
?>
β Professional invoice PDF
β Dynamic & reusable
β Client-ready
β Perfect for SaaS, billing systems, freelancers
With this guide, you can create dynamic, professional invoices in PDF format using PHP and Dompdf. Customize the template to fit your brand and business needs. This Generate Invoice PDF with PHP (AJAX) solution is:
You can extend this Invoice PDF Generator script further by adding Database storage, Invoice numbers, Currency selection, Email PDF to client, and Payment gateway integration. Happy coding!
If you want complete project with all features, check out our PHP Invoice Management System Script.
– Convert HTML to PDF in PHP with Dompdf: https://www.codexworld.com/convert-html-to-pdf-php-dompdf/
– Export MySQL Data to Excel using PHP: https://www.codexworld.com/export-data-to-excel-in-php/
Looking for expert assistance to implement or extend this scriptβs functionality? Submit a Service Request
π° Budget-friendly β’ π Global clients β’ π Production-ready solutions