How do install wkhtmltopdf on App Platform?
I use to render HTML into PDF on Symfony application.
This textbox defaults to using Markdown to format your answer.
You can type !ref in this text area to quickly search our full set of tutorials, documentation & marketplace offerings and insert the link!
Hello here,
Any luck with this issue since? I have the same requirement.
Cheers
I got wkhtmltopdf on App Platform for my use case. Details below in case helpful for anyone.
wkhtmltopdf
libpcre2-16-0
libxrender1
libxext6
libfontconfig1
libqt5core5a
libqt5gui5
libqt5webkit5
wk_path = shutil.which("wkhtmltopdf") and then pass this into the pdfkit config config = pdfkit.configuration( wkhtmltopdf=wk_path )Bit of a code dump, but the following works for me in python / django. You’ll need to customise the path to your html template
import os
from io import BytesIO
from decimal import Decimal
from datetime import datetime
from pathlib import Path
from typing import Dict, Any, Union, Optional
import platform
import shutil
import pybars
import pdfkit
from ....models import Quote
# wkhtmltopdf etc installed on Digitalocean app by listing in Aptfile file in root of project
def format_currency(value: Decimal) -> str:
_"""Helper function to format currency values"""_
return f"£{value:,.2f}"
def get_quote_context(quote: Quote) -> Dict[str, Any]:
_"""Convert a Quote model instance into a context dict for the template"""_
# Get addresses
invoice_address = None
delivery_address = None
if quote.job.invoice_client_address:
invoice_address = quote.job.invoice_client_address.address_set.first()
if quote.job.delivery_client_address:
delivery_address = quote.job.delivery_client_address.address_set.first()
# Calculate totals
vat_rate = Decimal('0.20') # 20% VAT # Move to Var? do we use anywhere else?
total_ex_vat = sum(
item.quantity * item.unit_price
for item in quote.line_items.all()
if (item.is_optional and item.option_selected) or not item.is_optional
)
vat_total = Decimal('0') if quote.is_vat_exempt else total_ex_vat * vat_rate
total_inc_vat = total_ex_vat + vat_total
# Format line items
items = []
for item in quote.line_items.all():
if (item.is_optional and item.option_selected) or not item.is_optional:
unit_price = item.unit_price
price_ex_vat = item.quantity * unit_price
price_vat = Decimal('0') if quote.is_vat_exempt else price_ex_vat * vat_rate
price_total = price_ex_vat + price_vat
items.append({
'description': item.description,
'unit_price_float': float(unit_price),
'quantity': item.quantity,
'price_ex_vat': price_ex_vat,
'price_vat': price_vat,
'price_total_inc_vat': price_total
})
return {
'accepted_date': quote.accepted_at.strftime('%d/%m/%Y') if quote.accepted_at else 'N/A',
'client_name': f"{quote.job.client.first_name} {quote.job.client.last_name}" if quote.job.client else '',
'client_invoice_address': invoice_address.format_with_newlines() if invoice_address else '',
'client_delivery_address': delivery_address.format_with_newlines() if delivery_address else '',
'quote_number': str(quote.id),
'sent_at': quote.sent_at.strftime('%d/%m/%Y') if quote.sent_at else '',
'status': quote.status,
'accepted_at': quote.accepted_at,
'items': items,
'is_vat_exempt': quote.is_vat_exempt,
'vatRate': 0 if quote.is_vat_exempt else vat_rate * 100,
'totalExVat': total_ex_vat,
'vatTotal': vat_total,
'totalIncVat': total_inc_vat,
'signature': quote.signature_base64,
'quote_accepted_formatted': quote.accepted_at.strftime('%d %B %Y %H:%M') if quote.accepted_at else None,
'termsAndConditions': quote.quote_terms_html or '',
'document_type_is_quote': True,
'document_type_is_invoice': False
}
def generate_quote_pdf_wk(quote: Quote) -> bytes:
_"""Generate a PDF from a Quote instance using pybars3 + wkhtmltopdf"""_
# Get template path
template_path = Path(__file__).parent.parent / 'templates/quote_template_wkhtmltopdf.html'
# Read template
with open(template_path, 'r') as file:
template_content = file.read()
# Setup Handlebars
compiler = pybars.Compiler()
template = compiler.compile(template_content)
# Compile template and helpers
helpers = {
'currency': lambda this, value: pybars.strlist([f"£{float(value):,.2f}" if value else "£0.00"]),
'safeString': lambda this, value: pybars.strlist([str(value) if value else ""])
}
# Get context and render template
context = get_quote_context(quote)
html_content = template(context, helpers=helpers)
# Convert string list to string if necessary
if isinstance(html_content, pybars.strlist):
html_content = ''.join(html_content)
# Configure wkhtmltopdf options
options = {
'page-size': 'A4',
'margin-top': '0.75in',
'margin-right': '0.75in',
'margin-bottom': '0.75in',
'margin-left': '0.75in',
'encoding': 'UTF-8',
}
try:
# Debug information
print("Checking wkhtmltopdf location:")
wk_path = shutil.which("wkhtmltopdf")
print(f"wkhtmltopdf path: {wk_path}")
if platform.system() == 'Linux':
print("Running on Linux")
# Set Qt environment variables that we know work
os.environ['QT_QPA_PLATFORM'] = 'offscreen'
# Qt plugin path can be found by:
# 1. Access Digital Ocean App console via DO UI
# 2. Run: find /layers -name "platforms" | grep qt
# 3. Look for directory containing libqoffscreen.so
os.environ['QT_PLUGIN_PATH'] = '/layers/digitalocean_apt/apt/usr/lib/x86_64-linux-gnu/qt5/plugins'
config = pdfkit.configuration(
wkhtmltopdf=wk_path
)
else:
print("Running on non-Linux")
config = pdfkit.configuration()
# Generate PDF
pdf = pdfkit.from_string(
html_content,
False,
options=options,
configuration=config
)
return pdf
except OSError as e:
print(f"PDF Generation Error: {str(e)}")
print(f"Current PATH: {os.environ.get('PATH')}")
print(f"Current QT_PLUGIN_PATH: {os.environ.get('QT_PLUGIN_PATH')}")
print(f"Current QT_QPA_PLATFORM: {os.environ.get('QT_QPA_PLATFORM')}")
raise
Get paid to write technical tutorials and select a tech-focused charity to receive a matching donation.
Full documentation for every DigitalOcean product.
The Wave has everything you need to know about building a business, from raising funding to marketing your product.
Stay up to date by signing up for DigitalOcean’s Infrastructure as a Newsletter.
New accounts only. By submitting your email you agree to our Privacy Policy
Scale up as you grow — whether you're running one virtual machine or ten thousand.
Sign up and get $200 in credit for your first 60 days with DigitalOcean.*
*This promotional offer applies to new accounts only.