PDF to HTML

Turn a PDF into one clean, self-contained HTML file with headings, paragraphs and lists.

About PDF to HTML

Drop in a PDF and get back one self-contained .html file: headings, paragraphs and lists in reading order, with the stylesheet inlined and nothing loaded from the network. The preview below the controls is the actual file you will download, rendered in a sandboxed frame. Everything runs in this browser tab and the PDF is never uploaded.

How the structure is recovered

A PDF stores no headings, no paragraphs and no lines — only fragments of text, each with a font size and a position on the page. Rebuilding a document therefore means inferring structure from geometry. Fragments sharing a baseline are collected into a line and read along the writing direction, with a space inserted where a coordinate jump implies a tab stop. A short run set much smaller than the line it sits beside — a footnote marker, a superscript ordinal — is pulled back into that line and butted against the word it annotates instead of breaking away as a paragraph of its own. Runs drawn at a quarter turn, such as a rotated sidebar or stamp, are kept as their own flow and appear after the upright text rather than being spliced into it.

The size carrying the most characters across the whole document is taken as the body size, measured only over the flow that carries the document: text drawn at a quarter turn is kept in the output but never sets the scale and is never ranked as a heading, so a 14pt DRAFT stamp over 12pt body cannot become the document’s only h1. Every distinct size at least 1.12 times larger is then collected and sorted, and the three biggest become h1, h2 and h3 in order — so the largest upright type in the file is always the h1, whatever its absolute size. A fourth or fifth heading size still outranks body text and becomes an h3. A heading that ran out of room and wrapped onto a second line is joined back together, so a long chapter title is one h1 and not two. Weight and slope are not in the text layer at all — pdf.js describes every font there as plain “serif”, “sans-serif” or “monospace” — so the font objects themselves are read to find out which faces are bold and which are italic. A short line set entirely in bold at body size then becomes an h4, and a paragraph set entirely in italic is wrapped in em. A bold or italic word inside an otherwise ordinary sentence is deliberately left alone, so that one emphasised word cannot turn a paragraph into a heading. Reading the fonts costs a second look at a page, so it is only done for pages that bring in a face not seen before, and only for the first handful of those; a typeface that first appears deep inside a long document is treated as regular.

Consecutive body lines are then merged into one paragraph unless something says otherwise: a vertical gap larger than about 1.65 line heights, a left edge that shifts by more than 0.6 of a body size (at least 6 points), or a previous line that ended with sentence punctuation well short of the right margin. Lines opening with a bullet character or a numbered marker become list items wrapped in a real ul or ol; the marker itself is stripped out so the browser’s own bullet or number is the only one you see, and the document’s counter is carried across with start and value so a list beginning at 5, or resuming after a paragraph, keeps its own numbers. A line following a full-measure list item at or just inside its left edge is treated as that item wrapping, not as a new block. Hyphens left at the end of a merged line are repaired, so a word broken across two lines comes back whole.

Two things are then checked across the document as a whole. A line of text that appears verbatim at the very top or the very bottom of most pages is a running head or foot rather than a sentence, and in a document of four pages or more it is dropped; page numbers vary from page to page, so anything that is only a number counts as the same piece of furniture. Whatever is removed is named above the download button with the number of pages it appeared on, and a switch puts it back. Separately, every line is measured for the widest horizontal void inside it. Prose has none. A line stitched out of two columns sharing a baseline, or out of table cells, is mostly void — and when enough lines look like that, the page says so, because those lines have been read straight across and their words are in the wrong order.

What the output contains — and what it does not

The file is a complete HTML5 document with a charset declaration, a viewport tag, a title taken from the PDF metadata or the filename, and one inline stylesheet giving a readable measure, sensible heading sizes, a print rule and a dark-mode rule. The markup itself is plain: h1 to h4, p, ul, ol, li and the occasional em, with nothing else and no inline styling. There is no JavaScript, no external stylesheet, no web font and no image, so it opens from disk with no network at all and survives being emailed as a single attachment. Every character lifted from the PDF is escaped, so text containing angle brackets stays text.

What is not carried over: images, logos, charts, ruled lines, table borders, colours, the original typefaces, absolute positioning, page geometry, links, annotations and form fields. This is a text-structure converter, not a visual one. For the pictures use Extract Images from PDF; for grid data use PDF to Excel; for a faithful image of each page use PDF to PNG.

Where it works well, and where it does not

  • Good: single-column reports, letters, contracts, manuscripts, policy documents, manuals — anything that reads straight down the page.
  • Poor: two-column layouts. Both columns share baselines, so each output line is the left and right column stitched together. Journal papers and newsletters come out interleaved and there is no way round it from the text layer alone.
  • Poor: tables. The words survive, the cells do not.
  • Nothing at all: scans and any PDF whose text was converted to outlines. There is no text layer to read.

Tuning the result

Four switches change the output and the preview updates immediately. Detect headings can be turned off for a document set entirely in one size, where the heuristic has nothing to work with; nothing is then promoted to h1 to h4, though bullet and numbered lists still come through as lists, because they are found by their markers rather than by their size. Page sections wraps each page in its own section with an anchor and a small label, which is useful for cross-referencing back to the original and noise if you want continuous prose. Drop repeated page headers and footers is on by default and only ever touches the first or last line of a page in a file of four pages or more; turn it off if the running head is text you want to keep. The typeface choice sets a single system font stack for the whole file. Beyond that, the output is meant to be edited: run it through HTML Formatter for readable indentation, or paste it into your own template.

Lines and paragraphs rebuilt, not guessed at

Text fragments are grouped into lines by baseline, then joined into paragraphs by looking at the vertical gap, the indentation and whether the previous line stopped short of the margin. Bullet and numbered lists become real <ul> and <ol> markup: the marker is taken out of the text so it is never printed twice, the document's own numbering is kept even when a list starts at 5 or is broken by a paragraph, and an item that wraps onto a second line stays one item. A running head or page number repeated across most pages of a document of four pages or more is dropped as furniture, and the page names exactly what it removed so you can put it back.

Headings from the font size

The size carrying the most characters across the document is treated as body text. Every distinct size at least 1.12x larger is collected, and the three biggest become h1, h2 and h3 in that order, so the largest upright type in the file is always the h1. A heading too long for one line is joined back into a single heading rather than becoming two. Text drawn at a quarter turn — a DRAFT stamp, a spine, a sidebar — is kept but never ranked, so it cannot outrank the real title. Weight is read from the font itself, so a short line set entirely in bold at body size becomes an h4 and a paragraph set entirely in italic comes through in <em>. Turn the heuristic off and no line is promoted to a heading — lists are still marked up as lists.

One self-contained file

You get a single .html with the stylesheet inlined — no external CSS, no script, no web font, nothing to fetch. It opens straight from disk, prints cleanly and has a dark-mode rule built in. Conversion happens in this tab; the PDF is never uploaded.

Frequently Asked Questions

Will the HTML look like my PDF?

No, and it is not trying to. This produces a readable single-column web document: headings, paragraphs and lists in reading order, in one typeface, on a flowing page. It does not reproduce columns, page geometry, absolute positions, colours, ruled lines or the original fonts. If a pixel-faithful copy is what you need, the honest answer is that a PDF page is a fixed canvas and HTML is a flowing one, and the two only agree by rendering the page as a picture — which PDF to PNG already does better than any converter can.

What happens to images, tables and forms?

They are dropped. Only the text layer is read, so photographs, logos, charts, ruled table borders, form fields, annotations and signatures do not appear in the output at all. The words inside a table survive, but with no cells: each row is read straight across as one line, and consecutive rows are then merged into a single run-on paragraph, so a five-row table arrives as one sentence of cell values. The page warns you when it sees that pattern. Pull the pictures out separately with Extract Images from PDF, and take tabular data to PDF to Excel, which groups by vertical position into rows.

My two-column document came out interleaved.

That is the single biggest limitation and it is worth understanding rather than working around. Lines are rebuilt by grouping fragments that share a baseline, and in a two-column layout the left and right columns sit on the same baselines — so each output line is the left column and the right column stitched together. Where the columns are set a point or two out of step you get alternating lines instead, which is just as scrambled. The converter now spots this and says so above the download button: a line with a wide horizontal void through the middle of it is either two columns or a row of table cells, and when enough lines look like that the page tells you how many. It still produces the file — the warning is there so you know not to trust the reading order. This converter is built for single-column documents: reports, letters, contracts, manuscripts, manuals.

How are lists handled?

A line opening with a bullet glyph, a dash, a number, a letter or a roman numeral becomes a list item, and the marker is removed from the text so the browser draws the only one you see. The counter is carried over, so a list that starts at 5 opens with start="5" and one that resumes after a paragraph picks up where it left off; digits, letters and roman numerals each get their own list rather than being renumbered into the one above. A line that wraps onto a second line at a hanging indent stays part of its item. Two things it will not do: nesting is flattened, so an indented sub-list comes out as further items at the same level (or, if it counts differently, as its own list beside the parent rather than inside it), and a line beginning with a bare capital and a full stop — "A. " — is only read as a marker when the next line starts with "B. ", because otherwise every sentence opening with an initial would be swallowed into a list.

The heading levels are wrong.

The heuristic knows only about size and weight, so it fails in predictable ways. A document set entirely in one size gets no headings at all, because there is nothing to rank. A file with five distinct heading sizes flattens everything below the top three into h3. A cover page whose only large text is the document title will make that title the h1 and demote the real section headings by one level. Small-caps or letter-spaced headings set at body size in a regular weight are missed, because nothing distinguishes them from the text around them. Two different headings of the same size set one line apart, with the first running the full measure, are read as one wrapped heading and joined — the same rule that stops a long title becoming two h1s. A running title repeated at the top of most pages is removed as furniture instead, which is what stops a sixty-page book from getting sixty h1s; untick that option if the repeats matter to you. When the levels matter, convert with heading detection off so no line is promoted to a heading (lists still come through as lists), and mark the structure by hand — HTML Formatter will keep the result readable while you do.

Where did the paragraph breaks come from?

From geometry. A new block starts when the vertical gap to the previous line is more than about 1.65 line heights, when the left edge shifts by more than 0.6 of a body-text size (at least 6 points), or when the previous line ended with sentence punctuation well short of the right margin. One exception is made for lists: a line that follows a full-measure list item and sits at or just inside its left edge is treated as the wrapped remainder of that item rather than as a new block. Those rules cover ordinary prose well. They mis-fire on justified text with unusual leading, on poetry and addresses where every line is deliberately short, and on documents that use a blank line rather than an indent inconsistently.

I got an empty file.

The PDF has no text layer. Scans, photographs of pages and exports that converted type to outlines contain pictures of writing, not writing, so there is nothing to convert. Render the pages with PDF to PNG and run them through Image to Text, which does recognition in the browser, then paste the recognised text into Markdown Previewer or an editor to build the HTML.

Is the output safe to publish as-is?

The markup itself is safe: every character taken from the PDF is HTML-escaped, so angle brackets and ampersands in the source text cannot become tags, and the file contains no script and no external reference of any kind. What it is not is production-ready — there is no semantic structure beyond headings, paragraphs, lists and the odd em, no language metadata beyond lang="en", no image alt text because there are no images, and the page title is taken from the PDF metadata or the filename. Treat it as clean source material to edit, not as a finished page.

Is my document uploaded?

No. The file is read with the File API, parsed by pdf.js in this browser tab, and the HTML is assembled as a string in memory and saved with a local download. Nothing is transmitted and nothing is stored. Password-protected PDFs are the one case that fails outright, since an encrypted document cannot be parsed — remove the password with Unlock PDF first.