If the same document wraps differently on macOS and Ubuntu, first identify which “PDFKit” you are using. Node.js PDFKit lays out PDF text directly; Python’s pdfkit package invokes the external wkhtmltopdf renderer; Apple’s PDFKit is a separate framework. Their fixes are not interchangeable. Reproduce a short input with explicit fonts, dimensions, versions and options, then compare the effective layout inputs before changing CSS or page-break rules.
Identify the PDFKit implementation before changing anything
The package name alone is not enough. Confirm the import, executable and rendering path in your project.
| Stack | What performs layout | First variables to compare |
|---|---|---|
| Node.js PDFKit | PDFKit’s JavaScript text and PDF engine | Font file and face, font size, text-box width, margins, page size and text options |
Python pdfkit |
HTML/CSS rendered by a selected wkhtmltopdf binary |
Binary path and version, HTML, CSS, installed fonts, command options and page settings |
| Apple PDFKit | Apple’s framework (a different project) | Framework version, document construction code and text/font attributes |
Apple documents a PDFKit framework separately at PDFLineStyle. Ruby’s PDFKit is also a wkhtmltopdf wrapper, as described in its project documentation. Do not apply Node API calls to a wrapper, or wkhtmltopdf switches to Node PDFKit.
Build a controlled macOS-versus-Ubuntu reproduction
- Create a minimal document containing one paragraph that currently differs. Use the same source text on both hosts.
- Set page size, margins, font size and text width explicitly in application code (or CSS and renderer options for
wkhtmltopdf). - Record operating-system versions, package versions, the exact renderer path, command-line options and selected font file.
- Save both PDFs and compare the first differing line. Determine whether the difference is inside a text box (wrapping) or at a page boundary (pagination).
- Change one variable at a time. A controlled comparison is a diagnostic method based on the documented layout controls; it is not proof that any single variable is your cause until the result reproduces.
Keep the input text, encoding and whitespace identical. A non-breaking space, different newline, missing glyph or substituted character can move a word even when the apparent paragraph is the same.
#1 Best Overall
Fixing Node.js PDFKit line wrapping
Node PDFKit says, “PDFKit includes support for line wrapping out of the box!” Its text API wraps within page margins by default and accepts an explicit width and other layout options (text documentation).
Make the font file and face explicit
Do not rely on a host-installed font with a familiar name such as Helvetica. PDFKit supports TrueType (.ttf), OpenType (.otf), WOFF, WOFF2, TrueType Collection (.ttc) and Datafork TrueType (.dfont) files. Bundle one known file with your application and select the intended face when a collection contains several faces. Register it once when you reuse it, as shown in the getting-started guide.
const PDFDocument = require('pdfkit');
const fs = require('fs');
const doc = new PDFDocument({
size: 'A4',
margins: { top: 72, bottom: 72, left: 72, right: 72 }
});
doc.pipe(fs.createWriteStream('mac-ubuntu-check.pdf'));
doc.registerFont('AppText', '/app/fonts/DejaVuSans.ttf');
doc.font('AppText').fontSize(12);
doc.text('Use exactly the same text, font file, size and width on both systems.', {
width: 451,
align: 'left',
lineGap: 0,
paragraphGap: 0
});
doc.end();
Use an absolute or otherwise deterministic font path in both environments. Verify the file checksum if the PDFs still differ. The standard AFM fonts supplied by PDFKit are metrics-based and cannot be embedded as font data; loading a real TrueType or OpenType file is the safer choice when identical embedded font data matters (project guide).
Compare effective geometry, not just source constants
- Use the same page size and all four margins.
- Pass an explicit
width; otherwise the available width is derived from page margins. - Match font size, character spacing, line gap, paragraph gap, alignment and indentation.
- Check that the same font face is selected from a collection and that every character exists in it.
- Ensure no platform-specific code changes text, locale, hyphenation or Unicode normalization.
A one-point difference in usable width can move a word and then change every following line. Compare the generated PDF’s embedded font and page boxes when possible, rather than assuming the JavaScript source produced the same effective values.
Crashes, No Sound, or Screen Glitches?
Random freezes, missing sound and display glitches usually trace back to one bad driver. Find and replace yours safely.Free scan · under a minutePC Slower Than It Used to Be?
A free scan shows the junk files, broken settings and background clutter dragging Windows down - then fixes them in one click.Free scan · Windows 10 & 11Fixing Python pdfkit and wkhtmltopdf
Python pdfkit is a wrapper around the wkhtmltopdf command-line utility. The wrapper can receive renderer options and a specific binary path (python-pdfkit documentation).
Pin and report the actual renderer
On each host, record the executable that the process really invokes, not merely the Python package version:
wkhtmltopdf --version
which wkhtmltopdf
Then configure that path explicitly:
import pdfkit
config = pdfkit.configuration(wkhtmltopdf='/opt/wkhtmltopdf/bin/wkhtmltopdf')
options = {
'page-size': 'A4',
'margin-top': '18mm',
'margin-right': '18mm',
'margin-bottom': '18mm',
'margin-left': '18mm',
'encoding': 'UTF-8',
'disable-smart-shrinking': ''
}
pdfkit.from_file('input.html', 'output.pdf', options=options, configuration=config)
Use the same binary build, HTML, CSS, viewport assumptions, options and font files on both systems. Ubuntu package builds vary by release; for example, the Ubuntu Focal manpage identifies package version 0.12.5-1ubuntu0.1 on that distribution’s page (Focal manpage). That label is distribution-specific, not a universal Ubuntu version.
Make HTML and fonts deterministic
- Declare UTF-8 in the document and pass the renderer’s encoding option.
- Use a bundled webfont with a stable
@font-faceURL or local file, and wait for it before rendering if your setup requires it. - Set the CSS font family, weight, size, line-height, width and margins explicitly.
- Remove responsive rules that depend on an unspecified viewport.
- Compare generated HTML after templating; platform-specific paths or locale formatting can alter its length.
@font-face {
font-family: 'AppText';
src: url('file:///app/fonts/DejaVuSans.ttf');
font-weight: 400;
}
body { margin: 0; font-family: 'AppText'; font-size: 12pt; line-height: 1.2; }
.text { width: 160mm; }
Do not confuse wrapping with page breaking
If a word moves to another line inside the same text block, investigate font metrics and usable width. If an element moves to another page or is split across pages, investigate pagination rules instead. The Ubuntu wkhtmltopdf manual says, “The current page breaking algorithm of WebKit leaves much to be desired.” It notes that CSS page-break-inside can help when using a patched-Qt build (Trusty manpage). That guidance concerns page segmentation, not a general within-line wrapping fix.
What’s actually slowing this PC down?
Pick the symptom - the matching free tool is one click away.
.invoice-row {
page-break-inside: avoid;
}
Apply this only when the symptom is a row or block being divided at a page boundary. It cannot make two different fonts calculate identical word widths.
Common symptoms, causes and fixes
| Symptom | Likely variable to inspect | Action |
|---|---|---|
| Every line differs from the first paragraph | Font file, face, size or width | Bundle and select the same font; set explicit geometry and compare checksums. |
| Only one host differs after a dependency update | Renderer or package build | Print versions and executable paths; pin the same build. |
| Differences appear only near page bottoms | Margins, page size or pagination | Compare page boxes and CSS page-break rules; do not alter wrapping options first. |
| Some characters change spacing | Missing glyph or fallback font | Check the chosen face contains those characters and embed a font that does. |
Python reports that wkhtmltopdf is missing |
Wrong path or permissions | Install a supported binary, verify which and pass pdfkit.configuration() the absolute path. |
| Output is blank or truncated | Load failure, blocked local resources or timeout | Inspect renderer stderr, test the HTML directly, and verify file and network access before comparing layout. |
Reliability and reproducibility checklist
- Keep a lockfile for Node or Python dependencies.
- Store the renderer binary (or a reproducible container image) and record its version.
- Version-control font files and verify their hashes in CI.
- Use fixed page dimensions, margins, viewport and locale.
- Generate a small golden PDF in continuous integration and review line positions after upgrades.
- Compare text extraction and embedded-font metadata as well as screenshots; a visual difference can be caused by pagination, rasterization or missing glyphs.
- Keep a minimal failing input so a renderer upgrade can be tested without application noise.
There is no documented universal macOS-versus-Ubuntu root cause from the title alone. A defensible diagnosis requires the project, versions, font files, source input and both outputs.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Or skip the browser setup
If your real task is obtaining a clean screenshot or PDF of a web page rather than debugging local PDF text layout, ScreenshotNeo provides a single HTTP request and an MCP server for AI clients. It accepts cookie and consent banners before capture and removes more than 60 known consent platforms, newsletter popups and chat widgets; bot checks, blank pages, timeouts, failed loads and cache hits are not billed, and response headers identify the page verdict and billing status.
For a direct capture, see the ScreenshotNeo API documentation:
The Tool Desk
Outbyte PC Repair FREEClear out junk files and repair common Windows errorsFree Scan →Outbyte Driver Updater FREEScan for outdated or missing drivers - takes under a minuteDriver Scan →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 also offers take_screenshot, get_page_info and capture_pdf MCP tools for Claude, Cursor and other MCP clients. Its free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000. Create a free ScreenshotNeo account.
Rank #4
Frequently Asked Questions
Can I fix this by installing the same system font name on both machines?
It may help, but it does not prove the same font file, face or metrics are being used. Bundle the font and select it explicitly, then verify the files match.
Should I use CSS page-break-inside: avoid for a word that wraps early?
No. That property addresses blocks split between pages in wkhtmltopdf. A word wrapping within a line requires checking font metrics and available width.
Does matching the Python pdfkit version guarantee identical PDFs?
No. The wrapper delegates to an external wkhtmltopdf executable, so its path, build, HTML, CSS, fonts and options also need to match.
Quick wins for a faster PC:
Fix the driver behind crashes, sound loss and screen glitchesFind Drivers →Clear out junk files and repair common Windows errorsFree Scan →Quick Recap
Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.

