ScreenshotNeo

BlogHTML to image & PDF

How to Fix pdfkit Line Break Differences Between macOS and Ubuntu

Make PDFKit line breaks reproducible across macOS and Ubuntu by identifying the renderer, pinning fonts and comparing layout inputs.

By the ScreenshotNeo team1 October 20267 min read

Direct answer: first identify which “PDFKit” you use. Node PDFKit lays out text through a JavaScript API; Python pdfkit calls the external wkhtmltopdf renderer; Apple PDFKit is a separate framework. Their line-breaking inputs and fixes differ. Then run the same short document on both systems with an explicit font file, page size, margins, font size and text width (where supported), and record every package, renderer and font version.

Without the project name, versions, font files, source text and both output PDFs, no single root cause can be confirmed. Treat fonts, widths and renderer versions as variables to control and compare.

1. Identify your PDFKit implementation

Stack Rendering path Compare first
Node pdfkit JavaScript API writes PDF text directly Font file and face, font size, text-box width, margins and text options
Python pdfkit Wrapper invokes wkhtmltopdf on HTML/CSS Executable path and version, HTML/CSS, options and available fonts
Apple PDFKit Apple framework Framework APIs and platform text services

The name is ambiguous: Apple documents a distinct PDFKit framework, while the Python package is a wrapper around wkhtmltopdf. Check your dependency file and import statements before applying advice. See the Node PDFKit text documentation, python-pdfkit documentation and Apple PDFKit reference.

2. Build a controlled comparison

  1. Use identical source text, page size, margins, font size and output settings.
  2. Save the exact command, dependency lockfile, operating-system release, renderer path and renderer version for each host.
  3. Use a short fixture containing ordinary words, punctuation, accented characters and a long unbroken token.
  4. Compare the effective text width, not only the nominal page width. A margin, border or padding changes available width.
  5. Diff extracted text positions or inspect the PDFs at high zoom. Separate a word moving to another line from content moving to another page.

This procedure follows the layout controls documented by PDFKit and the renderer-selection controls documented by python-pdfkit; it is a diagnostic method, not a claim that every macOS/Ubuntu difference has one cause.

3. Node PDFKit: make every layout input explicit

Node PDFKit wraps text by default inside page margins and accepts an explicit width. It can load TrueType, OpenType, WOFF, WOFF2, TrueType Collection and Datafork TrueType font files. Use the same file and face on both machines instead of relying on a system-installed font.

const PDFDocument = require('pdfkit');
const fs = require('node:fs');

const doc = new PDFDocument({
  size: 'A4',
  margin: 54
});

doc.pipe(fs.createWriteStream('comparison.pdf'));
doc.font('/absolute/path/fonts/Inter-Regular.ttf');
doc.fontSize(12);

doc.text(
  'A controlled paragraph makes line wrapping comparable across operating systems. Include punctuation, accents and a long-unbroken-token-example.',
  54,
  80,
  {
    width: 487,
    lineGap: 0,
    align: 'left',
    continued: false
  }
);

doc.end();

Register a named font if it is used repeatedly, and specify the intended face when using a collection. PDFKit’s built-in standard fonts use AFM metrics and cannot be embedded as font data; a familiar name such as Helvetica does not prove that both hosts selected the same installed font. The PDFKit getting-started guide documents font registration and standard-font behavior.

Node checklist

  • Hash the font file shipped with the application and verify the hashes match.
  • Use the same font face, weight and style; synthetic bold or italic can change metrics.
  • Set page size and margins in code.
  • Pass a numeric text width instead of relying on defaults when exact wrapping matters.
  • Keep font size, character spacing, line gap, alignment and columns identical.
  • Record the Node and PDFKit versions from the lockfile.

4. Python pdfkit: pin wkhtmltopdf

Python pdfkit is not the renderer. It builds a command for wkhtmltopdf. A different binary, build or font environment can therefore change layout even when the Python code is unchanged.

import pdfkit

configuration = pdfkit.configuration(
    wkhtmltopdf='/absolute/path/to/wkhtmltopdf'
)
options = {
    'page-size': 'A4',
    'margin-top': '15mm',
    'margin-right': '15mm',
    'margin-bottom': '15mm',
    'margin-left': '15mm',
    'encoding': 'UTF-8',
    'print-media-type': None,
}

html = '''


A controlled paragraph makes line wrapping comparable across operating systems. Include punctuation, accents and a long-unbroken-token-example.

''' pdfkit.from_string( html, 'comparison.pdf', options=options, configuration=configuration, )

Log the resolved executable and version on both hosts:

command -v wkhtmltopdf
wkhtmltopdf --version
python -c "import pdfkit; print(pdfkit.__version__)"

The Ubuntu Focal manpage identifies package version 0.12.5-1ubuntu0.1 for that distribution page; do not assume every Ubuntu installation uses it. Compare the actual binary selected by your configuration. See the Focal wkhtmltopdf manpage.

5. Fonts are usually the first variable to eliminate

  • Ship a licensed, embeddable .ttf or .otf file with the application.
  • Reference the same path or packaged asset on macOS and Ubuntu.
  • Confirm the requested weight exists; fallback or synthetic faces have different widths.
  • Ensure the renderer can read local files and that CSS @font-face URLs resolve.
  • Check Unicode coverage. A missing glyph can trigger fallback for only part of a line.

Do not diagnose a font problem solely from the operating-system names. Verify the effective font and metrics in the generated PDF or renderer logs.

6. Keep line wrapping separate from page splitting

The Ubuntu wkhtmltopdf manual says its WebKit page-breaking algorithm “leaves much to be desired.” It also describes page-break-inside as a mitigation when using patched Qt. That guidance concerns content crossing page boundaries, not a word moving to another line inside a text block. Apply it only when pagination is the symptom. Read the Trusty wkhtmltopdf manpage for the documented limitation.

/* Pagination control for wkhtmltopdf HTML output */
.keep-together {
  page-break-inside: avoid;
}

7. Common errors and fixes

Symptom Likely cause Fix
Only some words move Different font face, fallback glyph or font file Package one font, select the exact face and verify hashes and Unicode coverage.
Every line is wider or narrower Different font size, text width, margins or page size Set each value explicitly and log the effective content width.
Python output differs while code matches Different wkhtmltopdf binary or build Pass an explicit binary path and compare --version.
HTML font is ignored Unresolved @font-face URL or local-file restriction Use a readable absolute/file URL, confirm permissions and inspect renderer warnings.
Text breaks between pages Pagination algorithm, not line wrapping Use page-break CSS where supported and test the selected wkhtmltopdf build.
Accented characters alter wrapping Encoding or missing glyph fallback Declare UTF-8, embed a font covering the characters and verify the input bytes.
Long URL or token overflows No break opportunity Insert controlled break opportunities or enable the library’s documented wrapping behavior; do not hide the issue with arbitrary width changes.

8. Performance, reliability and cost

For Node PDFKit, deterministic local fonts and fixed layout values remove repeated font discovery and reduce environment-dependent work. For Python pdfkit, renderer startup and HTML/CSS loading dominate many small jobs, so reuse a known binary and keep assets local when practical. Measure your own workload; the supplied sources publish no cross-platform benchmark.

For reliable builds, pin dependency versions, archive the font assets, record renderer paths and versions, and keep a golden PDF fixture for review after upgrades. Treat a renderer upgrade as a layout change until the fixture has been compared. There is no evidence in the supplied research for a universal macOS-versus-Ubuntu correction or performance figure.

9. Or skip the browser setup

If your real task is obtaining a clean image or PDF of a web page rather than generating a document with PDFKit, ScreenshotNeo provides a single screenshot API request. It accepts consent banners before capture and removes more than 60 known consent platforms, newsletter popups and chat widgets; each step can be disabled. Bot checks, blank pages, timeouts, failed loads and cache hits are not billed, and response headers report the page verdict and billing status. Its MCP server provides take_screenshot, get_page_info and capture_pdf for Claude, Cursor and other MCP clients.

See the ScreenshotNeo API documentation for all options, including PDF paper size, margins, landscape mode and page ranges.

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);

ScreenshotNeo includes full-page and element capture, dark mode, device presets, custom viewports, retina scale, custom CSS and JavaScript, waits, request blocking, headers, cookies, user agents, timezone and geolocation, resizing, caching, signed links, asynchronous webhooks, bulk capture and a usage API. The Free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000 shots. Create a free ScreenshotNeo account.

10. FAQ

Does changing the operating system always change PDFKit wrapping?

No. Differences occur only when effective inputs or rendering behavior differ. Confirm the implementation, fonts, dimensions and renderer before attributing the result to the OS.

Should I use page-break-inside: avoid to fix a word wrapping differently?

No. That property targets page splitting in wkhtmltopdf. It does not establish a fix for within-line wrapping.

Can I compare only the Python package versions?

No. Python pdfkit delegates to the selected wkhtmltopdf executable, so record and compare that binary and its version.

What information is needed for a definitive diagnosis?

Provide the PDFKit project, package and renderer versions, OS releases, source text or HTML, font files and faces, layout options, and both generated PDFs.

Sources