Skip to content

Concepts

Ream is written from the specifications, not as a wrapper. There is no LibreOffice, headless Office, ZetaJS/WASM, or commercial SDK underneath — the conversion is its own code. Existing implementations (the LibreOffice source, Apache POI) are read only as a reference for the edge cases, never used as a dependency.

A conversion walks the same stages a word processor would, each mapped to a section of the standard:

  1. OPC unpacking (ECMA-376 Part 2) — the .docx/.xlsx is a ZIP of XML parts. fflate unzips; fast-xml-parser parses. (ZIP and XML are the only “incidental” pieces we don’t write ourselves — they are separate standards, not the task.)
  2. OOXML parsing (Part 1) — WordprocessingML (§17), SpreadsheetML (§18), DrawingML (§20), OfficeMath (§22) become a typed, in-memory document model.
  3. Style cascade (§17.7) — docDefaults → named styles → direct formatting are resolved into the effective properties of every run, paragraph and cell.
  4. Layout — the box model is laid out into lines and pages: Knuth–Plass paragraph breaking, Liang hyphenation, table auto-layout, the spreadsheet print model (gridlines, print area, fit-to-page, repeated titles, page breaks) — or, for a screen, Excel’s window: each sheet whole, with no paper to fit.
  5. Text shaping — Unicode text becomes positioned glyphs: OpenType ligatures and kerning (GSUB/GPOS), BiDi reordering (UAX #9), Arabic cursive joining.
  6. PDF writing (ISO 32000) — content streams, the cross-reference table, Type0 + CIDFontType2 font embedding with subsetting, image XObjects. Optionally tagged PDF, a PDF/A profile, or a digital signature.

The stages above are decoupled by an intermediate representation: readers parse a format into a FlowDoc (a semantic, format-neutral document tree — no pages or coordinates yet), the layout turns it into positioned pages (the frozen page model: PageItems in a top-left, point-unit frame), and writers emit a target format from there:

bytes → reader → FlowDoc → layout → pages → writer → bytes

Ream.parse runs a reader once and hands you the FlowDoc (doc.flow); every doc.convert renders from it without re-reading the source. A workbook keeps its grid beside it and projects it again for a screen: HTML and SVG get each sheet as Excel’s window shows it, PDF its printed pages. New formats plug in as DocumentReader/DocumentWriter implementations (the @experimental interfaces) instead of new end-to-end converters — the SVG preview writer (consuming positioned pages) and the HTML and Markdown writers (consuming the FlowDoc directly, no layout or fonts involved) are exactly such adapters.

The same seam runs in reverse for PDF input. A PDF reader is just another DocumentReader, but inbound it reconstructs a FlowDoc rather than parsing a clean tree: a tagged PDF is rebuilt from its logical structure tree, an untagged one heuristically from glyph positions (lines by baseline, paragraphs by spacing and indent, the page read for its own gutters — as many columns of prose as it was set in, breaking to the next column and balancing the last page where the page does, blocks set side by side read down each on its own, a running head or foot made the section’s header or footer with its page number as a field, a ruled grid rebuilt as a table, a gap no word space could be — or one that lands on a stop the lines around it share — written as a tab on that stop, consecutive lines standing on the same stops rebuilt as a table of their own, a table set across the columns read whole, a code listing kept line for line, a drawing kept with the labels set on it as one figure, and an index’s hanging entries an entry apiece). Which reading it gets is the FILE’s to decide — a paper is mostly lines and re-flows, a form is mostly marks and keeps its page, with nothing for a caller to configure. The result is an ordinary FlowDoc, so Ream.parse(pdfBytes) converts onward to HTML, Markdown, docx or a re-laid PDF like any other source. A PDF names FACES (Inter-SemiBold, ArialMT), which is what a layout finds the document’s own program by; beside them the FlowDoc carries the family each face belongs to (faceFamilies, a FaceFamily apiece), the name a writer that hands the text to another program — a .docx — gives it, with the kind of face it is for a program that has to substitute. And it carries the faces themselves (faceOutlines): the outline of every character each face drew and the advance the page set it at, its kerning pairs and the ligatures the page drew, from which the .docx writer rebuilds a TrueType font and embeds it where its licence lets it — so the document is set in the type it was set in, on a machine that has none of its fonts.

The public API is deliberately small and I/O-free: Uint8Array in, Uint8Array out. Ream never touches the filesystem or the network on the conversion path (zero node:*, Buffer, process or __dirname imports), which is what lets the identical code run in a browser tab, a Node server, a Lambda, or an edge worker. How the bytes reach you — a File, a fetch, fs — is your call.

PDF embeds the fonts it draws with, so a converter needs font bytes. Ream’s model is caller-supplies-fonts: the synchronous API takes the TTF bytes you pass; the async API, as a convenience, fetches an open metric-compatible substitute (Arimo, Tinos, Cousine, plus Carlito/Caladea for Calibri/Cambria — the same families LibreOffice substitutes) based on the document’s referenced fonts, and a Noto face for any writing system those five cannot draw — Han, Kana, Hangul, Arabic, Hebrew, Thai, geometric symbols — asked for only when the document holds that script. A font the document embeds is used ahead of both. There are no bundled fonts on the main path — the library renders faithfully with whatever font you give it.

Ream is a correct typesetter — it lays out faithfully for the font it’s given, not a clone of any one program. A document Word sets — a .docx or a .doc — is laid out by Word’s own rules, each measured in Word: its lines as tall as Word sets its faces, two paragraphs the larger of their spacings apart, widow control, a heading kept with what it heads, table borders that take their room. A workbook is drawn by Excel’s: its columns in the digit Excel counts them in, its tints in Excel’s arithmetic, its tables in the styles Excel builds in — each set beside Excel’s own PDF of a workbook made to ask it. When you need the page to track a specific renderer instead, convert('pdf', { layoutProfile }) switches how text is measured, broken into lines and stacked to match it: 'word' for Microsoft Word, 'libreoffice' for LibreOffice, 'ream' (the default) for Ream’s own typesetter. Combined with the metric-compatible substitutes above, visual parity no longer needs the other tool’s private font metrics — only a font engineered to reproduce them and its layout rules.

Beyond the converters, the typed document model is exported from the reamkit/document-model subpath — paragraphs, runs, tables, sections, numbering, shapes, math and more. renderStyledPdf drives the layout engine directly if you want to build or transform a document programmatically rather than parse one.