Speeding Up Laravel Tests with tmpfs (MySQL in RAM)
If your Laravel test suite is painfully slow and does a lot of database work (multi-tenancy, migrations, seeding), the bottleneck is almost…
Rendering Chart.js charts into PDFs with wkhtmltopdf can be surprisingly tricky. wkhtmltopdf uses QtWebKit (an older browser engine), while Chart.js draws on an HTML5 canvas and often assumes modern browser APIs. This post shows a robust, production-ready approach to make them play nicely together in a Laravel app.
wkhtmltopdf runs an old WebKit that's missing or has buggy JS features like Function.prototype.bind and CanvasRenderingContext2D.setLineDash. Canvas timing is another issue since charts may render after wkhtmltopdf snapshots the page. Responsive and animated charts cause additional problems, often resulting in blank or partial renders due to animations and fluid sizing.
Use wkhtmltopdf 0.12.6 or higher with patched Qt. Older versions are prone to Chart.js issues and crashes. Chart.js 2.9.x works best for compatibility with QtWebKit. You need to disable animations and responsiveness in Chart.js and fix the canvas size explicitly using both width/height attributes and CSS.
Apply two small polyfills (shown below) to fix compatibility issues. Signal wkhtmltopdf when JS is done via window.status combined with the wkhtml "window-status" option. For extra reliability, replace the canvas with a static PNG using canvas.toDataURL just before printing, since wkhtmltopdf captures images more reliably than live canvas.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<!-- Use Chart.js 2.9.x for compatibility -->
<script src="https://cdn.jsdelivr.net/npm/chart.js@2.9.4/dist/Chart.min.js"></script>
<style>
.chart-wrap { width: 900px; height: 420px; margin: 0 auto; }
</style>
</head>
<body>
<h2>Monthly Signups</h2>
<div class="chart-wrap">
<!-- Fixed size helps wkhtmltopdf capture correctly -->
<canvas id="reportChart" width="900" height="420" style="width:900px;height:420px;"></canvas>
<!-- We'll swap the canvas for this image right before printing -->
<img id="reportChartImage" alt="chart" style="display:none;width:900px;height:420px;" />
</div>
<!-- wkhtmltopdf compatibility polyfills -->
<script>
(function(setLineDash){
if (setLineDash) {
CanvasRenderingContext2D.prototype.setLineDash = function(){
if (!arguments[0] || !arguments[0].length) { arguments[0] = [1,0]; }
return setLineDash.apply(this, arguments);
};
}
})(CanvasRenderingContext2D && CanvasRenderingContext2D.prototype ? CanvasRenderingContext2D.prototype.setLineDash : null);
Function.prototype.bind = Function.prototype.bind || function (thisp) {
var fn = this;
return function () { return fn.apply(thisp, arguments); };
};
</script>
<script>
(function() {
function render() {
try {
var canvas = document.getElementById('reportChart');
if (!canvas) { window.status = 'readyToPrint'; return; }
var ctx = canvas.getContext('2d');
var config = {
type: 'bar',
data: {
labels: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun'],
datasets: [{
label: 'Signups',
data: [12, 32, 18, 27, 33, 21],
backgroundColor: '#2E7D32',
borderColor: '#2E7D32',
fill: true
}]
},
options: {
responsive: false,
maintainAspectRatio: false,
devicePixelRatio: 1,
animation: { duration: 0 },
hover: { animationDuration: 0 },
responsiveAnimationDuration: 0,
legend: { display: true },
title: { display: false }
}
};
var chart = new Chart(ctx, config);
// Replace canvas with image for rock-solid capture
setTimeout(function(){
try {
var img = document.getElementById('reportChartImage');
if (img) {
img.src = canvas.toDataURL('image/png');
img.style.display = 'block';
canvas.parentNode && canvas.parentNode.removeChild(canvas);
}
} catch (ignore) {}
// Tell wkhtmltopdf we're done
window.status = 'readyToPrint';
}, 100);
} catch (e) {
window.status = 'readyToPrint';
}
}
if (document.readyState === 'complete' || document.readyState === 'interactive') {
render();
} else {
window.addEventListener('load', render);
}
})();
</script>
</body>
</html>
wkhtmltopdf \
--enable-javascript \
--javascript-delay 4000 \
--window-status readyToPrint \
--no-stop-slow-scripts \
--enable-local-file-access \
http://localhost/example-report.html out.pdf
These options do the following:
public function exportPdf()
{
// Render a view that inlines Chart.js and the polyfills above
$html = view('reports.pdf-example', [/* your data */])->render();
// Example using Snappy (wkhtmltopdf)
$pdf = \PDF::loadHTML($html)
->setOption('enable-javascript', true)
->setOption('no-stop-slow-scripts', true)
->setOption('enable-local-file-access', true)
->setOption('javascript-delay', 4000)
->setOption('window-status', 'readyToPrint')
->setOption('page-size', 'A4')
->setOption('orientation', 'Portrait');
return $pdf->download('report.pdf');
}
Blank chart
Ensure window.status is set to 'readyToPrint' after rendering. Increase --javascript-delay if data loading is slow. Verify fixed sizes: both canvas width/height attributes and CSS.
Segmentation fault or crash (0.12.4/0.12.5)
Use the setLineDash polyfill shown above or upgrade to 0.12.6+.
Blurry charts
Increase the canvas size (e.g., 1200×560) and allow downscale in CSS. Keep devicePixelRatio = 1 for consistency, or render at higher logical size before toDataURL.
Chart.js v3+
QtWebKit lacks ES features used by v3. Prefer v2.9.x with wkhtmltopdf. Alternative: switch to a headless Chromium renderer (e.g., Puppeteer) or an SVG charting library.
External assets blocked
Use --enable-local-file-access and inline assets where possible.
For production deployments, ensure you have wkhtmltopdf 0.12.6 or higher with patched Qt and Chart.js 2.9.x. Disable animations and responsiveness, use fixed canvas size with both attributes and CSS, and include the Function.prototype.bind and setLineDash polyfills. Replace the canvas with PNG via canvas.toDataURL before printing. Use window-status and a sensible javascript-delay. Inline assets or allow local asset access.
With these practices, Chart.js charts render consistently and clearly in PDFs generated by wkhtmltopdf without resorting to vendor-specific hacks or fragile timing tricks.
Give Vroni a GitHub issue, bug report, spec, or rough idea. It reads the repo, plans the change, writes code, runs checks, and works toward a review-ready pull request.
Take a look at vroni.com