> ## Documentation Index
> Fetch the complete documentation index at: https://theteam-13fc7f49.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Wkhtmltopdf removal — every PDF on dompdf

> Why and how the backend dropped wkhtmltopdf (laravel-snappy) on 24/10/2026: the admin dashboard PDF now renders with dompdf, a pure-PHP composer package, and how to uninstall the wkhtmltopdf binaries from the servers and developer machines.

Written on 24/10/2026 from the code on `chat+ai` of `LMS_Backend`. Keywords for search: wkhtmltopdf,
wkhtmltoimage, wkhtmltox, Snappy, `WKHTML_PDF_BINARY`, `WKHTML_IMG_BINARY`, dompdf.

**Status: CODE DONE ✅ (24/10/2026), SERVER CLEAN-UP PENDING ⚠️.** Every PDF the backend makes is
rendered by **dompdf** (`barryvdh/laravel-dompdf`), a pure-PHP package that `composer install`
brings in. Nothing has to be installed on a server for PDFs anymore. The wkhtmltopdf binaries can be
uninstalled from each server once the new backend runs there (see [Server clean-up](#server-clean-up)).

## At a glance

| | Before | After |
| :- | :- | :- |
| PDF engines | dompdf **and** Snappy (wkhtmltopdf) | dompdf only |
| Server install | wkhtmltopdf 0.12.6 binaries in `/usr/local/bin`, installed by hand on every server | Nothing: `composer install` is enough |
| `.env` | `WKHTML_PDF_BINARY`, `WKHTML_IMG_BINARY` | Nothing |
| Admin dashboard export | `GET /api/dashboard/admin/pdf`, rendered by Snappy | Same endpoint, parameters and file name, rendered by dompdf |
| Web and mobile apps | — | No change, no rebuild |

## Why

* **wkhtmltopdf is a system program**, installed by hand outside composer on every server and developer machine. A new server without it answered the admin export with a `500`.
* **The project is archived** upstream (since January 2023): no new releases and no security fixes, and it is built on an old Qt WebKit engine.
* **Only one PDF used it**, the admin dashboard export. Invoices, order PDFs and the instructor dashboard export were already rendered by dompdf, so two PDF engines were kept for one file.

## What changed in LMS\_Backend

| File | Change |
| :- | :- |
| `app/Http/Controllers/DashboardsController.php` | `downloadMinimalDashboardPdf()` renders `admin_dashboard_pdf` with `Barryvdh\DomPDF\Facade\Pdf` on A4, with `isRemoteEnabled` (the company logo is an R2 URL), the same way `OrdersController` renders invoices |
| `resources/views/admin_dashboard_pdf.blade.php` | The three flexbox rules became tables (dompdf has no flexbox), the footer moved into the bottom page margin, and the role-breakdown subtitle went from weight `600` to `bold` |
| `composer.json`, `composer.lock` | `barryvdh/laravel-snappy` removed, with its dependency `knplabs/knp-snappy` |
| `config/snappy.php` | Deleted |
| `config/app.php` | The `PDF` → `SnappyPdf` alias removed. `PDF` and `Pdf` are now dompdf's own auto-discovered facades |
| `.env.example` | `WKHTML_PDF_BINARY` / `WKHTML_IMG_BINARY` removed |
| `tests/Feature/AdminDashboardPdfTest.php` | New. The export answers `200` with `application/pdf`, an `admin_dashboard_*.pdf` attachment starting with `%PDF-`, and Snappy and its config are gone |
| `README.md`, `CLAUDE.md`, `clean_up.md` | The wkhtmltopdf setup section became "PDF generation (dompdf only)". Mirror: [Backend Clean-Up](https://theteam-13fc7f49.mintlify.site/clean-up/backend.md) D.3 / D.4 |

**Verified:** the full backend suite passes (166 tests), and a 5-page sample export (12 months and 52
weeks) was checked page by page: header with logo, company name and export date side by side, table
headers repeated on each new page, and the footer on every page without covering any row.

## The admin dashboard export

The request and the response did not change, so the apps need nothing.

```text theme={null}
GET {{base_url}}/api/dashboard/admin/pdf?company_id=12&year=2026
```

| Name | Type | Required | Description |
| :- | :- | :- | :- |
| `company_id` | integer | ✅ | Must exist in `companies` |
| `year` | integer | — | 2000–2100. Defaults to the current year |

* **200**: `Content-Type: application/pdf` and `Content-Disposition: attachment; filename=admin_dashboard_20261024_113000.pdf`. The A4 report holds the executive summary, the revenue summary (TND), users by role (super admin excluded), users per year, and the monthly and weekly performance tables.
* **500**: `{ "message": "…" }`. Validation errors come back this way too, because the controller's `validate()` sits inside its `try`.

## Writing PDF views for dompdf

Keep these in mind for any new or changed PDF view (`resources/views/*pdf*.blade.php`):

> [accordion] No flexbox or grid: use tables
>
> dompdf supports CSS 2.1 plus a few CSS 3 properties. `display: flex` and `display: grid` are
> ignored, so the children stack. Put side-by-side blocks in a `<table>` (or `display: table` /
> `table-cell`). `border-radius`, `:nth-child` and `@page` margins work.

> [accordion] Fixed header or footer: first in the body, in the page margin
>
> A `position: fixed` box is drawn on the page where it appears and on every page after it, so put it
> **first** in `<body>`. Its offsets are measured from the page content area (inside the `@page`
> margins): a **negative** `bottom` moves it into the bottom margin. The admin report uses a 20 mm
> bottom margin and `bottom: -14mm; height: 10mm`, so the footer never covers a table row.

> [accordion] Fonts: DejaVu Sans, regular and bold only
>
> `DejaVu Sans` ships with dompdf and covers the `•` and `—` characters. It has a regular and a bold
> weight only: `font-weight: 600` finds no match and falls back to the default **serif** font. Use
> `bold` (or `700`).

> [accordion] Images: remote URLs need isRemoteEnabled
>
> Company logos are R2 URLs. dompdf only downloads them with
> `->setOption(['isRemoteEnabled' => true])` (every PDF of the backend sets it). A local image must
> sit inside the project folder (dompdf's `chroot`).

> [accordion] Long tables
>
> `<thead>` rows repeat on each new page, and a table breaks between rows, never inside one.

***

## Server clean-up

**Order matters.** Uninstall wkhtmltopdf from a server only **after** the new backend runs there: the
old code still calls the binary, and the admin export would fail with a `500`. Do it once per server
(DEV, TEST, PROD).

**1. Deploy the new backend**

Over SSH, as the site's system user:

```bash theme={null}
cd /var/www/vhosts/<domain>/httpdocs          # the folder that holds `artisan`
PHP=/opt/plesk/php/8.2/bin/php                 # Plesk's PHP 8.2, not the system `php`
git pull
composer install --no-dev --optimize-autoloader   # also deletes laravel-snappy and knp-snappy from vendor/
```

If the server deploys through Plesk's Git integration, pull there and run **PHP Composer → Install**.

**2. Delete the two WKHTML lines from .env**

Remove `WKHTML_PDF_BINARY` and `WKHTML_IMG_BINARY` (over SSH, or Plesk → **Files** → `.env` →
**Edit in Text Editor**). Nothing reads them anymore, so a forgotten line does no harm.

**3. Rebuild the caches**

```bash theme={null}
$PHP artisan optimize:clear
$PHP artisan optimize
```

Then Plesk → the domain → **PHP Settings** → **Apply**, which restarts PHP-FPM so OPcache drops the
old files.

**4. Test the export**

Sign in as an admin, open the dashboard and download the PDF report. It opens with the company logo,
the tables, and the footer on every page.

**5. Uninstall wkhtmltopdf**

Needs root. Follow [Uninstall the binaries](#uninstall-the-binaries) below.

### Uninstall the binaries

First find out how it was installed. The old `.env` pointed at `/usr/local/bin`, which is where the
official `wkhtmltox` package from wkhtmltopdf.org puts it.

```bash theme={null}
command -v wkhtmltopdf wkhtmltoimage     # e.g. /usr/local/bin/wkhtmltopdf
wkhtmltopdf --version                    # wkhtmltopdf 0.12.6 (with patched qt)
dpkg -S "$(command -v wkhtmltopdf)"      # Debian / Ubuntu: "wkhtmltox: /usr/local/bin/wkhtmltopdf" = the .deb package
rpm -qf "$(command -v wkhtmltopdf)"      # AlmaLinux / Rocky / CentOS: the owning package, if any
```

Then remove it the matching way:

> [tab] wkhtmltox package (.deb / .rpm)
>
> ```bash theme={null}
> sudo apt purge wkhtmltox      # Debian / Ubuntu
> sudo dnf remove wkhtmltox     # AlmaLinux / Rocky / CentOS
> ```

> [tab] apt package wkhtmltopdf
>
> Installed with `apt install wkhtmltopdf` (the binaries are then in `/usr/bin`):
>
> ```bash theme={null}
> sudo apt purge wkhtmltopdf
> ```

> [tab] Copied by hand
>
> No package owns the files (`dpkg -S` / `rpm -qf` find nothing):
>
> ```bash theme={null}
> sudo rm -f /usr/local/bin/wkhtmltopdf /usr/local/bin/wkhtmltoimage
> sudo rm -rf /usr/local/include/wkhtmltox
> sudo rm -f /usr/local/lib/libwkhtmltox.so*
> sudo ldconfig
> ```

On Debian / Ubuntu, `sudo apt autoremove --purge` then removes the libraries the package pulled in
(X11 libraries, `xfonts-*`) when nothing else needs them. **Read the list before you answer `y`**,
and answer `n` if it names something you don't recognise.

Check that it is gone:

```bash theme={null}
command -v wkhtmltopdf wkhtmltoimage || echo "wkhtmltopdf removed"
```

### Developer machines

| System | How to remove it |
| :- | :- |
| macOS, `.pkg` from wkhtmltopdf.org | `sudo /usr/local/bin/uninstall-wkhtmltox` (removes the binaries, headers, libraries and man pages, then itself) |
| macOS, Homebrew | `brew uninstall --cask wkhtmltopdf` |
| Windows | Settings → **Apps** → **wkhtmltox** → **Uninstall** |
| Linux | Same as the servers above |

Delete the two `WKHTML_*` lines from the local `.env` as well, then run `composer install` and
`php artisan optimize:clear`.

## Rolling back

Not expected to be needed. If it ever is: revert the backend commit, run `composer install`,
reinstall wkhtmltopdf 0.12.6 in `/usr/local/bin`, and put the two `WKHTML_*` lines back in `.env`.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.