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.
The pipeline
Section titled “The pipeline”A conversion walks the same stages a word processor would, each mapped to a section of the standard:
- OPC unpacking (ECMA-376 Part 2) — the
.docx/.xlsxis a ZIP of XML parts.fflateunzips;fast-xml-parserparses. (ZIP and XML are the only “incidental” pieces we don’t write ourselves — they are separate standards, not the task.) - OOXML parsing (Part 1) — WordprocessingML (§17), SpreadsheetML (§18), DrawingML (§20), OfficeMath (§22) become a typed, in-memory document model.
- Style cascade (§17.7) —
docDefaults→ named styles → direct formatting are resolved into the effective properties of every run, paragraph and cell. - 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.
- Text shaping — Unicode text becomes positioned glyphs: OpenType ligatures and kerning (GSUB/GPOS), BiDi reordering (UAX #9), Arabic cursive joining.
- 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 interlayer (FlowDoc)
Section titled “The interlayer (FlowDoc)”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 → bytesReam.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.
Bytes in, bytes out
Section titled “Bytes in, bytes out”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.
You supply the fonts
Section titled “You supply the fonts”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.
Renderer parity
Section titled “Renderer parity”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.
Document model
Section titled “Document model”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.