HTML to PDF Converter

Paste HTML, drop an .html file, or open a saved .mhtml page, and get a PDF back at your own page size, orientation and margins. Everything runs in this tab your markup is never uploaded, there is no sign-up, no watermark and no daily limit.

Your HTML

Inline CSS and <style> blocks are applied. Scripts are removed, since a PDF cannot run them.

Live preview nothing loaded yet

Your page will appear here, at the exact size it will be printed.

A4 · Portrait · 15mm margins
STEP 1

Add your HTML

Paste the markup or drop an .html file. Nothing is sent anywhere the file is read by the browser itself.

STEP 2

Set up the page

Choose A4 or Letter, portrait or landscape, and the margins. The preview updates at true page size.

STEP 3

Check the preview

The page on the right is rendered at true size, so what you see is what the PDF will contain.

STEP 4

Download

Take the instant file, or use print-quality output when the text needs to stay selectable.

Every flavour of HTML this accepts

Whether you need to turn HTML code to PDF, an HTML file to PDF, an HTML page to PDF or a whole HTML document to PDF, it is the same two steps: paste it or drop it in, then convert. Files ending .html, .htm and .shtml all behave the same. XHTML works too, since browsers parse it natively, and modern HTML5 markup with semantic tags, flexbox and grid renders exactly as it does on screen.

A saved MHTML archive the single-file format Chrome and Edge produce when you save a page as “Webpage, Single File” is unpacked automatically. The tool pulls out the HTML part and re-inlines the images stored alongside it, which is the only reliable way to convert a saved web page without a server fetching it for you.

CSS is honoured in every form: inline style attributes, <style> blocks, and stylesheets linked over https. Print rules written with @media print apply on the print-quality route.

It is an HTML to PDF converter online free without watermark in the literal sense no stamp on the output, no account, and no cap on how many times you save an HTML page to PDF in a day.

HTML & HTML5 XHTML MHTML archive CSS & images PDF
Release to load your HTML file
1 / 3

The conversion happens in your tab, not on someone’s server

Sejda, Smallpdf, iLovePDF, FreeConvert, PDFCrowd and CloudConvert all work the same way: your file is uploaded, converted on their infrastructure, and handed back. It works, but the document sits on a machine you do not control, and it is the reason those tools cap you at two or three conversions a day.

This converter renders the page with your browser’s own engine and builds the PDF in JavaScript. There is no upload step, so there is no per-conversion cost to anyone and no limit to impose run it a hundred times on an invoice with a client’s bank details and nothing ever leaves the machine.

It also means the tool keeps working after the page has loaded, even if your connection drops.

INSTANT DOWNLOAD PRINT QUALITY

Two outputs, because “PDF” means two different things

Download PDF renders your page to an image and pages it into a PDF. What you see in the preview is exactly what lands in the file gradients, shadows, web fonts, unusual CSS and it downloads immediately, with no queue and no upload.

Print-quality PDF hands the page to your browser’s own PDF engine with the page size and margins already applied. The text stays real: selectable, searchable, copy-pasteable, and a fraction of the file size. You pick “Save as PDF” as the destination.

Most free converters give you one of these and never explain the trade-off. Use the first for design-heavy pages and the second for anything text-based that someone will search or quote from.

What actually happens when HTML becomes a PDF

HTML has no concept of a page it is a continuous canvas that reflows to whatever window it lands in. A PDF is the opposite: a fixed stack of pages. Converting means deciding where the cuts fall and how wide the content should be.

It is why two converters give different files from the same markup: a page rendered at 1280 pixels and squeezed onto A4 looks nothing like one rendered at A4’s width. This tool renders at the real content width, so preview and PDF match.

The page it produces

Every conversion comes out A4 portrait with 15mm margins, which is the shape most documents are printed, emailed and filed at. There is nothing to configure and nothing to get wrong: paste, look at the preview, download.

If you need something different, set it in your own markup rather than in a settings panel. An @page rule inside a <style> block @page{size:A4 landscape;margin:25mm} is honoured by the print-quality route, and a wrapper with a max-width or padding changes the layout on both routes. That keeps the tool down to one button while leaving full control to anyone who wants it.

Selectable text or pixel-perfect layout

Rasterising gives a perfect visual copy in which text is a picture unsearchable, unselectable, invisible to screen readers, and a bigger file. Preserving the text layer gives a searchable, accessible, far smaller file, but the layout depends on the print engine’s reading of your CSS.

Both are offered. Use the instant download when the visual is the point an invoice, a certificate, a one-pager. Use print-quality when the words are: documentation, contracts, anything that will be searched later.

Why your CSS might not survive

Images and stylesheets on relative paths stop working once the HTML leaves its folder, so inline them or use absolute URLs. Web fonts must be reachable. Anything positioned as fixed appears once rather than on every page, because fixed positioning means nothing in a paginated medium. And @media print rules are only honoured by the print-quality route.

Scripts are stripped before rendering a PDF cannot execute JavaScript. If your content is built client-side, copy the rendered DOM from developer tools and paste that.

Apps, extensions and the other converters

People look for an HTML to PDF converter app, a Chrome extension or a free download because they assume a web tool cannot work locally. That was true when every converter needed a server. An extension saves a tab; a desktop install buys offline use, which this page already has.

Among online options, iLovePDF and Smallpdf are friendliest but cap the free tier at a couple of tasks a day; Sejda allows three an hour with size limits; Adobe, AvePDF and CloudConvert all require the upload step. PDFCrowd, Winnovative and EVO sell APIs rather than one-off conversions.

Automating it in code

For a Python or server-side route, use a headless browser: Puppeteer or Playwright driving Chrome’s print engine gives the best fidelity, WeasyPrint is the usual Python choice, and wkhtmltopdf still handles simple pages.

Frequently asked questions

Drop the .html file onto the tool above or paste your markup, check the preview on the right, then press Download PDF. Everything happens in your browser, so nothing is uploaded and there is nothing to configure.

Chrome prints any page to PDF with Ctrl+P, but you get the whole browser page including navigation and ads. The Print-quality button prints just your markup, on A4 with proper margins press it, then choose “Save as PDF”. It works the same in Edge, Firefox and Safari.

With the Print-quality route, yes real, searchable, copyable text. The instant Download route renders the page as an image, guaranteeing the layout but leaving the text unselectable. Pick based on whether the words or the look matters more.

No. Rendering and the PDF are built in this tab. Sejda, Smallpdf, iLovePDF, FreeConvert, PDFCrowd and CloudConvert all send the file to their servers first, which matters if the document holds client data.

Neither. Nothing is stamped on the output and there is no cap, because there is no per-file server cost to recover. Smallpdf allows two tasks a day free; Sejda three an hour.

Not directly. Converting an HTML link to PDF means fetching that page, and a browser cannot fetch another site’s page from script so any tool offering URL conversion does it on a server. Save the page instead: Ctrl+S, “Webpage, Single File”, then drop the .mhtml here.

Because they point at files the page cannot reach. Use absolute https URLs or inline images as base64. Images on servers blocking cross-origin requests can come through blank in the instant download; print-quality usually handles them.

All three. XHTML and HTML5 render natively, and an MHTML archive what Chrome and Edge save as “Webpage, Single File” is unpacked automatically, with its embedded images re-inlined.

Not for this. Apps and extensions exist mostly because older web converters needed a server. This one runs locally and keeps working offline once loaded, with nothing to install or grant permissions to.

It can write the HTML, but conversion is a rendering job rather than a language one a model still has to hand the markup to a browser engine, exactly as this page does. Get the markup from a chatbot, paste it above, convert it here.

Your markup, your machine, your PDF.

No upload, no account, no watermark, no daily cap. Paste some HTML and see it laid out on a real page in seconds.

"; }function updatePreview() { const frame = $("#hpFrame"); const empty = $("#hpPreviewEmpty"); const has = S.html.trim().length > 0; empty.hidden = has; frame.hidden = !has; const c = contentMm(); const p = pageMm(); frame.style.width = p.w + "mm"; frame.style.height = p.h + "mm"; frame.style.padding = c.m + "mm"; const stage = $("#hpPreviewStage"); const avail = stage.clientWidth - 24; const wPx = p.w * PX_PER_MM; const fit = avail > 0 ? Math.min(1, avail / wPx) : 1; frame.style.transform = "scale(" + fit + ")"; stage.style.height = has ? (p.h * PX_PER_MM * fit + 24) + "px" : ""; if (has) frame.srcdoc = frameDoc(sanitizeHtml(S.html), false); $("#hpPageInfo").textContent = PAGES[S.opts.size].label + " · " + (S.opts.orient === "portrait" ? "Portrait" : "Landscape") + " · " + c.m + "mm margins"; }function setHtml(src, name) { if (src.length > MAX_HTML) { toast("That file is over 4 MB of markup. Trim it and try again.", "err", 4000); return; } let markup = src; let unpacked = false; if (isMhtml(src)) { const inner = parseMhtml(src); if (inner) { markup = inner; unpacked = true; } } const stripped = hadScript(markup); S.html = markup; if (name) { S.name = name.replace(/\.[^.]+$/, "") || "document"; $("#hpName").value = S.name; } $("#hpCode").value = markup; ROOT.classList.toggle("hp-ready", markup.trim().length > 0); updatePreview(); updateActions(); if (unpacked) toast("MHTML archive unpacked — embedded images kept.", "ok", 3600); if (stripped) toast("Scripts were removed — a PDF cannot run them anyway.", "info", 3600); }function updateActions() { const has = S.html.trim().length > 0; $("#hpDownload").disabled = !has; $("#hpPrint").disabled = !has; $("#hpClear").disabled = !has; const bytes = new Blob([S.html]).size; $("#hpStat").textContent = has ? humanSize(bytes) + " of markup" : "nothing loaded yet"; }function humanSize(b) { if (b < 1024) return b + " B"; if (b < 1048576) return Math.round(b / 1024) + " KB"; return (b / 1048576).toFixed(1) + " MB"; }function safeName(n) { const out = String(n || "document") .replace(/[\/\\:*?"<>|]+/g, "") .replace(/[\x00-\x1f\x7f]+/g, "") .trim() .slice(0, 100); return out || "document"; }function toast(msg, kind, ms) { const el = document.createElement("div"); el.className = "hp-toast hp-" + (kind || "info"); el.textContent = msg; $("#hpToasts").append(el); const t = setTimeout(() => { el.classList.add("hp-out"); el.addEventListener("animationend", () => el.remove(), { once: true }); }, ms || 2800); el.addEventListener("click", () => { clearTimeout(t); el.remove(); }); return el; }function loadScript(src, globalName) { if (globalName && window[globalName]) return Promise.resolve(); return new Promise((ok, fail) => { if ([...document.scripts].some(s => s.src === src)) { if (globalName && !window[globalName]) { const started = Date.now(); const poll = setInterval(() => { if (window[globalName]) { clearInterval(poll); ok(); } else if (Date.now() - started > 20000) { clearInterval(poll); fail(new Error("timed out")); } }, 60); return; } return ok(); } const s = document.createElement("script"); s.src = src; s.async = true; s.onload = ok; s.onerror = () => fail(new Error("could not load " + src)); document.head.append(s); }); }function offscreenFrame(html, widthMm) { return new Promise(resolve => { const f = document.createElement("iframe"); f.setAttribute("sandbox", "allow-same-origin"); f.style.cssText = "position:fixed;left:-10000px;top:0;border:0;width:" + (widthMm * PX_PER_MM) + "px;height:100px;"; f.onload = () => resolve(f); document.body.appendChild(f); f.srcdoc = html; setTimeout(() => resolve(f), 1200); }); }async function waitForImages(doc) { const imgs = Array.from(doc.images || []); await Promise.all(imgs.map(img => img.complete ? Promise.resolve() : new Promise(r => { img.onload = r; img.onerror = r; setTimeout(r, 4000); }))); }async function convert() { if (!S.html.trim()) return; const btn = $("#hpDownload"); const label = $(".hp-dl-label").textContent; const setLabel = t => $(".hp-dl-label").textContent = t; btn.disabled = true; let frame; try { setLabel("Loading engine…"); await Promise.all([loadScript(CDN.h2c, "html2canvas"), loadScript(CDN.jspdf, "jspdf")]);const page = pageMm(); const c = contentMm(); const zoom = Math.max(25, Math.min(200, +S.opts.scale || 100)) / 100;setLabel("Rendering…"); frame = await offscreenFrame(frameDoc(sanitizeHtml(S.html), false), c.w / zoom); const fdoc = frame.contentDocument; if (!fdoc) throw new Error("the preview frame could not be read"); await waitForImages(fdoc); const body = fdoc.body; const fullH = Math.max(body.scrollHeight, body.offsetHeight, 1); frame.style.height = fullH + "px";const raster = S.opts.quality === "high" ? 2 : 1.35; const canvas = await window.html2canvas(body, { scale: raster * zoom, backgroundColor: S.opts.background ? "#ffffff" : null, useCORS: true, logging: false, windowWidth: c.w / zoom * PX_PER_MM, width: c.w / zoom * PX_PER_MM, height: fullH });const { jsPDF } = window.jspdf; const doc = new jsPDF({ orientation: S.opts.orient === "landscape" ? "landscape" : "portrait", unit: "mm", format: [page.w, page.h], compress: true }); doc.setProperties({ title: safeName($("#hpName").value), creator: "Toolesh HTML to PDF" });const pxPerMm = canvas.width / c.w; const usableH = Math.max(20, c.h); const sliceH = Math.max(1, Math.floor(usableH * pxPerMm)); const total = Math.max(1, Math.ceil(canvas.height / sliceH));const cut = document.createElement("canvas"); const ctx = cut.getContext("2d");for (let i = 0; i < total; i++) { setLabel("Page " + (i + 1) + "/" + total); const h = Math.min(sliceH, canvas.height - i * sliceH); cut.width = canvas.width; cut.height = h; if (ctx) { if (S.opts.background) { ctx.fillStyle = "#ffffff"; ctx.fillRect(0, 0, cut.width, h); } else ctx.clearRect(0, 0, cut.width, h); ctx.drawImage(canvas, 0, i * sliceH, canvas.width, h, 0, 0, canvas.width, h); } if (i > 0) doc.addPage([page.w, page.h], S.opts.orient === "landscape" ? "landscape" : "portrait"); const type = S.opts.background ? "image/jpeg" : "image/png"; doc.addImage(cut.toDataURL(type, 0.92), S.opts.background ? "JPEG" : "PNG", c.m, c.m, c.w, h / pxPerMm); }setLabel("Saving…"); doc.save(safeName($("#hpName").value) + ".pdf"); toast("Saved " + total + (total === 1 ? " page" : " pages") + " as " + safeName($("#hpName").value) + ".pdf", "ok", 3600); } catch (e) { console.error(e); toast("Conversion failed: " + (e.message || "unknown error"), "err", 5200); } finally { if (frame) frame.remove(); btn.disabled = false; setLabel(label); } }function printQuality() { if (!S.html.trim()) return; const c = contentMm(); const page = pageMm(); const extra = ""; const html = frameDoc(sanitizeHtml(S.html) + extra, true);const f = document.createElement("iframe"); f.setAttribute("sandbox", "allow-same-origin allow-modals"); f.style.cssText = "position:fixed;right:0;bottom:0;width:0;height:0;border:0;opacity:0;"; document.body.appendChild(f); f.onload = () => { try { const w = f.contentWindow; w.focus(); w.print(); toast("Choose \u201cSave as PDF\u201d as the destination for selectable text.", "info", 6000); } catch (e) { toast("Your browser blocked the print dialog. Try the direct download instead.", "err", 5000); } setTimeout(() => f.remove(), 60000); }; f.srcdoc = html; }function init() { const code = $("#hpCode"); const pick = $("#hpFilePick");$$(".hp-tab").forEach(b => b.onclick = () => { const which = b.dataset.tab; $$(".hp-tab").forEach(x => { const on = x === b; x.classList.toggle("hp-on", on); x.setAttribute("aria-selected", on ? "true" : "false"); }); $("#hpPaneCode").hidden = which !== "code"; $("#hpPaneFile").hidden = which !== "file"; if (which === "code") code.focus(); });let typing; code.addEventListener("input", () => { clearTimeout(typing); typing = setTimeout(() => { S.html = code.value; ROOT.classList.toggle("hp-ready", S.html.trim().length > 0); updatePreview(); updateActions(); }, 260); });$("#hpBrowse").onclick = () => pick.click(); pick.onchange = e => { const f = e.target.files[0]; e.target.value = ""; if (!f) return; if (!/\.(html?|xhtml|mhtml|mht|shtml|txt)$/i.test(f.name) && !/text|html|message/.test(f.type)) { return toast("That is not an HTML file. Choose .html, .htm, .xhtml or .mhtml.", "err", 4000); } const r = new FileReader(); r.onload = () => { setHtml(String(r.result), f.name); toast("Loaded " + f.name, "ok"); }; r.onerror = () => toast("That file could not be read.", "err"); r.readAsText(f); };const drop = $("#hpDrop"); let depth = 0; window.addEventListener("dragenter", e => { if (!Array.from(e.dataTransfer.types || []).includes("Files")) return; depth++; $("#hpVeil").classList.add("hp-on"); }); window.addEventListener("dragover", e => e.preventDefault()); window.addEventListener("dragleave", () => { if (--depth <= 0) { depth = 0; $("#hpVeil").classList.remove("hp-on"); } }); window.addEventListener("drop", e => { if (!e.dataTransfer.files.length) return; e.preventDefault(); depth = 0; $("#hpVeil").classList.remove("hp-on"); const f = e.dataTransfer.files[0]; const r = new FileReader(); r.onload = () => { setHtml(String(r.result), f.name); toast("Loaded " + f.name, "ok"); }; r.readAsText(f); }); if (drop) drop.addEventListener("click", e => { if (!e.target.closest("button")) pick.click(); });$("#hpSample").onclick = () => { setHtml(SAMPLE, "quarterly-report"); $$(".hp-tab").forEach(x => x.classList.toggle("hp-on", x.dataset.tab === "code")); $("#hpPaneCode").hidden = false; $("#hpPaneFile").hidden = true; toast("Sample loaded — press Download PDF to try it", "ok", 3200); };$("#hpClear").onclick = () => { S.html = ""; S.name = "document"; code.value = ""; $("#hpName").value = "document"; ROOT.classList.remove("hp-ready"); updatePreview(); updateActions(); };$("#hpDownload").onclick = convert; $("#hpPrint").onclick = printQuality;const ctaStart = $("#hpCtaStart"); if (ctaStart) ctaStart.onclick = () => { const t = $("#hpTool"); if (t && t.scrollIntoView) t.scrollIntoView({ behavior: "smooth", block: "start" }); code.focus(); }; const ctaFaq = $("#hpCtaFaq"); if (ctaFaq) ctaFaq.onclick = () => { const t = $("#hpFaq"); if (!t) return; t.scrollIntoView({ behavior: "smooth", block: "start" }); const first = $(".hp-faq-q"); if (first && !first.parentElement.classList.contains("hp-open")) first.click(); };$$(".hp-faq-q").forEach(btn => { btn.onclick = () => { const item = btn.parentElement; const willOpen = !item.classList.contains("hp-open"); $$(".hp-faq").forEach(f => { f.classList.remove("hp-open"); const q = $(".hp-faq-q", f); if (q) q.setAttribute("aria-expanded", "false"); }); if (willOpen) { item.classList.add("hp-open"); btn.setAttribute("aria-expanded", "true"); } }; });let rt; window.addEventListener("resize", () => { clearTimeout(rt); rt = setTimeout(updatePreview, 180); });updatePreview(); updateActions(); }init();})();