
wkhtmltopdf with Patched Qt: The Complete Developer's Guide

Introduction
PDF generation from HTML remains one of the core challenges in web development. In the PHP world, wkhtmltopdf stands as one of the most popular tools for this task — a command-line utility that uses the WebKit engine to render HTML into PDF. However, not all wkhtmltopdf versions are created equal. In this comprehensive article, we'll explore the critical differences between regular and "patched" versions, why the latter is essential for production applications, and how to properly install and configure it.
What is wkhtmltopdf?
wkhtmltopdf is an open-source command-line utility developed for converting HTML and CSS to PDF and various image formats. Created by Jakob Truelsen in 2008, it has since become the de-facto standard for server-side PDF generation in web applications.
Key advantages of wkhtmltopdf:
- Accurate rendering: Uses the WebKit engine, the same as in browsers
- CSS support: Full CSS support, including media queries
- JavaScript: Ability to execute JavaScript before PDF generation
- Headers and footers: Built-in header/footer support
- Stability: Time-tested solution
- Integration: Easy integration with PHP through libraries like SnappyPDF
The Problem: Regular vs Patched Qt
Regular (Unpatched) Version
When you install wkhtmltopdf through standard package managers (apt, yum, etc.), you get a version compiled against the system Qt library. This version has significant limitations:
$ sudo apt-get install wkhtmltopdf
$ wkhtmltopdf --version
wkhtmltopdf 0.12.6
$ wkhtmltopdf --print-media-type test.html test.pdf
The switch --print-media-type, is not support using unpatched qt, and will be ignored.
Limitations of the unpatched version:
- No print media query support — CSS
@media print
rules are ignored - Landscape orientation issues — incorrect handling of landscape orientation
- Unstable header/footer — positioning and styling problems
- Limited CSS3 support — some modern CSS properties don't work
- Font issues — incorrect rendering of custom fonts
Patched Qt Version
The "patched" version of wkhtmltopdf comes with a specially modified Qt WebKit that includes numerous print-specific fixes:
$ wkhtmltopdf --version
wkhtmltopdf 0.12.6 (with patched qt)
$ wkhtmltopdf --print-media-type test.html test.pdf
Loading page (1/2)
Printing pages (2/2)
Done
Advantages of the patched version:
- Full print media query support — correct handling of CSS for print
- Stable landscape orientation — flawless landscape orientation handling
- Reliable header/footer — proper positioning and repetition on each page
- Modern CSS support — support for CSS3, flexbox, grid
- Quality typography — correct font and kerning handling
- Precise positioning — pixel-perfect element positioning
Technical Differences
Anatomy of the Patches
The patches in Qt WebKit for wkhtmltopdf include hundreds of changes aimed at improving print functionality:
// Example patch for print media query support
// Original Qt WebKit
bool WebPage::shouldApplyPrintMediaType() const
{
return false; // Always returned false
}
// Patched version
bool WebPage::shouldApplyPrintMediaType() const
{
return m_printMediaTypeEnabled; // Considers settings
}
Key improvement areas:
- Media Query Engine: Redesigned media query processing mechanism
- Page Layout: Improved page layout algorithm
- Font Rendering: Optimized font rendering for print
- CSS Parser: Extended CSS property support for print
- Memory Management: Optimized memory management for large documents
Installing the Patched Version
Ubuntu/Debian
# Remove old version
sudo apt-get remove wkhtmltopdf
# Download patched version
wget https://github.com/wkhtmltopdf/packaging/releases/download/0.12.6.1-3/wkhtmltox_0.12.6.1-3.jammy_amd64.deb
# Install dependencies
sudo apt-get update
sudo apt-get install -y libfontconfig1 libfreetype6 libx11-6 libxext6 libxrender1
# Install package
sudo dpkg -i wkhtmltox_0.12.6.1-3.jammy_amd64.deb
sudo apt-get install -f
CentOS/RHEL
# Remove old version
sudo yum remove wkhtmltopdf
# Download and install
wget https://github.com/wkhtmltopdf/packaging/releases/download/0.12.6.1-3/wkhtmltox-0.12.6.1-3.centos8.x86_64.rpm
sudo yum localinstall wkhtmltox-0.12.6.1-3.centos8.x86_64.rpm
macOS
# With Homebrew
brew install --cask wkhtmltopdf
# Or download from official site
wget https://github.com/wkhtmltopdf/packaging/releases/download/0.12.6.1-3/wkhtmltox-0.12.6.1-3.macos-cocoa.pkg
Docker (Recommended for production)
FROM ubuntu:20.04
RUN apt-get update && apt-get install -y \
wget fontconfig libfontconfig1 libfreetype6 \
libx11-6 libxext6 libxrender1 xvfb
RUN wget -O wkhtmltox.deb \
https://github.com/wkhtmltopdf/packaging/releases/download/0.12.6.1-3/wkhtmltox_0.12.6.1-3.focal_amd64.deb && \
dpkg -i wkhtmltox.deb && \
rm wkhtmltox.deb
# Headless wrapper
RUN echo '#!/bin/bash\nxvfb-run -a wkhtmltopdf "$@"' > /usr/local/bin/wkhtmltopdf-headless && \
chmod +x /usr/local/bin/wkhtmltopdf-headless
Practical Examples
CSS for print with patched version
/* Basic print styles */
@page {
size: A4 portrait;
margin: 2cm 1.5cm;
}
@page:first {
margin-top: 5cm; /* More space for header */
}
@page:left {
margin-left: 3cm;
margin-right: 1cm;
}
@page:right {
margin-left: 1cm;
margin-right: 3cm;
}
/* Media queries only work with patched version */
@media print {
/* Hide navigation elements */
.navigation, .sidebar {
display: none !important;
}
/* Optimize typography */
body {
font-size: 10pt;
line-height: 1.4;
color: #000;
}
/* Handle page breaks */
.page-break {
page-break-before: always;
}
.no-break {
page-break-inside: avoid;
}
/* Special table styles */
table {
border-collapse: collapse;
page-break-inside: auto;
}
tr {
page-break-inside: avoid;
page-break-after: auto;
}
thead {
display: table-header-group;
}
tfoot {
display: table-footer-group;
}
}
/* Screen styles (not applied in PDF) */
@media screen {
.print-only {
display: none;
}
}
Advanced capabilities
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Advanced PDF Report</title>
<style>
@page {
size: A4 landscape;
margin: 15mm;
@top-left {
content: "Company Report";
font-size: 8pt;
color: #666;
}
@top-right {
content: "Page " counter(page) " of " counter(pages);
font-size: 8pt;
color: #666;
}
@bottom-center {
content: "Confidential";
font-size: 8pt;
color: #999;
}
}
/* CSS Grid supported in patched version */
.dashboard {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 20px;
margin: 20px 0;
}
.metric-card {
border: 1px solid #ddd;
padding: 15px;
border-radius: 8px;
text-align: center;
}
/* Flexbox for alignment */
.flex-container {
display: flex;
justify-content: space-between;
align-items: center;
}
/* CSS3 gradients work */
.chart-header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 10px;
border-radius: 5px 5px 0 0;
}
</style>
</head>
<body>
<h1>Financial Dashboard</h1>
<div class="dashboard">
<div class="metric-card">
<h3>Revenue</h3>
<div class="metric-value">$125,000</div>
</div>
<div class="metric-card">
<h3>Expenses</h3>
<div class="metric-value">$89,500</div>
</div>
<div class="metric-card">
<h3>Profit</h3>
<div class="metric-value">$35,500</div>
</div>
</div>
<div class="chart-section">
<div class="chart-header">
<div class="flex-container">
<h2>Monthly Trends</h2>
<span>July 2025</span>
</div>
</div>
<!-- Chart content -->
</div>
</body>
</html>
PHP Integration
SnappyPDF
// composer require knplabs/knp-snappy
use Knp\Snappy\Pdf;
$snappy = new Pdf('/usr/local/bin/wkhtmltopdf');
// Settings for patched version
$snappy->setOptions([
'page-size' => 'A4',
'orientation' => 'landscape',
'print-media-type' => true, // Only works with patched!
'enable-javascript' => true,
'javascript-delay' => 1000,
'enable-smart-shrinking' => false,
'margin-top' => '15mm',
'margin-bottom' => '15mm',
'margin-left' => '15mm',
'margin-right' => '15mm',
'header-html' => $headerHtml,
'footer-html' => $footerHtml,
'header-spacing' => 5,
'footer-spacing' => 5,
]);
$html = view('reports.invoice', compact('data'))->render();
$pdf = $snappy->getOutputFromHtml($html);
return response($pdf, 200, [
'Content-Type' => 'application/pdf',
'Content-Disposition' => 'attachment; filename="invoice.pdf"'
]);
Laravel DomPDF vs SnappyPDF
// Performance and quality comparison
// DomPDF - pure PHP, limited CSS
$pdf = PDF::loadView('report', $data);
// ❌ No CSS Grid/Flexbox support
// ❌ Limited CSS3 support
// ❌ Complex layout issues
// SnappyPDF with patched wkhtmltopdf
$pdf = SnappyPdf::loadHTML($html)
->setOption('print-media-type', true)
->setOrientation('landscape');
// ✅ Full modern CSS support
// ✅ Accurate browser-like rendering
// ✅ Excellent performance
Installation Automation
Ansible Playbook
---
- name: Install wkhtmltopdf with patched Qt
hosts: webservers
become: yes
vars:
wkhtmltopdf_version: "0.12.6.1-3"
wkhtmltopdf_package: "wkhtmltox_{{ wkhtmltopdf_version }}.focal_amd64.deb"
wkhtmltopdf_url: "https://github.com/wkhtmltopdf/packaging/releases/download/{{ wkhtmltopdf_version }}/{{ wkhtmltopdf_package }}"
tasks:
- name: Remove existing wkhtmltopdf
apt:
name: wkhtmltopdf
state: absent
- name: Install dependencies
apt:
name:
- libfontconfig1
- libfreetype6
- libx11-6
- libxext6
- libxrender1
- fontconfig
- xvfb
state: present
update_cache: yes
- name: Download wkhtmltopdf with patched Qt
get_url:
url: "{{ wkhtmltopdf_url }}"
dest: "/tmp/{{ wkhtmltopdf_package }}"
- name: Install wkhtmltopdf
apt:
deb: "/tmp/{{ wkhtmltopdf_package }}"
state: present
- name: Create symlink
file:
src: /usr/local/bin/wkhtmltopdf
dest: /usr/bin/wkhtmltopdf
state: link
force: yes
- name: Verify installation
command: wkhtmltopdf --version
register: version_output
- name: Check for patched Qt
fail:
msg: "wkhtmltopdf is not using patched Qt"
when: "'with patched qt' not in version_output.stdout"
- name: Test PDF generation
shell: |
echo '<h1>Test</h1>' | wkhtmltopdf --print-media-type - /tmp/test.pdf
test -f /tmp/test.pdf
register: test_result
- name: Cleanup test file
file:
path: /tmp/test.pdf
state: absent
Monitoring and Diagnostics
Runtime version checking
class WkhtmltopdfChecker
{
public static function checkVersion()
{
$version = shell_exec('wkhtmltopdf --version 2>&1');
return [
'version' => trim($version),
'is_patched' => strpos($version, 'with patched qt') !== false,
'binary_path' => shell_exec('which wkhtmltopdf'),
'is_executable' => is_executable(trim(shell_exec('which wkhtmltopdf')))
];
}
public static function validateConfiguration()
{
$info = self::checkVersion();
if (!$info['is_patched']) {
throw new Exception('wkhtmltopdf is not using patched Qt. Print media queries will not work.');
}
if (!$info['is_executable']) {
throw new Exception('wkhtmltopdf binary is not executable.');
}
return $info;
}
}
// In Laravel service provider
public function boot()
{
if (app()->environment('production')) {
try {
WkhtmltopdfChecker::validateConfiguration();
} catch (Exception $e) {
Log::critical('wkhtmltopdf configuration error', [
'error' => $e->getMessage()
]);
}
}
}
Logging and debugging
class PdfGenerator
{
private function generatePdf($html, $options = [])
{
$start = microtime(true);
Log::info('PDF generation started', [
'options' => $options,
'html_size' => strlen($html)
]);
try {
$pdf = $this->snappy->getOutputFromHtml($html);
$duration = microtime(true) - $start;
Log::info('PDF generation completed', [
'duration' => $duration,
'pdf_size' => strlen($pdf)
]);
return $pdf;
} catch (Exception $e) {
Log::error('PDF generation failed', [
'error' => $e->getMessage(),
'options' => $options,
'duration' => microtime(true) - $start
]);
throw $e;
}
}
}
Performance Optimization
Caching
class CachedPdfGenerator
{
private $cache;
private $snappy;
public function __construct(Cache $cache, Pdf $snappy)
{
$this->cache = $cache;
$this->snappy = $snappy;
}
public function generateFromTemplate($template, $data, $options = [])
{
// Create cache key based on template, data and options
$cacheKey = 'pdf_' . md5($template . serialize($data) . serialize($options));
return $this->cache->remember($cacheKey, 3600, function () use ($template, $data, $options) {
$html = view($template, $data)->render();
return $this->snappy->getOutputFromHtml($html, $options);
});
}
}
Queue jobs
// Job for asynchronous PDF generation
class GeneratePdfJob implements ShouldQueue
{
private $reportId;
private $template;
private $data;
public function handle(PdfGenerator $generator)
{
$pdf = $generator->generateFromTemplate($this->template, $this->data);
// Save PDF to storage
Storage::put("reports/{$this->reportId}.pdf", $pdf);
// Notify user
$this->notifyUser();
}
}
// Usage
GeneratePdfJob::dispatch($reportId, 'reports.invoice', $data);
Alternatives and Comparison
Modern alternatives
Puppeteer/Chrome Headless
const puppeteer = require('puppeteer');
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.setContent(html);
const pdf = await page.pdf({
format: 'A4',
landscape: true,
printBackground: true
});
Playwright
use Nesk\Puphpeteer\Puppeteer;
$puppeteer = new Puppeteer;
$browser = $puppeteer->launch();
$page = $browser->newPage();
$page->setContent($html);
$pdf = $page->pdf(['format' => 'A4']);
WeasyPrint (Python-based, probably micro service or docker)
import weasyprint
html = weasyprint.HTML(string=html_string)
pdf = html.write_pdf()
Comparison table
Solution | CSS Support | Performance | Memory Usage | Maintenance |
wkhtmltopdf (patched) | ★★★★☆ | ★★★★★ | ★★★★☆ | ★★★☆☆ |
Puppeteer | ★★★★★ | ★★★☆☆ | ★★☆☆☆ | ★★★★☆ |
DomPDF | ★★☆☆☆ | ★★★★★ | ★★★★★ | ★★★★★ |
WeasyPrint | ★★★★☆ | ★★★☆☆ | ★★★☆☆ | ★★★☆☆ |
Conclusion
wkhtmltopdf with patched Qt remains one of the best solutions for server-side PDF generation, especially in PHP applications. Despite the emergence of modern alternatives like Puppeteer, the patched version of wkhtmltopdf provides:
- Stability: Years-tested solution
- Performance: Optimized resource consumption
- Compatibility: Excellent integration with existing PHP applications
- Functionality: Full support for modern web standards
Key takeaways:
- Always use the patched version for production applications
- Plan installation in advance — the patched version requires manual installation
- Test print media queries — this is the main indicator of correct functionality
- Monitor version at runtime — add checks to your CI/CD pipeline
- Consider Docker for consistency across environments
A properly configured wkhtmltopdf with patched Qt will provide your application with high-quality PDF document generation with minimal overhead and maximum stability.
🎁 Bonus Script: One-Command wkhtmltopdf Installation
Made it this far? Here's your reward - a universal installer that handles everything automatically:
#!/bin/bash
# =============================================================================
# wkhtmltopdf Patched Qt Universal Installer
# =============================================================================
#
# Universal script for installing wkhtmltopdf with patched Qt
# Supports: Ubuntu 18.04+, Debian 9+, CentOS 7+, Amazon Linux 2
#
# Author: DevOps Team
# Version: 2.1.0
# Date: 2024
#
# Usage:
# chmod +x install_wkhtmltopdf.sh
# sudo ./install_wkhtmltopdf.sh
#
# Options:
# --version VER Specify version to install (default: 0.12.6.1-3)
# --force Force reinstallation without confirmation
# --test Run tests only without installation
# --docker Prepare for Docker environment
# --headless Install with headless mode support
# --help Show help
#
# =============================================================================
set -euo pipefail
# Colors for output
readonly RED='\033[0;31m'
readonly GREEN='\033[0;32m'
readonly YELLOW='\033[1;33m'
readonly BLUE='\033[0;34m'
readonly NC='\033[0m' # No Color
# Configuration
readonly SCRIPT_VERSION="2.1.0"
readonly DEFAULT_WKHTMLTOPDF_VERSION="0.12.6.1-3"
readonly GITHUB_RELEASES_URL="https://github.com/wkhtmltopdf/packaging/releases/download"
readonly TEMP_DIR="/tmp/wkhtmltopdf-installer-$$"
readonly LOG_FILE="/var/log/wkhtmltopdf-install.log"
# Global variables
WKHTMLTOPDF_VERSION="$DEFAULT_WKHTMLTOPDF_VERSION"
FORCE_INSTALL=false
TEST_ONLY=false
DOCKER_MODE=false
HEADLESS_MODE=false
DETECTED_OS=""
DETECTED_VERSION=""
DETECTED_ARCH=""
PACKAGE_URL=""
PACKAGE_NAME=""
# =============================================================================
# Utilities
# =============================================================================
log() {
local level="$1"
shift
local message="$*"
local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
echo -e "${timestamp} [${level}] ${message}" | tee -a "$LOG_FILE" >&2
}
info() {
echo -e "${BLUE}[INFO]${NC} $*"
log "INFO" "$*"
}
warn() {
echo -e "${YELLOW}[WARN]${NC} $*"
log "WARN" "$*"
}
error() {
echo -e "${RED}[ERROR]${NC} $*"
log "ERROR" "$*"
}
success() {
echo -e "${GREEN}[SUCCESS]${NC} $*"
log "SUCCESS" "$*"
}
die() {
error "$*"
exit 1
}
# Check root privileges
check_root() {
if [[ $EUID -ne 0 ]]; then
die "This script must be run as root (use sudo)"
fi
}
# Setup temporary directory
setup_temp_dir() {
mkdir -p "$TEMP_DIR"
trap cleanup EXIT
}
cleanup() {
if [[ -d "$TEMP_DIR" ]]; then
rm -rf "$TEMP_DIR"
fi
}
# =============================================================================
# System Detection
# =============================================================================
detect_os() {
info "Detecting operating system..."
if [[ -f /etc/os-release ]]; then
source /etc/os-release
DETECTED_OS="$ID"
DETECTED_VERSION="$VERSION_ID"
elif [[ -f /etc/redhat-release ]]; then
if grep -q "CentOS" /etc/redhat-release; then
DETECTED_OS="centos"
DETECTED_VERSION=$(grep -oE '[0-9]+\.[0-9]+' /etc/redhat-release | head -1)
elif grep -q "Red Hat" /etc/redhat-release; then
DETECTED_OS="rhel"
DETECTED_VERSION=$(grep -oE '[0-9]+\.[0-9]+' /etc/redhat-release | head -1)
fi
else
die "Unable to detect operating system"
fi
# Detect architecture
DETECTED_ARCH=$(uname -m)
if [[ "$DETECTED_ARCH" == "x86_64" ]]; then
DETECTED_ARCH="amd64"
elif [[ "$DETECTED_ARCH" == "aarch64" ]]; then
DETECTED_ARCH="arm64"
else
die "Unsupported architecture: $DETECTED_ARCH"
fi
info "Detected system: $DETECTED_OS $DETECTED_VERSION ($DETECTED_ARCH)"
}
# Determine the correct package
determine_package() {
info "Determining appropriate package..."
case "$DETECTED_OS" in
"ubuntu")
case "$DETECTED_VERSION" in
"24.04")
PACKAGE_NAME="wkhtmltox_${WKHTMLTOPDF_VERSION}.jammy_${DETECTED_ARCH}.deb"
warn "Ubuntu 24.04 - using package for 22.04 (jammy)"
;;
"22.04")
PACKAGE_NAME="wkhtmltox_${WKHTMLTOPDF_VERSION}.jammy_${DETECTED_ARCH}.deb"
;;
"20.04")
PACKAGE_NAME="wkhtmltox_${WKHTMLTOPDF_VERSION}.focal_${DETECTED_ARCH}.deb"
;;
"18.04")
PACKAGE_NAME="wkhtmltox_${WKHTMLTOPDF_VERSION}.bionic_${DETECTED_ARCH}.deb"
;;
*)
warn "Unknown Ubuntu version $DETECTED_VERSION, trying jammy"
PACKAGE_NAME="wkhtmltox_${WKHTMLTOPDF_VERSION}.jammy_${DETECTED_ARCH}.deb"
;;
esac
;;
"debian")
if [[ "${DETECTED_VERSION%%.*}" -ge 11 ]]; then
PACKAGE_NAME="wkhtmltox_${WKHTMLTOPDF_VERSION}.bullseye_${DETECTED_ARCH}.deb"
elif [[ "${DETECTED_VERSION%%.*}" -eq 10 ]]; then
PACKAGE_NAME="wkhtmltox_${WKHTMLTOPDF_VERSION}.buster_${DETECTED_ARCH}.deb"
else
die "Unsupported Debian version: $DETECTED_VERSION"
fi
;;
"centos"|"rhel")
if [[ "${DETECTED_VERSION%%.*}" -eq 8 ]]; then
PACKAGE_NAME="wkhtmltox-${WKHTMLTOPDF_VERSION}.centos8.x86_64.rpm"
elif [[ "${DETECTED_VERSION%%.*}" -eq 7 ]]; then
PACKAGE_NAME="wkhtmltox-${WKHTMLTOPDF_VERSION}.centos7.x86_64.rpm"
else
die "Unsupported CentOS/RHEL version: $DETECTED_VERSION"
fi
;;
"amzn")
PACKAGE_NAME="wkhtmltox-${WKHTMLTOPDF_VERSION}.centos7.x86_64.rpm"
;;
*)
die "Unsupported operating system: $DETECTED_OS"
;;
esac
PACKAGE_URL="${GITHUB_RELEASES_URL}/${WKHTMLTOPDF_VERSION}/${PACKAGE_NAME}"
info "Selected package: $PACKAGE_NAME"
info "Download URL: $PACKAGE_URL"
}
# =============================================================================
# Dependencies Installation
# =============================================================================
install_dependencies() {
info "Installing dependencies..."
case "$DETECTED_OS" in
"ubuntu"|"debian")
export DEBIAN_FRONTEND=noninteractive
apt-get update -qq
local deps=(
"wget" "ca-certificates" "fontconfig" "libfontconfig1"
"libfreetype6" "libx11-6" "libxext6" "libxrender1"
"libstdc++6" "libc6" "zlib1g"
)
# Additional dependencies for different versions
if [[ "$DETECTED_OS" == "ubuntu" ]] && [[ "${DETECTED_VERSION}" > "20" ]]; then
deps+=("libgcc-s1")
else
deps+=("libgcc1")
fi
if [[ "$HEADLESS_MODE" == true ]]; then
deps+=("xvfb" "xfonts-base" "xfonts-75dpi")
fi
if [[ "$DOCKER_MODE" == true ]]; then
deps+=("fonts-liberation" "fonts-dejavu-core")
fi
apt-get install -y "${deps[@]}"
;;
"centos"|"rhel"|"amzn")
local package_manager="yum"
if command -v dnf >/dev/null 2>&1; then
package_manager="dnf"
fi
$package_manager update -y -q
local deps=(
"wget" "ca-certificates" "fontconfig" "freetype"
"libX11" "libXext" "libXrender" "libstdc++" "glibc" "zlib"
)
if [[ "$HEADLESS_MODE" == true ]]; then
deps+=("xorg-x11-server-Xvfb" "xorg-x11-fonts-base" "xorg-x11-fonts-75dpi")
fi
$package_manager install -y "${deps[@]}"
;;
esac
}
# =============================================================================
# Remove Existing Installation
# =============================================================================
remove_existing() {
info "Checking for existing installations..."
case "$DETECTED_OS" in
"ubuntu"|"debian")
if dpkg -l | grep -q wkhtmltopdf; then
if [[ "$FORCE_INSTALL" == true ]]; then
info "Removing existing wkhtmltopdf..."
apt-get remove -y wkhtmltopdf wkhtmltox
else
warn "Existing wkhtmltopdf installation detected"
read -p "Remove existing version? (y/N): " -n 1 -r
echo
if [[ $REPLY =~ ^[Yy]$ ]]; then
apt-get remove -y wkhtmltopdf wkhtmltox
else
die "Installation cancelled by user"
fi
fi
fi
;;
"centos"|"rhel"|"amzn")
if rpm -qa | grep -q wkhtmltopdf; then
if [[ "$FORCE_INSTALL" == true ]]; then
info "Removing existing wkhtmltopdf..."
rpm -e wkhtmltopdf || true
rpm -e wkhtmltox || true
else
warn "Existing wkhtmltopdf installation detected"
read -p "Remove existing version? (y/N): " -n 1 -r
echo
if [[ $REPLY =~ ^[Yy]$ ]]; then
rpm -e wkhtmltopdf || true
rpm -e wkhtmltox || true
else
die "Installation cancelled by user"
fi
fi
fi
;;
esac
}
# =============================================================================
# Download and Installation
# =============================================================================
download_package() {
info "Downloading wkhtmltopdf package..."
cd "$TEMP_DIR"
# Try downloading from main URL
if ! wget -q --timeout=30 --tries=3 "$PACKAGE_URL"; then
warn "Main URL unavailable, trying alternative sources..."
# Alternative URLs
local alt_urls=(
"https://downloads.wkhtmltopdf.org/${WKHTMLTOPDF_VERSION%%-*}/${WKHTMLTOPDF_VERSION}/${PACKAGE_NAME}"
"https://github.com/wkhtmltopdf/wkhtmltopdf/releases/download/${WKHTMLTOPDF_VERSION}/${PACKAGE_NAME}"
)
local downloaded=false
for url in "${alt_urls[@]}"; do
info "Trying alternative URL: $url"
if wget -q --timeout=30 --tries=3 "$url"; then
downloaded=true
break
fi
done
if [[ "$downloaded" == false ]]; then
die "Failed to download wkhtmltopdf package"
fi
fi
# Check file integrity
if [[ ! -f "$PACKAGE_NAME" ]] || [[ ! -s "$PACKAGE_NAME" ]]; then
die "Downloaded file is corrupted or empty"
fi
info "Package downloaded successfully: $(ls -lh "$PACKAGE_NAME" | awk '{print $5}')"
}
install_package() {
info "Installing wkhtmltopdf package..."
cd "$TEMP_DIR"
case "$DETECTED_OS" in
"ubuntu"|"debian")
# Try installing with dpkg
if dpkg -i "$PACKAGE_NAME" 2>/dev/null; then
success "Package installed successfully"
else
info "Fixing dependencies..."
apt-get install -f -y
# Retry
if dpkg -i "$PACKAGE_NAME"; then
success "Package installed after fixing dependencies"
else
die "Failed to install package"
fi
fi
;;
"centos"|"rhel"|"amzn")
local package_manager="yum"
if command -v dnf >/dev/null 2>&1; then
package_manager="dnf"
fi
if $package_manager localinstall -y "$PACKAGE_NAME"; then
success "Package installed successfully"
else
die "Failed to install package"
fi
;;
esac
}
# =============================================================================
# Post-installation Setup
# =============================================================================
setup_symlinks() {
info "Setting up symbolic links..."
# Standard installation paths
local possible_paths=(
"/usr/local/bin/wkhtmltopdf"
"/opt/wkhtmltox/bin/wkhtmltopdf"
"/usr/bin/wkhtmltopdf"
)
local installed_path=""
for path in "${possible_paths[@]}"; do
if [[ -f "$path" ]]; then
installed_path="$path"
break
fi
done
if [[ -z "$installed_path" ]]; then
die "Unable to find installed wkhtmltopdf"
fi
info "wkhtmltopdf found at: $installed_path"
# Create standard link if needed
if [[ "$installed_path" != "/usr/bin/wkhtmltopdf" ]]; then
if [[ -f "/usr/bin/wkhtmltopdf" ]]; then
rm -f "/usr/bin/wkhtmltopdf"
fi
ln -sf "$installed_path" "/usr/bin/wkhtmltopdf"
info "Created symbolic link: /usr/bin/wkhtmltopdf -> $installed_path"
fi
# Set proper permissions
chmod 755 "$installed_path"
# Create headless wrapper if needed
if [[ "$HEADLESS_MODE" == true ]]; then
create_headless_wrapper "$installed_path"
fi
}
create_headless_wrapper() {
local wkhtmltopdf_path="$1"
local wrapper_path="/usr/local/bin/wkhtmltopdf-headless"
info "Creating headless wrapper..."
cat > "$wrapper_path" << EOF
#!/bin/bash
# wkhtmltopdf headless wrapper
# Auto-generated by installer
set -euo pipefail
# Check for DISPLAY
if [[ -z "\${DISPLAY:-}" ]]; then
# Run with xvfb-run
exec xvfb-run -a --server-args="-screen 0 1024x768x24" "$wkhtmltopdf_path" "\$@"
else
# DISPLAY already set, run directly
exec "$wkhtmltopdf_path" "\$@"
fi
EOF
chmod +x "$wrapper_path"
info "Headless wrapper created: $wrapper_path"
}
# =============================================================================
# Testing
# =============================================================================
run_tests() {
info "Running tests..."
local test_dir="$TEMP_DIR/tests"
mkdir -p "$test_dir"
cd "$test_dir"
# Test 1: Version check
info "Test 1: Version check"
local version_output
if version_output=$(wkhtmltopdf --version 2>&1); then
info "Version: $version_output"
if echo "$version_output" | grep -q "with patched qt"; then
success "✅ Patched Qt detected"
else
error "❌ Patched Qt NOT detected"
return 1
fi
else
error "❌ Failed to get wkhtmltopdf version"
return 1
fi
# Test 2: Basic PDF generation
info "Test 2: Basic PDF generation"
cat > test_basic.html << 'EOF'
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Basic Test</title>
<style>
body { font-family: Arial, sans-serif; margin: 20px; }
.header { background: #f0f0f0; padding: 10px; border-radius: 5px; }
</style>
</head>
<body>
<div class="header">
<h1>wkhtmltopdf Test Document</h1>
<p>Timestamp: $(date)</p>
</div>
<p>This is a basic test of wkhtmltopdf functionality.</p>
</body>
</html>
EOF
if wkhtmltopdf test_basic.html test_basic.pdf >/dev/null 2>&1; then
if [[ -f test_basic.pdf ]] && [[ -s test_basic.pdf ]]; then
success "✅ Basic PDF generation works (size: $(ls -lh test_basic.pdf | awk '{print $5}'))"
else
error "❌ PDF file is empty or not created"
return 1
fi
else
error "❌ Error generating basic PDF"
return 1
fi
# Test 3: Print media queries (critical for patched version)
info "Test 3: Print media queries"
cat > test_media.html << 'EOF'
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Media Query Test</title>
<style>
@media screen {
.screen-only { display: block; color: blue; }
.print-only { display: none; }
}
@media print {
.screen-only { display: none; }
.print-only { display: block; color: red; background: yellow; padding: 10px; }
}
body { font-family: Arial, sans-serif; margin: 20px; }
</style>
</head>
<body>
<h1>Media Query Test</h1>
<div class="screen-only">This should NOT appear in PDF</div>
<div class="print-only">This SHOULD appear in PDF with red text and yellow background</div>
<p>Regular content that should always appear.</p>
</body>
</html>
EOF
if wkhtmltopdf --print-media-type test_media.html test_media.pdf 2>&1 | grep -q "not support using unpatched qt"; then
error "❌ Print media queries not supported (unpatched Qt)"
return 1
elif wkhtmltopdf --print-media-type test_media.html test_media.pdf >/dev/null 2>&1; then
if [[ -f test_media.pdf ]] && [[ -s test_media.pdf ]]; then
success "✅ Print media queries work"
else
error "❌ Error generating PDF with media queries"
return 1
fi
else
error "❌ Error testing print media queries"
return 1
fi
# Test 4: Landscape orientation
info "Test 4: Landscape orientation"
cat > test_landscape.html << 'EOF'
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Landscape Test</title>
<style>
@page { size: A4 landscape; margin: 20mm; }
body { font-family: Arial, sans-serif; }
.wide-table { width: 100%; border-collapse: collapse; }
.wide-table th, .wide-table td { border: 1px solid #333; padding: 8px; text-align: left; }
.wide-table th { background-color: #f2f2f2; }
</style>
</head>
<body>
<h1>Landscape Orientation Test</h1>
<table class="wide-table">
<tr>
<th>Column 1</th><th>Column 2</th><th>Column 3</th><th>Column 4</th><th>Column 5</th><th>Column 6</th>
</tr>
<tr>
<td>Data 1</td><td>Data 2</td><td>Data 3</td><td>Data 4</td><td>Data 5</td><td>Data 6</td>
</tr>
</table>
</body>
</html>
EOF
if wkhtmltopdf --orientation Landscape --print-media-type test_landscape.html test_landscape.pdf >/dev/null 2>&1; then
if [[ -f test_landscape.pdf ]] && [[ -s test_landscape.pdf ]]; then
success "✅ Landscape orientation works"
else
error "❌ Error generating landscape PDF"
return 1
fi
else
error "❌ Error testing landscape orientation"
return 1
fi
# Test 5: Headless mode (if enabled)
if [[ "$HEADLESS_MODE" == true ]]; then
info "Test 5: Headless mode"
if command -v wkhtmltopdf-headless >/dev/null 2>&1; then
if DISPLAY="" wkhtmltopdf-headless test_basic.html test_headless.pdf >/dev/null 2>&1; then
if [[ -f test_headless.pdf ]] && [[ -s test_headless.pdf ]]; then
success "✅ Headless mode works"
else
error "❌ Error in headless mode"
return 1
fi
else
error "❌ Error testing headless mode"
return 1
fi
else
warn "⚠️ Headless wrapper not found"
fi
fi
success "All tests passed successfully!"
# Show summary
info "Test summary:"
echo " 📁 Test files created in: $test_dir"
echo " 📄 PDF file sizes:"
ls -lh *.pdf 2>/dev/null | awk '{print " " $9 ": " $5}' || true
return 0
}
# =============================================================================
# Information Functions
# =============================================================================
show_help() {
cat << 'EOF'
wkhtmltopdf Patched Qt Universal Installer
USAGE:
sudo ./install_wkhtmltopdf.sh [OPTIONS]
OPTIONS:
--version VER Specify version to install (default: 0.12.6.1-3)
--force Force reinstallation without confirmation
--test Run tests only without installation
--docker Prepare for Docker environment (additional fonts)
--headless Install with headless mode support (xvfb)
--help Show this help
EXAMPLES:
# Basic installation
sudo ./install_wkhtmltopdf.sh
# Install specific version
sudo ./install_wkhtmltopdf.sh --version 0.12.6.1-2
# Force reinstallation
sudo ./install_wkhtmltopdf.sh --force
# Install for Docker with headless support
sudo ./install_wkhtmltopdf.sh --docker --headless
# Test existing installation only
sudo ./install_wkhtmltopdf.sh --test
SUPPORTED SYSTEMS:
- Ubuntu 18.04, 20.04, 22.04, 24.04
- Debian 9, 10, 11, 12
- CentOS 7, 8
- RHEL 7, 8
- Amazon Linux 2
IMPORTANT:
- Script requires root privileges (use sudo)
- Patched version provides print media query support
- Regular version from repositories does NOT support many features
EOF
}
show_system_info() {
info "System information:"
echo " 🖥️ OS: $DETECTED_OS $DETECTED_VERSION"
echo " 🏗️ Architecture: $DETECTED_ARCH"
echo " 📦 Target package: $PACKAGE_NAME"
echo " 🔗 Download URL: $PACKAGE_URL"
echo " 📝 Log file: $LOG_FILE"
echo " 🔧 Modes:"
echo " - Force install: $FORCE_INSTALL"
echo " - Docker mode: $DOCKER_MODE"
echo " - Headless mode: $HEADLESS_MODE"
echo " - Test only: $TEST_ONLY"
}
show_final_info() {
success "wkhtmltopdf installation completed!"
echo
info "Installation information:"
local wkhtmltopdf_path=$(which wkhtmltopdf)
echo " 📍 Path: $wkhtmltopdf_path"
local version_info=$(wkhtmltopdf --version 2>&1 | head -1)
echo " 📋 Version: $version_info"
if echo "$version_info" | grep -q "with patched qt"; then
echo " ✅ Patched Qt: Yes"
else
echo " ❌ Patched Qt: No"
fi
if [[ "$HEADLESS_MODE" == true ]] && command -v wkhtmltopdf-headless >/dev/null 2>&1; then
echo " 🖥️ Headless wrapper: /usr/local/bin/wkhtmltopdf-headless"
fi
echo
info "Usage examples:"
echo " # Basic PDF generation"
echo " wkhtmltopdf index.html document.pdf"
echo
echo " # With print media query support"
echo " wkhtmltopdf --print-media-type index.html document.pdf"
echo
echo " # Landscape orientation"
echo " wkhtmltopdf --orientation Landscape index.html document.pdf"
if [[ "$HEADLESS_MODE" == true ]]; then
echo
echo " # Headless mode (without X11)"
echo " wkhtmltopdf-headless index.html document.pdf"
fi
echo
info "Documentation: https://wkhtmltopdf.org/usage/wkhtmltopdf.txt"
info "Installation log saved to: $LOG_FILE"
}
# =============================================================================
# Main Logic
# =============================================================================
parse_arguments() {
while [[ $# -gt 0 ]]; do
case $1 in
--version)
WKHTMLTOPDF_VERSION="$2"
shift 2
;;
--force)
FORCE_INSTALL=true
shift
;;
--test)
TEST_ONLY=true
shift
;;
--docker)
DOCKER_MODE=true
shift
;;
--headless)
HEADLESS_MODE=true
shift
;;
--help)
show_help
exit 0
;;
*)
error "Unknown option: $1"
echo "Use --help for help"
exit 1
;;
esac
done
}
main() {
echo "wkhtmltopdf Patched Qt Universal Installer v$SCRIPT_VERSION"
echo "============================================================"
echo
# Create log file
mkdir -p "$(dirname "$LOG_FILE")"
touch "$LOG_FILE"
# Parse arguments
parse_arguments "$@"
# Check root privileges (except for test mode)
if [[ "$TEST_ONLY" == false ]]; then
check_root
fi
# Setup temporary directory
setup_temp_dir
# Detect system
detect_os
determine_package
show_system_info
echo
# If test only
if [[ "$TEST_ONLY" == true ]]; then
info "Test mode - skipping installation"
if command -v wkhtmltopdf >/dev/null 2>&1; then
run_tests
else
die "wkhtmltopdf not installed. Install it first without --test option"
fi
exit 0
fi
# Main installation procedure
info "Starting wkhtmltopdf installation with patched Qt..."
install_dependencies
remove_existing
download_package
install_package
setup_symlinks
echo
info "Running tests to verify installation..."
if run_tests; then
echo
show_final_info
else
die "Tests failed. Please check installation."
fi
}
# Run main function with all arguments
main "$@"