Examples
Every snippet below is runnable as-is; they all start from document bytes
(Uint8Array) however you obtained them — a File, a fetch, fs.
One parse, many targets
Section titled “One parse, many targets”Ream.parse reads the document once into the interlayer; every convert
renders from it without re-parsing:
import { Ream } from 'reamkit';
const doc = Ream.parse(bytes);
const pdf = await doc.convert('pdf', { fonts });const svg = await doc.convert('svg', { fonts }); // page-stack preview, no PDF involvedconst html = await doc.convert('html'); // flowed HTML — no fonts, zero I/Oconst md = await doc.convert('md'); // GitHub-Flavored Markdown — same, narrowerconst docx = await doc.convert('docx'); // WordprocessingML back out — no fonts, no layoutconst xlsx = await doc.convert('xlsx'); // SpreadsheetML back out — from an .xlsx sourcedocx → docx: normalize, sanitize, edit
Section titled “docx → docx: normalize, sanitize, edit”convert('docx') writes the parsed document back to a valid .docx. The
round-trip is semantic, not byte-exact — the writer emits the resolved
formatting as direct properties rather than named styles — so use it to
normalize, sanitize or programmatically edit a document in the browser, not to
preserve the original markup verbatim. Images, tables, lists, links, bookmarks,
shapes, headers/footers, multi-section geometry, footnotes/endnotes, charts and
OfficeMath all round-trip, and a drawing that states where on the page it
belongs is written back as the wp:anchor that puts it there rather than
flattened into the text flow. No image part the format cannot show goes into the
package: JPEG 2000 is dropped with a loss that names it.
import { Ream } from 'reamkit';
const doc = Ream.parse(bytes); // a .docx (xlsx has no docx writer)const out = await doc.convert('docx');// `out` is a fresh, valid .docx — hand it to a download, an upload, or re-parse it.docx → markdown: structure without geometry
Section titled “docx → markdown: structure without geometry”convert('md') writes GitHub-Flavored Markdown. Like HTML it is a flow medium —
no pagination, no layout engine, no fonts — but a much narrower one, so it keeps
what the document says and reports everything it cannot say back.
import { Ream } from 'reamkit';
const doc = Ream.parse(bytes);const { bytes: md, losses } = await doc.convertWithReport('md');
// Headings, lists (with their real numbers and nesting), GFM tables,// links, footnotes and the text inside shapes all survive.console.log(new TextDecoder().decode(md));
// Everything markdown has no syntax for is reported — once each, however// many paragraphs it happened in:// [dropped] text: paragraph alignment has no markdown expression// [degraded] tables: merged cells flattened — markdown has no spansfor (const loss of losses) console.log(loss.severity, loss.feature, loss.detail);Pictures are inlined as data: URIs so the output is a single self-contained
file. When you would rather write the image bytes yourself — a docs site, a
repo — ask for links instead and the writer names them predictably:
const md = await doc.convert('md', { images: 'link' }); // const bare = await doc.convert('md', { images: 'drop' }); // no pictures at allA deck’s only structure is where one slide ends and the next begins, and the
.pptx / .ppt readers carry that as a page break. Markdown has no pages, so
by default the break is dropped and reported — ask for the rule and each slide
gets its own section:
const deck = Ream.parse(pptxBytes);const md = await deck.convert('md', { pageBreaks: 'rule' }); // --- between slidesA workbook has the same problem and a different answer: markdown cannot tell
one sheet from the next, so each opens with a heading carrying its tab name.
A printed page never shows one — Excel and Calc emit it nowhere — so this is
markdown’s alone, and { sheetNames: false } turns it off:
const book = Ream.parse(xlsxBytes);const md = await book.convert('md'); // # Revenue, then its grid; # Costs, then itsAny attachment → Markdown for a language model
Section titled “Any attachment → Markdown for a language model”A chat that takes a document and hands it to a model wants what the document says, not how its pages look — and one function serves all seven inputs, a PDF and the legacy binaries included:
import { Ream } from 'reamkit';
/** An attached document as Markdown a model can read, whatever its format. */async function attachmentToMarkdown(bytes: Uint8Array): Promise<string> { const doc = Ream.parse(bytes); // .docx .xlsx .pptx .pdf .doc .xls .ppt — sniffed const deck = doc.format === 'pptx' || doc.format === 'ppt'; const md = await doc.convert('md', { images: 'drop', // no base64 pictures in the prompt pageBreaks: deck ? 'rule' : 'drop', // a deck's slides apart by --- }); return new TextDecoder().decode(md);}Markdown needs no fonts, so this touches neither the network nor a file system
and runs as well in the page, before anything is uploaded, as on a server. A PDF
comes back as the document it was — read from its structure tree when it is
tagged, from where its glyphs stand when it is not: paragraphs, headings, lists,
columns and tables. A form or a drawing comes back line by line instead, each
line where it stands. Ream does no OCR, so a scan gives up its text only from a
text layer OCR software left under the picture. What Markdown cannot carry —
charts, math, headers and footers — is in convertWithReport’s losses, as with
any other target.
xlsx → HTML / SVG: a workbook as Excel shows it
Section titled “xlsx → HTML / SVG: a workbook as Excel shows it”A workbook comes out two ways. Its PDF is the printed pages — print area, repeated titles, page breaks, fit-to-page, the drawings cut to the page they print on. Its HTML and SVG are what Excel’s window shows: every visible tab, one grid a sheet with all its columns at full size, text running on across the row, drawings over the cells where they are anchored, the window’s gridlines where the sheet asks for them, and a red corner on every cell with a note.
const book = Ream.parse(xlsxBytes);
const pdf = await book.convert('pdf', { fonts }); // the printed pagesconst html = await book.convert('html'); // the window: one <section data-sheet> a tabconst svg = await book.convert('svg', { fonts }); // a picture of each sheet, wholeThe HTML needs no fonts — the browser sets the text, and a note shows on hover over its cell. The SVG draws every glyph from its face’s own outline, so it looks the same in any viewer with no font to find; the words are laid over the glyphs unseen, so the text can still be searched and selected.
xlsx → xlsx: re-emit a workbook
Section titled “xlsx → xlsx: re-emit a workbook”convert('xlsx') writes a spreadsheet’s grid back to a valid .xlsx. Unlike the
docx writer it consumes the native grid tree, so the round-trip is lossless on
the grid surface — cells, styles, merges, the print model, conditional
formatting, sparklines, tables and embedded charts all survive a read → write →
read loop byte-stably. It requires a spreadsheet source; a .docx has no grid.
import { Ream } from 'reamkit';
const doc = Ream.parse(xlsxBytes); // a .xlsxconst out = await doc.convert('xlsx');// `out` is a fresh, valid .xlsx — normalize, sanitize, or re-parse it.Password-protected Word and Excel documents
Section titled “Password-protected Word and Excel documents”A password-protected .docx / .xlsx is not a zip at all — ECMA-376 §2.3 puts
the whole package inside an OLE container as ciphertext — so it has to be
decrypted before any reader sees it. The same password option does that, for
both Office schemes (the 2007 standard one and the agile one 2010 and later
write):
const doc = Ream.parse(docxBytes, { password: 'letmein' });const pdf = await doc.convert('pdf');A wrong password is an error, not a loss — here and in a PDF alike: every
scheme stores a verifier, so the file can say the password is wrong instead of
handing back rubbish. It comes as WrongPasswordError — a class, so a corrupt
file is not mistaken for a bad password:
import { Ream, WrongPasswordError } from 'reamkit';
try { Ream.parse(bytes, { password });} catch (e) { if (e instanceof WrongPasswordError) promptAgain(); else throw e; // not a password problem}A document that needs a password and gets none throws too, naming the option to
pass. An interface usually wants to know before that — to show a password field
rather than an error — and isEncryptedPackage answers from the bytes:
import { Ream, isEncryptedPackage } from 'reamkit';
const password = isEncryptedPackage(bytes) ? await askTheUser() : undefined;const doc = Ream.parse(bytes, { password });Decryption only: Ream never writes an encrypted package, so convert output is
always in the clear.
pdf → html / docx: read a PDF back
Section titled “pdf → html / docx: read a PDF back”Ream.parse also accepts a PDF — including a modern compressed one
(cross-reference streams, object streams) or an encrypted one. A tagged PDF (the
ones Ream writes) is rebuilt from its structure tree — headings, paragraphs,
tables, lists in reading order, through the role map a document names its own
elements by, with an inline element (a span, a link, MathML) kept in its
parent’s line; an untagged PDF is reconstructed heuristically
from glyph positions — in the columns of prose the page was set in, breaking to
the next column and balancing the last page where the page does, with a
running head or foot kept out of the body and its page number written as a
field — counted in the page’s own numerals, front matter in roman, and a foot
to each side where a book signs its even and odd pages apart — a page ruled into
a grid rebuilt as a table with its own columns and row heights, 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, a list’s items and an index’s hanging
entries a paragraph apiece, a heading on a dark bar given the bar as its
shading, a word the line broke in half put back together, and a bracketed block
of rows and columns read as an OfficeMath matrix.
A page that is not prose in columns is read ACROSS: an invoice sets its labels along the left and its figures against the right margin, and every one of those figures belongs to the line beside it. Blocks set SIDE BY SIDE are another matter: the same invoice stacks its dates and numbers beside the address it bills, each stack at a leading of its own, and read across, every line would catch a line of the other. Such a band comes back as the borderless table a writer would set it in, each block read down on its own, while blocks whose lines share their baselines stay one text. A gap no word space could be — or one that lands on a stop the lines around it share — comes back as a TAB standing on the stop the page set the piece after it at, consecutive lines broken at the SAME stops come back as a table, with a column of figures that end together set against its right edge, and a line the page drew between two blocks becomes the border of the paragraph it separates rather than a shape anchored where the words used to be. What a form writes — a button’s caption, a field’s value, a tick — stands where the page placed it, over the box it is written on, rather than joining the lines of the page.
Raster images, hyperlinks and the page’s artwork come
back too — images lifted out and sized from their placement (including the
ones written into the content stream, stencil masks painted in the page’s own
colour, a colour key leaving unpainted the samples it names, and a CMYK JPEG in
the colours its inks paint rather than in Adobe’s inverted convention), link
annotations re-attached to the text, filled paths, stroked lines — dashed and
capped as the page draws them — and shading-pattern gradients turned into
shapes, the clipping paths that limit them, tiling patterns (a hatch or a grid
as the tone it gives the page), the opacity a fill or a stroke is painted at,
the appearance an annotation carries — or one drawn from its properties where
the file supplies none, a free-text note from its contents, and a form that
asks for its appearances rebuilt showing its fields’ values — and the Type 3
glyphs that are drawings rather than letters. Colour comes back
through whatever space states it: device, CIE (CalGray, CalRGB, Lab), an
/ICCBased stream through the profile it carries, or a Separation/DeviceN
run through its own tint transform — for an image as much as for a fill. A
gradient the page paints with a bare sh is read too, function-based included,
and words filled with one come back in its colours. The layers a file
turns off stay off, and the box it says to show is the box you get. Type is
read the way the font states it — the base encoding it declares, the built-in
one a symbolic font carries (ZapfDingbats is a font of pictures and comes back
as pictures), a predefined CMap it names instead of embedding — a ligature
comes back as the letters it joins, punctuation a font names no character for
is read off the shape the page draws, and a face that cannot be addressed by
character at all is DRAWN from its outlines rather than guessed at. The result
is an ordinary FlowDoc, so it converts onward like any other source. Written
to .docx, a run names its face’s family — Inter, not Inter-SemiBold, a
font no word processor has — and the face itself travels with the document:
rebuilt as TrueType from the outlines the page drew, at the advances it set them
at, with its kerning and the ligatures the page drew, and embedded where its
licence lets it, so Word and LibreOffice break each line where the page broke
it on a machine that has none of the document’s fonts. A word space TeX shrank
to fit a line is written as narrow as TeX set it, a picture keeps its crop, its
turn and its opacity, and a line its dash.
A form or a drawing is not a reflowable document, though — its rules and boxes are placed absolutely, and a label an inch from the box it labels says nothing. The file decides for itself: a paper is mostly lines and is re-flowed, a form or a drawing is mostly marks and is kept as a page, with every line where its glyphs stand. There is nothing to configure — the reader records which reading it took, in the losses:
const doc = Ream.parse(pdfBytes);const docx = await doc.convert('docx');doc.losses.find((l) => /read as a/u.test(l.detail)); // …which reading it tookimport { Ream } from 'reamkit';
const doc = Ream.parse(pdfBytes); // doc.format === 'pdf'const html = await doc.convert('html'); // the PDF's text as flowed HTMLconst docx = await doc.convert('docx'); // …or an editable Word document
const { bytes, losses } = await doc.convertWithReport('html');// losses note the untagged-heuristic degradation and any unread vector art.Encrypted PDFs
Section titled “Encrypted PDFs”A locked PDF opens with its password — the user password, or the owner’s, which opens it as well. The empty-string default unlocks the common permissions-only encryption (where the owner set restrictions but no open password), so most encrypted PDFs need no password at all:
const doc = Ream.parse(pdfBytes, { password: 'letmein' });Every revision of the PDF security handler stores a verifier, so a wrong
password — or none, where one is needed — is refused rather than decrypted
into rubbish, with the same WrongPasswordError an Office package throws:
import { Ream, WrongPasswordError } from 'reamkit';
try { Ream.parse(lockedPdf); // no password} catch (err) { if (err instanceof WrongPasswordError) { err.message; // 'This PDF needs a password to open — pass it as `password`' }}A file locked by a security handler Ream does not implement is not a password
problem: it parses, and what could not be decrypted is a read-time loss. In
strict mode that loss throws a ConversionLossError, with the offending
Loss on its .loss property:
import { Ream, ConversionLossError } from 'reamkit';
try { await Ream.parse(otherHandlerPdf).convert('html', { strict: true });} catch (err) { if (err instanceof ConversionLossError) { err.loss.detail; // 'encrypted PDF — its security handler is not supported, …' }}A PDF whose streams use a filter Ream does not carry
Section titled “A PDF whose streams use a filter Ream does not carry”§7.4 leaves the filter set open, and PDF 2.0 added Brotli. A stream Ream cannot decode is a stream it has not read — and when that stream is the cross-reference, the file has no pages at all. Supply the decoder and it does:
import brotliPromise from 'brotli-dec-wasm'; // ~200 KB wasm, browsers and workersimport { Ream } from 'reamkit';
const brotli = await brotliPromise;
const doc = Ream.parse(pdfBytes, { filters: { BrotliDecode: (bytes) => brotli.decompress(bytes) },});Any Brotli implementation you already ship will do — on a server the runtime
carries one, so import { brotliDecompressSync } from 'node:zlib' and pass
that instead. Ream itself imports neither: a 122 KB static dictionary in every
browser bundle, for a filter almost nothing produces, is not a cost the common
case should pay. Without a decoder the loss report names the filter, so a
document that comes back empty says why.
pdf → pdf: a form keeps its form
Section titled “pdf → pdf: a form keeps its form”A paper reads back as a re-flowable document — paragraphs, columns and tables in reading order. A form is not that: its grid is painted at fixed coordinates, and text that flows beside the grid sits in none of its boxes. So a form is read as a page instead, every line standing where its glyphs stand — and the file, not the call, decides which of the two it gets:
import { Ream } from 'reamkit';
const doc = Ream.parse(pdfBytes);const pdf = await doc.convert('pdf', { fonts });The reader counts the marks against the baselines on the median page and records which reading it took and why, so a plan folded into a report does not make the report a plan. Worth knowing when the output is DOCX, Markdown or HTML: a file read as a page has no reading order, no paragraphs and no tables to give them.
pptx → pdf: render a slide deck
Section titled “pptx → pdf: render a slide deck”Ream.parse also accepts a PowerPoint .pptx. Each slide becomes a page at
the deck size, its shapes read as positioned content — text boxes, placeholders,
pictures, shapes, tables, charts, embedded objects, groups and hyperlinks. What
a deck states once reaches every slide that relies on it: the master’s and the
layouts’ own decoration (drawn under the slide’s content), the background —
solid, gradient, picture or a theme reference — the deck’s colour map, the text
style its p:defaultTextStyle and p:txStyles set, and the table styles in
tableStyles.xml. The result is an ordinary FlowDoc, so it converts onward
like any other source:
import { Ream } from 'reamkit';
const doc = Ream.parse(pptxBytes); // doc.format === 'pptx'const pdf = await doc.convert('pdf', { fonts }); // a page per slideconst html = await doc.convert('html'); // …or the slides as flowed HTMLWord review comments
Section titled “Word review comments”Review comments (w:commentReference) render as a bracketed superscript marker in
the text and a “Comments” section after the body — author, date and content, with
reply threads nested and resolved threads flagged, the commented range highlighted,
and author identities resolved from people.xml. In PDF the marker is a clickable
jump to the entry; pass commentAnnotations: true to also attach each comment as a
native sticky-note annotation (interactive output only — suppressed under PDF/A and
tagged):
import { Ream } from 'reamkit';
const doc = Ream.parse(docxBytes);
const pdf = await doc.convert('pdf', { fonts }); // markers + Comments section + highlightsconst sticky = await doc.convert('pdf', { fonts, commentAnnotations: true }); // …plus /Text pop-upsconst html = await doc.convert('html'); // replies nested, resolved threads flagged
doc.flow.comments?.get('0'); // { content, author?, date?, authorId?, parentId?, done? }Comments — threads and resolved flags included — also write back through
convert('docx').
Legacy .doc / .xls / .ppt
Section titled “Legacy .doc / .xls / .ppt”The binary Office formats parse through the same entry point and the same
interlayer — Ream.parse(bytes) sniffs OLE2/CFB and picks the reader, so every
target and every option below works on them unchanged. A legacy deck brings its
master’s decoration and background, the text its slide list holds with the
sizes, colours, typefaces and bullets its master states, grouped shapes and
tables, picture fills and the metafiles it draws with; a legacy workbook brings
its styling, its drawings placed on the sheet’s own grid, its charts, its print
model and its conditional formatting.
const doc = Ream.parse(bytes); // .doc / .xls / .ppt — sniffed like the restconst pdf = await doc.convert('pdf');const xlsx = await doc.convert('xlsx'); // a .xls re-writes as a modern workbookExcel pivot tables
Section titled “Excel pivot tables”A pivot table’s cached output renders as an ordinary grid; on top of that Ream applies
the table’s named pivot style (pivotTableStyleInfo) — banded rows and a styled
header — and emphasises the grand-total / subtotal rows and columns. Nothing is
recomputed from the cache:
import { Ream } from 'reamkit';
const doc = Ream.parse(xlsxBytes);const pdf = await doc.convert('pdf', { fonts });const html = await doc.convert('html');Excel data validation
Section titled “Excel data validation”A list data validation (<dataValidations>) paints an in-cell dropdown affordance
— a small button with a ▾ — on every cell of its range, in both PDF and HTML. The
validation, its formulas and the input/error prompts also round-trip through
convert('xlsx'):
import { Ream } from 'reamkit';
const doc = Ream.parse(xlsxBytes);const pdf = await doc.convert('pdf', { fonts }); // list cells show a ▾ dropdown buttonconst html = await doc.convert('html');const xlsx = await doc.convert('xlsx'); // the <dataValidations> survive the round-tripExcel slicers
Section titled “Excel slicers”A slicer (xl/slicers + xl/slicerCaches) renders as a captioned button box after the
grid — the way a chart frame does. A native-table slicer fills its buttons from the
referenced table column’s distinct values and highlights the ones the column’s
autofilter keeps; an OLAP/pivot slicer whose items live in a pivot cache degrades to a
caption-only box.
import { Ream } from 'reamkit';
const doc = Ream.parse(xlsxBytes);const pdf = await doc.convert('pdf', { fonts }); // a button box per slicer, after the gridconst html = await doc.convert('html');SmartArt diagrams
Section titled “SmartArt diagrams”SmartArt (DOCX and PPTX) renders either way round. Where the file caches a
pre-rendered DrawingML drawing (diagrams/drawing#.xml) that is drawn directly;
where it does not, the diagram is laid out from its own data, layout,
colors and quickStyle parts — the algorithms (lin, composite, cycle,
the hierarchy pair, snake and the rest), the constraint solver that sizes and
spaces the boxes, and the colour and style parts that paint them. Scheme colours
resolve through the document or deck theme, and box text is measured so a box
grows to what it holds.
Nothing special is needed to get it — it is part of the ordinary conversion:
import { Ream } from 'reamkit';
const { bytes, losses } = await Ream.parse(docxBytes).convertWithReport('pdf', { fonts });// `losses` is where anything the file asks for and Ream cannot draw is reported.Browser: file input → PDF preview
Section titled “Browser: file input → PDF preview”import { Ream } from 'reamkit';
input.addEventListener('change', async () => { const bytes = new Uint8Array(await input.files![0].arrayBuffer()); const pdf = await Ream.parse(bytes).convert('pdf'); window.open(URL.createObjectURL(new Blob([pdf], { type: 'application/pdf' })));});Node: file in, file out
Section titled “Node: file in, file out”import { readFileSync, writeFileSync } from 'node:fs';import { Ream } from 'reamkit';
const doc = Ream.parse(new Uint8Array(readFileSync('report.docx')));writeFileSync('report.pdf', await doc.convert('pdf'));Archival PDF/A with the source embedded
Section titled “Archival PDF/A with the source embedded”The whole PDF/A family is supported (1a/1b, 2a/2b/2u, 3a/3b/3u). PDF/A-3 can carry the source document inside the PDF as an associated file:
const { bytes: pdfa, losses } = await doc.convertWithReport('pdf', { fonts, pdfA: 'PDF/A-3b', embedSource: true, // .docx/.xlsx rides along as /AF with /AFRelationship /Source});'PDF/A-1a' / 'PDF/A-2a' / 'PDF/A-3a' additionally emit the tagged
logical structure (headings, tables, lists, figure alt text).
Accessible PDF/UA-1
Section titled “Accessible PDF/UA-1”pdfUA: true produces ISO 14289-1-conformant output — tagged structure,
alternate descriptions on links, an always-announced document title. It
combines with PDF/A in a single file (both veraPDF-validated):
const accessible = await doc.convert('pdf', { fonts, pdfUA: true });const archival = await doc.convert('pdf', { fonts, pdfA: 'PDF/A-2a', pdfUA: true });Digital signature
Section titled “Digital signature”PKCS#7 detached (ISO 32000 §12.8) via WebCrypto — RSA or ECDSA:
const signed = await doc.convert('pdf', { fonts, signature: { certificate: certificateDer, // Uint8Array, DER privateKey: cryptoKey, // WebCrypto CryptoKey // optional: signingTime, reason, location, pades: true, timestampUrl },});Font resolution chain
Section titled “Font resolution chain”Chain providers; the first byte answer wins. A remote or local winner is
recorded as a substituted loss:
import { Ream, callerFontProvider, localFontProvider, remoteFontProvider } from 'reamkit';
const { bytes, losses } = await doc.convertWithReport('pdf', { fontProviders: [ callerFontProvider(myFonts), // your bytes first localFontProvider(), // system fonts (Chromium Local Font Access; // embedding-restricted fonts are never used) remoteFontProvider(), // open substitute set, last resort ],});// losses[0] → { severity: 'substituted', feature: 'fonts.substitution', … }Fonts embedded in the document itself (w:embed, including obfuscated
.odttf, and a deck’s p:embeddedFontLst) always win — glyph-exact, no
substitution. A w:rFonts that names a theme slot rather than a family
resolves through the theme’s major / minor fonts before any of this runs.
A face the network fails to deliver is not remembered as failed: the next
conversion asks for it again, so a server that builds its provider chain once
and reuses it keeps working after a dropped connection. A required face that
cannot be fetched still throws, with the network’s error as its cause.
The substitute set is Latin, so a document holding another writing system is served separately: a Noto face is fetched per SCRIPT — Japanese, Korean, Chinese, Arabic, Hebrew, Thai, geometric symbols — and only for the scripts the document actually contains. It is chosen per character, so a paragraph that mixes Latin and Arabic is drawn in both faces rather than in one with holes:
// A sheet of Korean: NotoSansKR is fetched, in the regular weight only.// The same call on a Latin-only document downloads nothing extra.const pdf = await Ream.parse(bytes).convert('pdf');Renderer parity
Section titled “Renderer parity”Ream is a correct typesetter: it lays out faithfully for the font you give it, and a
document Word sets — a .docx or a .doc — stands its lines as Word sets them, by
rules measured in Word. For closer visual parity with a specific renderer, pass a
layoutProfile — it switches how text is measured, broken into lines and stacked to
match that tool. Paired with the metric-compatible substitutes above (so the same
glyph advances are in play), the page geometry tracks the target closely:
const pdf = await doc.convert('pdf', { fonts, layoutProfile: 'libreoffice' });// 'word' targets Microsoft Word; 'ream' (the default) is Ream's own typesetter.'libreoffice' breaks lines greedily (first-fit) and takes each line from the face in
hand — its hhea line, or its OS/2 typo line where the face asks for that; 'word'
measures without kerning (Word’s default), breaks first-fit and stands every line as
Word sets it, whatever the document. The profile applies to flowing text (DOCX/PPTX);
spreadsheet geometry follows Excel’s row model regardless.
Strict mode (compliance flows)
Section titled “Strict mode (compliance flows)”Make any loss fatal instead of reported:
import { ConversionLossError } from 'reamkit';
try { await doc.convert('pdf', { fonts, strict: true });} catch (e) { if (e instanceof ConversionLossError) { // e.loss — what exactly would have been dropped/degraded/substituted }}Inspect the interlayer
Section titled “Inspect the interlayer”parse produces a format-neutral tree before any rendering:
const doc = Ream.parse(bytes);
doc.format; // 'docx' | 'xlsx'doc.losses; // read-time lossesfor (const el of doc.flow.body) { // el.kind: 'paragraph' | 'table' | 'image' | 'chart' | 'shape'}PDF metadata
Section titled “PDF metadata”/Info is read automatically from the document’s docProps/core.xml;
caller values override it:
const pdf = await doc.convert('pdf', { fonts, info: { title: 'Q4 Report', author: 'Finance' },});Hyphenation
Section titled “Hyphenation”import { getHyphenator } from 'reamkit';
const hyphenator = await getHyphenator('en-us'); // or 'ru'const pdf = await doc.convert('pdf', { fonts, hyphenator });