The
imagerenderer — places JPEG, refuses honestly otherwise.
Module oconvPdfRenderImage | Source packages/front/office/oconv/src/write/pdf/render/image.js | Deps oconvPdfLinebreak | Worker-safe yes
Resolve
import { ModuleRuntime } from '@awacloud/fw/core/runtime.js';
import { fw_require, modules } from '@awacloud/oconv';
const runtime = new ModuleRuntime();
runtime.registerAll(fw_require);
runtime.registerAll(modules);
const oconvPdfRenderImage = runtime.resolve('oconvPdfRenderImage');
Composed internally by ir-to-pdf's render
dispatcher, never called directly by a consumer. Placement additionally
needs ctx.builder (a builder-shaped image sink) and ctx.imageCounter —
both supplied only by the real facade.
API
| Member | Signature | Returns | Throws |
|---|---|---|---|
render |
(node: object, ctx: object) => {items: object[], losses: object[], height: number} |
block-local laid-out items (the renderer contract) | Error oconv: pdf image renderer needs ctx.builder — only on the placement path, when the facade supplied no image sink/counter |
node is an oconv-ir/v1 image node ({name, alt}, optionally carrying
escapes.docx.bytes). ctx additionally reads ctx.assets (the caller's
image-bytes map).
Examples
The session below hands the renderer the child context the stack gives it:
the members ir-to-pdf assembles (measurer, layout, column,
sizeFor, leading, linebreak, losses, render, assets, builder,
imageCounter) plus the per-block index, indent and kind the stack
stamps on top, and the flowChildren re-entry helper the facade binds to
each renderer (the placeholder line is flowed through it). childContext
rebuilds that helper the way the facade does: it re-enters the stack's
flowBlocks, honouring the styleOverride the placeholder path uses. The
IR is built with oconvIr.node, so it passes oconvIr.validate.
No bytes resolved — the refusal path
const { node, validate } = runtime.resolve('oconvIr');
const metrics = runtime.resolve('oconvPdfMetrics');
const box = runtime.resolve('oconvPdfBox');
const linebreak = runtime.resolve('oconvPdfLinebreak');
const stack = runtime.resolve('oconvPdfStack');
const measurer = metrics.createMeasurer();
const layout = box.resolveLayout();
function childContext(block, index, assets) {
const base = {
measurer, layout, column: layout.column, sizeFor: layout.sizeFor,
leading: layout.leading, linebreak, losses: [],
render: (b) => ({ items: [], losses: [{ code: 'layout/unhandled-block', detail: { kind: b.kind } }] }),
assets,
builder: { addImage() { return this; } }, // the facade's image sink
imageCounter: { n: 0 },
index, indent: 0, kind: block.kind
};
const flowChildren = (blocks, opts = {}) => {
const sub = {
...base,
indent: base.indent + (Number.isFinite(opts.indentDelta) ? opts.indentDelta : stack.INDENT_STEP),
indexPrefix: base.index,
ruleX: opts.ruleX === undefined ? null : opts.ruleX,
linebreak: opts.styleOverride
? { ...linebreak, breakInlines: (inlines, m, size, column) =>
linebreak.breakInlines(inlines, m, size, column, opts.styleOverride) }
: linebreak
};
const flowed = stack.flowBlocks(node('document', {}, blocks), sub);
let height = 0;
for (const b of flowed) height += b.spaceBefore + b.height + b.spaceAfter;
return { blocks: flowed, height };
};
return { ...base, flowChildren };
}
const imgNode = node('image', { name: 'missing.png', alt: 'Missing' });
validate(imgNode).ok; // true
const { items, losses, height } = oconvPdfRenderImage.render(imgNode, childContext(imgNode, '0', {}));
items.length; // 1 — the italic "[image: Missing]" placeholder line
losses;
// [{ code: 'layout/image-dropped',
// detail: { index: '0', name: 'missing.png', alt: 'Missing', reason: 'no-bytes', bytes: 0 } }]
height; // 20.520000000000003 (20.52 to two decimals)
Executed against the live package (2026-10-06): validate(imgNode).ok === true, items.length === 1, losses is exactly the one
layout/image-dropped record shown, height is 20.520000000000003 (20.52
to two decimals).
Notes
- Placement scope is JPEG only, a measured boundary, not an omission.
pdfBuilder.addImagedecodes nothing — bytes go into the XObject stream verbatim behind the/Filterthe caller names. JPEG entropy-coded data is a PDF image stream (/DCTDecode), so it embeds directly. A PNG'sIDATis a zlib stream of filtered scanlines, which is NOT a/FlateDecodeimage stream — embedding the file bytes would render garbage; doing it properly needs a zlib-to-Flate re-wrap plus an/SMaskfor alpha and an/Indexedpalette for colour type 3, none of which the writer implements. PNG is REFUSED with an honest loss rather than guessed at. - Three-way outcome: JPEG bytes resolve → PLACED (
ctx.builder. addImage(...), aq … cm /Im<n> Do Qitem, NO loss). Nothing resolved → italic[image: …]placeholder +layout/image-dropped,reason: 'no-bytes'. Bytes resolve but the encoding is not placeable → same placeholder +layout/image-dropped,reason: 'unsupported- encoding'(plusformatwhen the header reader could name the container, e.g.'png'). - The
ctx.builderseam exists becausepdfBuilder.addImageneeds a CURRENT PAGE, and builder pages are created only AFTER stacking — a renderer runs during layout, before anyone knows which page its block lands on. The facade puts a builder-SHAPED sink onctx.builderthat queues the spec by resource name; the emission loop later replays each spec onto the real builder on the page whose items reference it. The erroroconv: pdf image renderer needs ctx.builderis unreachable on every refusal path — a caller that never suppliesassetsnever meets it. - Placement geometry: one image pixel = one PostScript point at
natural size. The box is scaled DOWN to the block's column width when
wider, preserving aspect ratio, never scaled up. Vertical fit is NOT
re-decided here — an image taller than the page is handed to the
stacker exactly like any other oversized block (clean page, then
layout/block-clippedfromstack.js). assetBytesandreadImageHeaderare MIRRORS, not imports, of the identical helpers in../../ir-to-docx.js/../../ir-to-odt.js(asset resolution) and../../image-header.js(header parsing) — an fw factory may not capture a module-scope binding (fw/no-factory-capture), so each copy is pinned byte-identical to its sibling by a dedicated drift test rather than shared by import.- The resolution rule for image bytes is the SAME as the other two
writers: caller
assets[name]first, then the docx reader's carriedescapes.docx.bytes, else nothing. - Capture-free (
fw/no-factory-capture), worker-safe.
See also
ir-to-pdf— the dispatcher, and the sole owner ofctx.builder/ctx.imageCounter/the real emission-replay loop.ir-to-docx,ir-to-odt— the sibling writers whoseassetBytesthis module mirrors.pdf-writer.md§ "Images" — the full placement-format boundary table and theaddImageseam contract.pdf-writer.md—layout/image-droppedin the audience-facing loss-code table.