Free tools Windows power users keep installed
One-click scans. No signup required.
Some links on this page are affiliate links: if you buy through them we may earn a commission, at no extra cost to you.
With WeasyPrint, keep both your HTML and stylesheet in memory: create the document with HTML(string=html_text), create the stylesheet with CSS(string=css_text), then pass that stylesheet to write_pdf(stylesheets=[stylesheet]). The following minimal program writes a styled PDF without creating a temporary CSS file.
from weasyprint import CSS, HTML
html = HTML(string="""
<h1>Report</h1>
<p>Generated from strings.</p>
""")
stylesheet = CSS(string="""
@page { size: A4; margin: 2cm }
h1 { color: #174a7e }
""")
html.write_pdf("report.pdf", stylesheets=[stylesheet])
CSS(string=...) is the important distinction: without the named string argument, a value may be interpreted as a filename or URL rather than stylesheet text. This article uses WeasyPrint because its documented API directly supports in-memory CSS and HTML. See the WeasyPrint first-steps documentation for the current API details.
Install WeasyPrint and prepare the inputs
Install the package in the Python environment that will generate the PDF:
Quick wins for a faster PC:
Clear out junk files and repair common Windows errorsFree Scan →Fix the driver behind crashes, sound loss and screen glitchesFind Drivers →Repair Windows errors before they cause bigger problemsFix Now →python -m pip install weasyprint
WeasyPrint also depends on native libraries. Follow the installation instructions for your operating system if the import fails because a platform library is missing. Keep the HTML and CSS as Unicode strings; Python triple-quoted strings make multiline templates readable.
#1 Best Overall
The smallest working pattern
- Build an HTML string.
- Build a CSS string.
- Construct
HTML(string=...)andCSS(string=...). - Pass the CSS object in the
stylesheetslist towrite_pdf().
The list can contain one stylesheet or several. Later stylesheets participate in the normal cascade, so put overrides after base rules.
Generate a PDF entirely in memory
Omit the output argument to receive PDF bytes. This is useful for an HTTP response, object storage upload, or a database blob.
from weasyprint import CSS, HTML
html_text = """
<!doctype html>
<html>
<head><meta charset="utf-8"></head>
<body>
<h1>Quarterly report</h1>
<p class="lede">This document was rendered from Python strings.</p>
<table>
<tr><th>Item</th><th>Amount</th></tr>
<tr><td>Service</td><td>$120</td></tr>
</table>
</body>
</html>
"""
css_text = """
@page { size: A4; margin: 18mm 16mm; }
body { font-family: sans-serif; color: #20252b; }
h1 { color: #174a7e; margin-bottom: 4mm; }
.lede { color: #56616d; }
table { width: 100%; border-collapse: collapse; margin-top: 8mm; }
th, td { border: 0.2mm solid #b8c2cc; padding: 2mm; text-align: left; }
th { background: #e9f0f7; }
"""
document = HTML(string=html_text)
stylesheet = CSS(string=css_text)
pdf_bytes = document.write_pdf()
with open("report.pdf", "wb") as output:
output.write(pdf_bytes)
Use binary mode ("wb") because a PDF is a byte stream, not text. In a web framework, return pdf_bytes with a PDF content type instead of writing it to disk.
Make dynamic CSS safe and predictable
Inject values deliberately
If colors, margins, or branding are generated at runtime, validate them before interpolating them into CSS. A controlled template is easier to audit than concatenating arbitrary declarations:
from weasyprint import CSS, HTML
brand_color = "#0b6e4f" # validate against an allow-list or a color parser
margin = "18mm" # validate units and range
css_text = f"""
@page {{ size: A4; margin: {margin}; }}
h1 {{ color: {brand_color}; }}
"""
pdf = HTML(string="<h1>Invoice</h1>").write_pdf(
stylesheets=[CSS(string=css_text)]
)
Never treat untrusted user input as raw CSS. Restrict accepted values (for example, a known set of hex colors and numeric dimensions) before formatting the string.
Rank #2
Use multiple in-memory stylesheets
base = CSS(string="body { font-size: 10pt; }")
brand = CSS(string="h1 { color: #174a7e; }")
HTML(string=html_text).write_pdf("report.pdf", stylesheets=[base, brand])
This is useful when a common stylesheet is combined with a per-customer theme. Keep the order explicit so an override does not accidentally lose to an earlier rule.
Control page size, margins, and page breaks
Print-specific rules belong in the same CSS string. The @page rule controls paper dimensions and margins; page-break properties can keep headings or table rows together. WeasyPrint broadly supports CSS 2.1, but it documents exceptions and additional limitations, so verify every property your layout depends on in its API and feature reference.
What’s actually slowing this PC down?
Pick the symptom - the matching free tool is one click away.
css_text = """
@page { size: Letter; margin: 0.7in; }
@page :first { margin-top: 0.4in; }
.report-section { break-inside: avoid; }
h2 { break-after: avoid; }
"""
Browser CSS is not automatically PDF CSS. A property that works in Chrome may be unsupported or behave differently in WeasyPrint; consult the feature reference rather than assuming full HTML5/CSS coverage.
Fonts, images, and relative URLs
Register custom fonts with FontConfiguration
When CSS contains @font-face, the documented pattern creates one FontConfiguration and passes it both to CSS and to write_pdf().
from weasyprint import CSS, HTML
from weasyprint.text.fonts import FontConfiguration
font_config = FontConfiguration()
css = CSS(
string="""
@font-face {
font-family: "Report Sans";
src: url("fonts/report-sans.woff2");
}
body { font-family: "Report Sans", sans-serif; }
""",
base_url="/srv/my-report",
font_config=font_config,
)
HTML(string="<p>Custom type</p>", base_url="/srv/my-report").write_pdf(
"report.pdf",
stylesheets=[css],
font_config=font_config,
)
Use the same configuration for the document’s CSS and PDF write step. The first-steps guide shows this arrangement and explains resource loading.
Give relative resources a base location
Relative URLs such as images/logo.png and fonts/report-sans.woff2 need a meaningful base URL. Set base_url on HTML and, when the stylesheet itself references files, on CSS. WeasyPrint’s default fetcher can open local files and HTTP URLs, but its default HTTP client does not provide advanced cookie or authentication handling. Protected resources therefore require a suitable custom fetcher or an accessible, authenticated asset pipeline. These behaviors and security considerations are described in the resource-fetching documentation.
Data URLs and embedded assets
For small generated images, embedding a data URL avoids a relative-path problem. For large assets, a stable local directory or HTTP origin with an explicit base_url is easier to maintain and keeps the HTML string smaller.
Return, cache, and stream the result
Rendering is CPU- and memory-intensive compared with assembling plain text. Reuse the HTML and CSS templates, avoid repeatedly downloading identical assets, and generate only the pages needed for a request. If you need a file, pass its path directly to write_pdf("report.pdf"); if you need an API response, call write_pdf() once and send the returned bytes. Do not convert PDF bytes to a text string.
For concurrent services, place rendering in a worker process or queue and enforce request timeouts around external resources. The renderer’s output still depends on supported CSS, available fonts, and successful resource fetches; valid source markup alone does not guarantee the intended visual result. WeasyPrint’s common use cases notes these practical limits.
Troubleshoot the common failures
“My CSS text is treated as a file”
Cause: constructing CSS(css_text) positionally or using the wrong keyword. Fix: use CSS(string=css_text). Likewise, use HTML(string=html_text) for in-memory markup.
“The PDF is unstyled”
Cause: the stylesheet object was created but not supplied to write_pdf(). Fix:
HTML(string=html_text).write_pdf(
"report.pdf",
stylesheets=[CSS(string=css_text)],
)
Also check selector spelling and CSS syntax. A malformed declaration can be ignored while the rest of the document renders.
“Images or fonts are missing”
Cause: relative URLs have no base location, or the default fetcher cannot authenticate to the resource. Fix: set base_url, use absolute accessible URLs, embed small assets, or implement a custom fetcher for protected content. Inspect the path and permissions from the same environment that runs Python.
“The font does not appear”
Cause: missing FontConfiguration, an invalid font URL, or a format the environment cannot read. Fix: create one FontConfiguration, pass it to both CSS and write_pdf(), and verify the font file is reachable.
Do these 3 things before closing this tab:
1Fix the driver behind crashes, sound loss and screen glitches2Clear out junk files and repair common Windows errors3Scan for outdated or missing drivers - takes under a minute“A browser layout does not match”
Cause: WeasyPrint is not a full browser engine and documents CSS 2.1 exceptions and other feature limits. Check the feature reference, replace unsupported properties with print-oriented rules, and test the exact WeasyPrint version deployed.
Best Value
“The request hangs while rendering”
Cause: a remote image, font, or stylesheet is slow or unreachable. Fix: make dependencies local or reliably cacheable, set application-level time limits, and avoid blocking rendering on optional third-party assets.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.When another Python renderer is a better fit
| Tool | What the documented material establishes | CSS-string implication |
|---|---|---|
| xhtml2pdf | HTML-to-PDF conversion built with ReportLab, html5lib, and pypdf; documentation describes HTML5, CSS 2.1, and some CSS 3 support. | The quickstart accepts HTML through pisa.CreatePDF() and a file-like object, but the available material does not establish an identical standalone CSS(string=...) API. Check its CSS reference and API before porting this pattern. |
| fpdf2 | The manual states that neither all HTML5 nor CSS is supported and points readers to WeasyPrint and xhtml2pdf for more robust HTML-to-PDF conversion. | Choose it for its own document API, not when the requirement is applying a broad HTML stylesheet. |
| ReportLab | The user guide documents its PDF-generation library and commercial offerings. | It is a different, programmatic layout approach rather than evidence of a WeasyPrint-compatible CSS-string interface. |
Compare the CSS properties your design needs, relative-resource and authentication behavior, font handling, and input/output forms. The available documentation does not establish a performance winner among these tools.
Or skip the browser setup
If the page you need is already reachable at a URL, ScreenshotNeo can capture it as a PNG, JPEG, WebP, or PDF through one GET request. It is a website screenshot API, not a replacement for WeasyPrint’s in-memory CSS object: use it when your HTML/CSS is deployed as a page and you want a rendered capture.
Recommended Free Tools
Its cleanup steps accept cookie or consent banners and remove more than 60 known consent platforms, newsletter popups, and chat widgets before capture; each step can be disabled. Bot checks or CAPTCHAs, blank pages, timeouts, failed loads, and cache hits cost nothing, and response headers identify the page verdict and whether the request was billed. An MCP server supplies take_screenshot, get_page_info, and capture_pdf tools to Claude, Cursor, and other MCP clients.
cURL
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
Python
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)
Node.js
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
The ScreenshotNeo documentation lists options such as PDF paper size, margins, landscape mode and page ranges, plus custom CSS and JavaScript, waits, selectors, headers, cookies, device presets, caching, signed links, asynchronous jobs, webhooks, bulk capture, and a usage API. Every feature is on every plan: 1,000 shots per month are free with no card; paid plans start at $5 for 3,000 shots. Create a free ScreenshotNeo account.
Frequently Asked Questions
Can I pass a CSS filename and a CSS string together?
Yes. Create each stylesheet with the appropriate constructor (for example, a file-backed CSS object and CSS(string=dynamic_rules)) and include both objects in the stylesheets list, ordering the dynamic override where you need it in the cascade.
How do I produce PDF bytes without writing a temporary file?
Call HTML(…).write_pdf() without an output argument. The returned value is bytes that you can return from an HTTP handler or upload directly.
Windows Errors? Fix Them Before They Spread
Repair common Windows errors and clear accumulated junk for a smoother, more stable PC - no reinstall needed.Free scan · no reinstallCrashes, 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 minuteWhy does a relative URL work in HTML but fail in CSS?
The stylesheet may have a different reference location. Supply an explicit base_url when constructing CSS, and supply one for HTML when its markup also contains relative resources.
Is WeasyPrint interchangeable with a browser for every CSS property?
No. Its documentation describes broad CSS 2.1 support with exceptions and additional limits. Verify the exact properties in the API reference before committing to a browser-only layout.
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.

