Repository navigation
PDF Exports Move To PDFKit
Issue: #254 (Polish letters broken in the Knowledge PDF) Β· Modules: every module that makes a PDF Β· From: the release after 4.10.0 (changelog #2745)
This is the decision record. If anyone asks "why does FreeITSM use PDFKit and not jsPDF?" or "why not just add a font?", the answer is here.
- How the PDF code works now: PDF writer - Developer Guide
- What module, bundle and vendoring mean: JavaScript modules and vendoring explained
- The bug as reported: Issue #254 write-up
FreeITSM made every PDF with a library called jsPDF, using its built-in fonts - which contain only Western European letters. So Polish came out garbled, and text measured with the wrong widths ran past the margins. A better font would have fixed Polish, but not Hindi, Tamil, Bengali and the other languages whose letters join and change shape - FreeITSM ships nine Indian languages - because jsPDF has no shaping engine, the part that does that joining. PDFKit has one. So every PDF export now goes through one shared writer built on PDFKit with bundled Noto fonts, and every language FreeITSM ships prints correctly - including on servers with no internet.
A Knowledge article containing ZaΕΌΓ³ΕΔ gΔΕlΔ
jaΕΊΕ exported to PDF with the Polish letters missing or wrong, the text running past the right margin, and FreeITSM's logo instead of the organisation's.
-
The fonts. jsPDF's built-in fonts (Helvetica, Times, Courier - the 14 "standard" PDF fonts) only contain the WinAnsi character set: English and Western European letters.
Δ Δ Ε Ε ΕΌare not in them. This is true of every jsPDF version - FreeITSM was already on the latest (4.2.1), so upgrading could not help. - The widths. Text is wrapped by measuring it. Letters a font does not have are measured wrongly, so lines were wrapped at the wrong place and ran off the page.
-
The logo. A separate bug: the Knowledge export hard-coded
../assets/images/CompanyLogo.png- FreeITSM's own file. -
Not just Knowledge. Eleven places in FreeITSM make PDFs, all with jsPDF: Knowledge, Report Packs, form submissions, Morning Checks, the data tables (Assets), asset labels, Change Management, Network Mapper, Process Mapper and the organisation chart. Report Packs went further and deliberately replaced every non-Western character with
?.
A font is two things: a box of letter shapes, and an instruction booklet of rules - "when these two letters meet, use this joined shape", "draw this mark in front of the letter before it".
| Languages | What they need | jsPDF + a full font |
|---|---|---|
| Polish, Czech, Turkish, Russian, Ukrainian, Greek, Chinese, Japanese | Shapes only - each typed letter is one shape | β Fixed |
| Hindi, Marathi, Bengali, Tamil, Telugu, Gujarati, Kannada, Malayalam, Punjabi, Thai | Shapes and the booklet - letters join and reorder. ΰ€ + ΰ₯ + ΰ€· must print as ΰ€ΰ₯ΰ€·; in ΰ€ΰ€Ώ the ΰ€Ώ is drawn before the letter it follows |
β Right letters, wrong arrangement - like "teh wrod sepllign" |
Reading the booklet is the job of a shaping engine. Every browser has one (it is why the article looks right on screen). jsPDF does not, and no font can give it one - it is a missing ability, not a missing shape. FreeITSM ships nine Indian languages, so a font alone would have fixed Polish and left a third of FreeITSM's languages broken.
| Option | Every language? | Cost | |
|---|---|---|---|
| 1 | Bundle one full font into jsPDF | β Polish yes, Indian scripts no | Small - but the Indian scripts would stay wrong forever |
| 2 | Use the browser's own "Print β Save as PDF" | β | A print dialog instead of a download; little control over layout; no good for Report Packs or labels, which need exact page sizes |
| 3 | Put a picture of each page in the PDF (html2canvas) | β | Text cannot be searched or copied; large files |
| 4 | Make PDFs on the server with a PHP library (mPDF) | β | A Composer dependency (FreeITSM has none), big fonts on the server, every export rewritten in PHP |
| 5 | A different browser library with a shaping engine | β | Every export rewritten - three candidates, below |
Within option 5:
| Library | Verdict |
|---|---|
| pdfnative | Very new (about six months, one author), Indian scripts added weeks before. It lays pages out itself and cannot place things at exact positions, has no bold or italic, and refuses transparent PNGs - so it could not do Report Packs |
| HarfBuzz + pdf-lib | HarfBuzz is the engine inside Chrome and Firefox - the best shaping there is - but the most wiring to build |
| PDFKit | Long-established and widely used. Shaping through fontkit, exact positioning, can measure text after shaping (which Report Packs needs), built-in tables, images with transparency, bold and italic from any font |
Two ideas that were raised and turned down, with the reasons, because they will come up again:
- "Ship two or three libraries and let each install choose in System settings." Each library is used differently, so every export would be written two or three times and kept working that many times - and nobody running a helpdesk can judge "jsPDF or PDFKit?" except by finding broken text. Instead there is one writer that every export uses (guide); if PDFKit is ever replaced, only the writer changes.
- "Fork jsPDF and add shaping to it." That would mean bolting a shaping engine onto jsPDF's internals and then maintaining a PDF library alone, copying every jsPDF security fix by hand. PDFKit is "a PDF library with a shaping engine", maintained by other people.
-
PDFKit 0.20.2, vendored as one self-contained file (
assets/js/vendor/pdfkit/pdfkit.bundle.js- what that means). - Bundled Noto fonts - Google's Noto family, made to cover every script: Noto Sans, Serif and Sans Mono for Latin, Greek and Cyrillic, plus Noto Sans for Devanagari, Bengali, Gujarati, Gurmukhi, Kannada, Malayalam, Tamil, Telugu, Arabic, Hebrew and Thai. A PDF downloads only the fonts its text needs.
-
One writer,
assets/js/pdf-writer.js, that every export goes through. It looks like jsPDF to the exports, so each export changed by a few lines, not a rewrite.
Checked against the published packages before anything was copied in:
| Part | Licence |
|---|---|
| PDFKit and fontkit, linebreak, fflate, png-js, @noble/hashes and ciphers, restructure, and fontkit's helpers | MIT |
@swc/helpers (a tiny helper inside fontkit) |
Apache 2.0 |
| Noto fonts | SIL Open Font License 1.1 |
All permissive - no copyleft - so they can ship inside MIT-licensed FreeITSM. Each one's notice ships beside it (assets/js/vendor/pdfkit/LICENSE.txt, OFL.txt in each font folder). PDFKit's browser build leaves out Adobe's standard-font metrics, so Adobe's notice does not apply.
Everything is in the repository and served by the FreeITSM server: PDFKit, every font. The browser fetches nothing from the internet to make a PDF. The writer's test checks this on every run - every file loaded must come from the server's own address.
| Export | Before | Now |
|---|---|---|
| Knowledge article | Western letters only; text past the margin; FreeITSM's logo; tables as cell | cell text |
Every language; wrapped within the margins; the organisation's logo, or none; real tables with a header row |
| Report Packs | Non-Western characters replaced with ?; widths from Helvetica/Times/Courier, added up a letter at a time |
Every language; widths measured after shaping, so lines break where they print; the "unsupported" warning only for Chinese, Japanese, Korean |
| Form submissions (single, bundle, separate) | Western letters only | Every language, table questions as tables |
| Morning Checks | Western letters only | Every language |
| Data tables (Assets) | Western letters only | Every language |
| Asset labels | Text drawn as a picture by the browser (already every language) | Unchanged - text stays a picture on purpose (uploaded label fonts, preview = print); boxes, lines, QR codes and images through the writer |
| Change Management, Network Mapper, Process Mapper, organisation chart | A picture of the page in a jsPDF document | The same picture, in a writer document |
Nothing changes in how anyone exports anything.
| Chinese, Japanese, Korean | No bundled font - each is 5-20 MB - so they print as empty boxes. Report Packs' designer warns. A font could be added later (or uploaded, an idea raised during the discussion) |
| Arabic, Hebrew | Letters join correctly, but punctuation and spacing in a line that mixes right-to-left and left-to-right text can be out of order - there is no bidirectional-text pass. FreeITSM ships no right-to-left language |
| Copying Indian-script text out of a PDF | Can give the letters in a different order. What is printed is right |
| Italic in Indian scripts, italic monospace | The fonts have no italic; regular is used |
Every export was run in a real (headless) Chrome against this install, and each PDF was opened and looked at - not just checked for existing:
-
The writer on its own -
php tests/pdf-writer.php, 20 checks: the right font per script, a joined Hindi letter measured narrower than its parts, wrapping, every drawing call, a table over three pages with a coloured status column, page numbers, an image stored once, and nothing fetched from the internet. Its PDF shows Polish, Russian, Hindi, Tamil and Bengali correctly. - Knowledge - an article with Polish, Hindi and Tamil, lists, code and a table: everything correct, inside the margins, the organisation's logo.
- Report Packs - a copy of a real pack with Polish, Hindi and Tamil added: headings, paragraphs, a list mixing bold and italic across scripts, KPI tiles, a chart, header and footer.
- Form submission, Morning Checks, the Assets table (41 pages), asset labels and a real change - each exported and checked.
- Organisation chart, Network Mapper, Process Mapper - their pictures could not be produced in headless Chrome (the organisation chart's PNG export fails there too, and that code was not touched); their PDF code is the same three lines as Change Management's, which was proved.
Three gaps found by looking - at the output, and at which characters each font has - all fixed with a test: a : the Bengali font does not have printed as a box (now taken from the main font); a ΰ₯€ ending a Hindi sentence printed as a box (now kept in the Devanagari font); and Noto Sans has no β¦, Report Packs' nested list bullet (now taken from Noto Sans Mono).
FreeITSM β an open-source IT Service Management platform Β· github.com/edmozley/freeitsm Β· MIT licence
- Installation
- β° Scheduled tasks (cron jobs)
-
Architecture
- β³ π§Ή Root folder tidy (URL routing)
- β³ π Database Integrity (FKs & constraints)
- β³ π οΈ Database Verification β Developer Guide
- β³ π§© Service Layer (UI + API share code)
- β³ πͺ€ Code traps (the TRAP: comments)
- β³ π Commenting for the Engineering wiki
- β³ π οΈ Engineering wiki scanner - Developer Guide
- β³ π Service-Layer progress
- π€ Contributing (read before a pull request)
- π§ͺ Developer tests
- π PDF exports moved to PDFKit (#254)
- AI Providers
- Internationalisation (i18n)
- Timezones & Time Handling
- π Date & Time Formats
- π± Currencies
- π§ The sidebar (optional module rail)
- π§© Preference templates
- π Display order (modules, Watchtower cards) β Developer Guide
- Theming & Dark Mode
- ποΈ Recent β getting back to what you were doing
- β¨οΈ Command palette (βK)
- π Searching inside tickets
- π Attached documents
-
MobileβFriendly
- β³ π« Mobile: Tickets
- β³ π» Mobile: Assets
- β³ π Mobile: Calendar
- β³ π Mobile: Knowledge
- β³ π¦ Mobile: Service Status
- β³ πΌ Mobile: Watchtower
- β³ π§© Mobile: Problem Management
- β³ π Mobile: Change Management
- β³ πΏ Mobile: Software
- β³ β Mobile: Tasks
- β³ π Mobile: Forms
- β³ π Mobile: Contracts
- β³ π Mobile: Domains
- β³ π Mobile: People
- β³ π Mobile: Projects
- β³ ποΈ Mobile: Files
- β³ π Mobile: LMS
- β³ πΊοΈ Mobile: CMDB
- β³ πΊοΈ Mobile: Network Mapper
- β³ π§ Mobile: Process Mapper
- β³ βοΈ Mobile: Workflow
- β³ π₯οΈ Mobile: System
- β³ π Mobile: Reporting
- β³ π Mobile: System Wiki
- β³ π Mobile: Self-Service Portal
- β³ π§° Mobile: Techniques & Tricks
-
Security
- Layer 1 β which modules you can enter
- β³ π§© Module Access Control
- β³ π οΈ Module Access β Developer Guide
- β³ π Switching modules off
- β³ π οΈ Module availability β Developer Guide
- β³ π Module availability β design (#217)
- Layer 2 β what you can administer
- β³ π Roles & Permissions
- β³ π οΈ Roles β Developer Guide
- β³ π€ Why capabilities are constants
- Layer 3 β the System module
- β³ π Admin Access Control
- Hardening
- β³ π Security review response 2026-08
- β³ π‘οΈ Security hardening 2026-08
- β³ π οΈ Security hardening 2026-08 β Developer Guide
- β³ π‘οΈ Round three β plain English
- β³ π οΈ Round three β Developer Guide
- β³ π‘οΈ CSRF protection (S4) β Developer Guide
- Single Sign-On (SSO)
- ποΈ LDAP & Active Directory
- π¦ Microsoft Entra ID
- π CardDAV contact sync
- Browser Extension
- API Reference
-
π REST API β how it works
- β³ π Proposal: custom fields, inventory import and change evidence (#194)
- β³ π οΈ Custom fields, evidence + inventory - review (#195-#197)
- β³ π οΈ Long forms in a task's Work view - review (#198)
- β³ π οΈ Empty Procedures fold - review (#247)
- β³ π« REST API: Tickets
- β³ π» REST API: Assets
- β³ π΄ REST API: Problems
- β³ π REST API: Changes
- β³ π REST API: Knowledge
- β³ β REST API: Tasks
- β³ ποΈ REST API: CMDB
- β³ π REST API: Contracts
- β³ ποΈ REST API: Calendar
- β³ πΏ REST API: Software
- β³ π REST API: Domains
- β³ π¦ REST API: Service Status
- β³ βοΈ REST API: Morning Checks
- β³ π REST API: Forms
- β³ βοΈ REST API: Workflow
- β³ π·οΈ REST API: Cost centres
- β³ π§βπΌ REST API: Analysts
- β³ πΊοΈ REST API: Network Mapper
- β³ π§ Using the API docs page
- β³ π OpenAPI specification
- β³ β OpenAPI: kept correct
- β³ π οΈ Maintaining the catalogue
- Watchtower
-
Tickets
- β³ π οΈ Inbox.js map β Developer Guide
- β³ π οΈ Floating windows & the file viewer β Developer Guide
- β³ π οΈ Tickets settings β Developer Guide
- β³ π Simpler ticket and task forms - design (#218)
- β³ ποΈ The ticket workspace
- β³ π The ticket table
- β³ π Rota copy and paste β Developer Deep Dive
- β³ β Checklists & SOPs
- β³ βοΈ Mandatory fields
- β³ π§Ύ Custom ticket fields
- β³ π·οΈ Ticket categories
- β³ π§Ύ A type for every new ticket
- β³ π₯ Assigning tickets to a team, and escalation
- β³ π’ One board across every company
- β³ π Ticket dashboard β Developer Guide
- β³ Mailbox Authentication
- β³ π€ Email send log
- β³ π Attachment size limits and large emails
-
β³ Basic IMAP mailboxes
- β³ π Basic IMAP without the PHP extension (#257)
- β³ π οΈ Basic IMAP client β Developer Guide
- β³ π§ͺ Basic IMAP client β Testing and verifying
- β³ π οΈ Verifying a mail folder β Developer Guide
- β³ π οΈ Mailbox providers β Developer Guide
- β³ π οΈ How Gmail mail collection works β Developer Guide
- β³ Email rendering & images
- β³ SLA Management
- β³ WhatsApp channel
-
β³
βοΈ Telegram channel - β³ β CSAT company scope and filters β Developer Guide
- β³ π₯ Microsoft Teams channel
- β³ π¨οΈ Mattermost channel
- β³ π¬ Web chat channel
- β³ π£ Slack channel
- β³ π Linking tickets
- β³ β Record previews
- β³ π Ticket notes: internal or shared
- β³ ποΈ Canned responses
- β³ βοΈ Limiting replies to particular senders
- β³ π¨ Telling the analyst a ticket is theirs
- β³ βοΈ Email signatures
- β³ π The public web address
- β³ π’ Ticket numbering
- β³ π Raising a ticket for someone else
- β³ π Merging tickets
- β³ π Confidential tickets
- β³ π₯ Portal managers
- β³ π Who has seen a ticket
- β³ π Reading long tickets
- β³ β Splitting tickets
- β³ β Selecting several tickets
- β³ ποΈ The folder pane
- β³ π½ Just my tickets, or no closed ones
- β³ π οΈ Snoozing tickets β Developer Guide
- β³ π₯ Collision detection
- β³ β±οΈ Time tracking
- β³ π Case reports - a ticket and its work as one PDF
- β³ π Scheduled work in your own calendar
- Problem Management
-
Tasks
- β³ π οΈ Task board and subtasks β Developer Guide
- β³ π§βπ€βπ§ People on a task ("Involved")
- β³ π Repeating tasks
- β³ π’ Moving a task between companies
- π Projects
-
Assets
- β³ π’ Moving an asset between companies
- β³ π Shared asset locations
- β³ π§βπΌ Assigning assets to analysts
- β³ π Warranty and lease alerts
- β³ π Leases
- β³ π Saved table views
- β³ π¨οΈ Recording anything, and importing it
- β³ π§© Asset type layouts
- β³ π Inventory freshness
- β³ π Linked configuration item
- β³ π Connected assets
- β³ π Asset review
-
β³ π·οΈ QR asset labels
- β³ π οΈ QR asset labels β Developer Guide
- β³ π Asset label designer - design (#229)
- β³ π· Asset scanning β Developer Guide
- β³ π οΈ Asset tag numbering β Developer Guide
- β³ π οΈ Asset reconciliation β Developer Guide
- β³ π οΈ Reconciliation, tags + labels - review (#164)
- β³ π Who holds what, and handover documents
- β³ π₯οΈ The inventory agent (PowerShell)
- β³ ποΈ Proxmox VE servers
- β³ βοΈ VMware Cloud Director servers
- β³ πͺ Hyper-V and Failover Clusters
- β³ π Linking equipment to tickets
- β³ βοΈ Follow-up tasks on a ticket
- Knowledge
- Change Management
- Calendar
- Morning Checks
- Reporting
- Software
-
Forms
- β³ π¨ The form designer β Developer Guide
- β³ π Layout & the grid β Developer Guide
- β³ ποΈ Collections β grouping submissions
- β³ π Submissions as PDFs
- β³ β‘ What happens next β a form's own actions
- β³ π οΈ Sections & conditional logic β Developer Guide
- β³ π οΈ Lookup fields β Developer Guide
- β³ π‘οΈ Catalogue request approvals
- People
- Starters & leavers
- Domains
- ποΈ Files
- Contracts
- Service Status
- π Notifications
- π¨ War Room
- Self-Service Portal
- LMS
- Process Mapper
- CMDB
- Network Mapper
- Workflows
- Issue trackers (Jira, Azure DevOps)
- π‘ Zabbix (monitoring)
- π¬ Microsoft Teams alerts
- π 3CX (phone system)
-
System
- β³ π οΈ Preferences β Developer Guide
- β³ π Finding people in the Analysts list
- β³ πͺ When an analyst leaves
- β³ π οΈ System β Branding β Developer Guide
- β³ π©Ί Page health (D019) β Developer Guide
- β³ β Feature Bingo
- β³ π·οΈ Cost centres
- β³ π· Photo Album
- β³ Demo Data
- β³ π οΈ Demo Data β Developer Guide
-
Overview
- β³ π Progress tracker
- β³ Concepts & vocabulary
- β³ Email routing & mailboxes
- β³ Settings: global vs per-company
- β³ Users & self-service
- β³ Staff cross-company access
- β³ One company supporting another
- β³ π’ One board across every company
- β³ Worked examples
- β³ Pitfalls & gotchas
- β³ Scope: what it's for
- β³ π οΈ Developer Guide (make a module multi-company)
- β³ ποΈ Case study: CMDB (a linked graph)
- β³ π§ͺ Test harness (prove it's isolated)
- What this is
-
π Bugs resolved
- β³ π #254 PDFs could not print Polish
- β³ π #250 Clicking a folder left the open article on screen
- β³ π« #260 The dashboard cog did nothing
- β³ π« #248 A department rule never matched on four routes
- β³ π A directory account could be matched to someone else
- β³ π« #244 Reply went to the assigned analyst
- β³ π A lookup answer could name any record
- β³ π« #227 A mailbox could not be deleted
- β³ π« #226 A picture on a note could not be closed
- β³ π» #222 The Assets page never loaded in 4.3.0
- β³ βοΈ #214 A "contains" condition did not match capitals
- β³ βοΈ #216 A workflow email from a chosen mailbox always failed
- β³ π³ The Docker image was missing PHP extensions
- β³ π A failed DNS lookup read as a missing record
- β³ π A link could change data
- β³ π Access outlived its removal
- β³ π Knowledge articles could run scripts
- β³ π Knowledge writes ignored permissions
- β³ π Company boundaries (Oct 2026)
- β³ π Smaller gaps (Oct 2026)
- β³ π Escaped text could leave its attribute
- β³ π Task descriptions were not cleaned
- β³ π Task documents ignored company scope
- β³ π Repeating tasks ignored company scope
- β³ π¬ #183 Mailbox checks overlapped
- β³ π #188 Transfer lock shown off when unknown
- β³ β #184 Subtask names and Overdue
- β³ π§© #186 Teams: the buttons did nothing
- β³ π Text in a change could run script
- β³ π AI settings changed without the AI permission
- β³ πͺ #178 Deleting an analyst failed
- β³ π« A team with no departments showed 0 tickets
- β³ π Projects: adding a phase did nothing on Save
- β³ π Leavers could still sign in
- β³ πΌοΈ Logo and courses broke on Apache with PHP-FPM
- β³ π’ Chat tickets ignored your ticket numbering
- β³ π Dates shown as a dash, or in server time
- β³ π Assets β Users showed people from other companies
- β³ π Restricted analysts could read other modules' data
- β³ πΌοΈ Replies with a picture in the thread failed to send
- β³ π Reply attachments never reached the customer
- β³ π οΈ Outbound email attachments β Developer Guide
- β³ π A global SSO provider was missing from the portal
- β³ π Behind a proxy, the SSO redirect said http
- β³ βοΈ The portal tagline moved when you saved it
- β³ π¨ The portal settings screen forgot what you saved
- β³ π‘οΈ The approvals inbox said "Error" and nothing else
- β³ π A table's answers were missing from the PDF
- β³ β A single-select column let you tick every option
- β³ π The portal ignored a form's field widths
- β³ π The tasks board stopped taking clicks
- β³ ποΈ #121 The index list is out of date after upgrading
- β³ π #133 The calendar subscription was empty
- β³ π #131 Tasks always reopened on the board
- β³ π₯ #129 Every page returned HTTP 500 after upgrading
- β³ π³ #127 A PHP warning above the System page
- β³ π #126 Notes stamped with the server's clock
- β³ π Storing every date in UTC
- β³ πͺ The portal was down for everyone signed in
- β³ βοΈ #120 Workflow notes could never be written
- β³ βοΈ #123 Three errors when running Database Verification
- β³ π #122 The description box was a stub in the corner
- β³ π£ Demo data deleted real accounts
- β³ π #117 Sign-in redirected to the wrong address
- β³ π¨ #108 The priority dot was invisible
- β³ β±οΈ #116 Time logged from the right-click menu
- β³ π #114 API keys refused by our own guard
- β³ ποΈ #110 Assigning a task told nobody
- β³ πͺ #107 Signed out while still working
- β³ π #103 "Share with Requester" reached nobody
- β³ π #102 Search found nothing for hyphens
- β³ πͺ #101 Source code editor opened behind
- β³ βοΈ #88 Subtasks could not be ticked off
- β³ π» #84 Asset deep link selected nothing
- β³ π« #79 A new ticket arrived with no status
- β³ π§ #79 A ticket from email did not say so
- β³ π #78 Bell opened to nothing
- β³ π¬ #77 Mail only collected from Inbox
- β³ π #74 The default password could not be changed
- β³ π¦ #70 Renaming an impact level
- β³ π€ #67 App-only mailboxes could not send
- β³ π #45 Verify only ever worked for Microsoft
- β³ π #45 IMAP reported as not authenticated
- β³ βοΈ An email template stopped escaping itself
- β³ π The portal dashboard showed the wrong time
- β³ π’ The folder said 99 and the list showed 96