Report this

What is the reason for this report?

How to install wkhtmltopdf on digitalocean App Platform?

Posted on February 22, 2022

How do install wkhtmltopdf on App Platform?

https://wkhtmltopdf.org/

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!

These answers are provided by our Community. If you find them useful, show some love by clicking the heart. If you run into issues leave a comment, or add your own answer to help others.

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.

  1. Create a file called Aptfile in to the root of your project if you don’t have it
  2. List the following packaes in Aptfile for installation
wkhtmltopdf
libpcre2-16-0
libxrender1
libxext6
libfontconfig1
libqt5core5a
libqt5gui5
libqt5webkit5
  1. Access the app console via the Digital Ocean app admin
  2. Test the wkhtmltopdf command works
  3. In your code, set the QT_QPA_PLATFORM and QT_PLUGIN_PATH env vars. Also find the actual path to wkhtmltopdf using 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

The developer cloud

Scale up as you grow — whether you're running one virtual machine or ten thousand.

Get started for free

Sign up and get $200 in credit for your first 60 days with DigitalOcean.*

*This promotional offer applies to new accounts only.