typograph-editor-documentation
    Preparing search index...

    Typograph — Page Preview Images

    This document describes PagePreview, the canvas library's API for turning a spread — or a single page inside a spread — into an image. It is intended for developers who embed Typograph and need thumbnails, contact sheets, a page picker, or an image to upload to a backend.

    PagePreview lives in the canvas library, not the editor, so it is available to any host: editor, viewer, or a headless integration.


    The editor ships as a single bundle (editor.js), so there is no module to import from — the library's internals have no import path of their own once bundled. PagePreview is reached through the global typograph object, the same place as setup() and get_typograph_document():

    <script type="text/javascript" src="https://cdn.typograph.nl/editor/latest/scripts/jsColorEngineWeb.js"></script>
    <script type="module" src="https://cdn.typograph.nl/editor/latest/editor.js"></script>
    const { PagePreview } = window.typograph;
    

    window.typograph is created once the colour engine has initialised, so read it inside a lifecycle listener rather than at the top level of your script:

    document.addEventListener('typograph_editor_ready', () => {
    const { PagePreview } = window.typograph;
    const spread = window.typograph.get_typograph_document().get_active_page();
    const url = PagePreview.data_url(spread, { width: 200 });
    });

    typograph_editor_ready is fired by the editor. In a non-editor host use typograph_init_ready, which the canvas library dispatches the moment window.typograph exists.

    Building against the canvas library from source instead of the bundle? Then the ordinary module import works and you do not need the global: import { PagePreview } from "typograph-canvas/scripts/pagepreview";


    // a 512px PNG data URL of the whole spread
    const url = PagePreview.data_url(spread);

    // 200px wide JPEG
    const url = PagePreview.data_url(spread, { width: 200, format: 'jpeg' });

    // just the right-hand page of a spread, 300px tall
    const url = PagePreview.data_url(spread, { page: 1, height: 300 });

    // a Blob for upload
    const blob = await PagePreview.blob(spread, { format: 'webp' });

    // a drawn <canvas> ready to put on the page
    container.appendChild(PagePreview.canvas(spread, { width: 256 }));

    A spread is a page container from the document:

    const doc = window.typograph.get_typograph_document();

    const spread = doc.get_active_page(); // the current one
    const spread = doc.pages[0]; // by index
    const all = doc.get_pages(); // every spread

    width and height are a bounding box, not the output size. The preview is scaled to fit inside the box and always keeps the spread's width/height ratio, so normally only one of the two is actually reached.

    For an A4 page (210 × 297 mm):

    Options Output
    (none) 362 × 512 — fits the default 512 × 512 box
    {width: 200} 200 × 283 — height follows from the ratio
    {height: 300} 212 × 300 — width follows from the ratio
    {width: 400, height: 400} 283 × 400 — height is the binding constraint
    {width: 100, height: 900} 100 × 141 — width is the binding constraint

    Give only one axis when you want that axis to be exact and do not care about the other. Give both when the preview has to fit a fixed slot.

    There is no upscale limit — ask for {width: 2000} and you get a 2000px-wide render, drawn at that resolution rather than scaled up from a small one.

    PagePreview.size() returns the exact pixel size the image will have without building or drawing anything. Use it to lay out around a preview instead of re-deriving the scale yourself:

    const { width, height } = PagePreview.size(spread, { width: 400, height: 400 });
    slot.style.width = `${width}px`;
    slot.style.height = `${height}px`;

    A spread contains one or more page records. Pass page with a 0-based index to render just that page; omit it for the whole spread.

    PagePreview.data_url(spread, { page: 0 });   // left-hand page
    PagePreview.data_url(spread, { page: 1 }); // right-hand page

    To find out how many pages a spread has:

    spread.sync_page_records();
    const count = spread.page_records.length; // 1 for a single page, 2+ for a spread

    An out-of-range index throws:

    PagePreview: page 5 does not existthe spread has 2
    

    Note the sizes differ: a 2-page A4 spread in a 400px box renders as 400 × 283 (landscape), while either of its pages renders as 283 × 400 (portrait).


    Option Type Default Description
    width number 512 Maximum output width in pixels
    height number 512 Maximum output height in pixels
    page number (whole spread) 0-based index of a single page within the spread
    format 'png' | 'jpeg' | 'webp' 'png' Encoding
    quality number 0.92 0..1. Applies to 'jpeg' and 'webp' only; ignored for 'png'

    Every option is optional. PagePreview.data_url(spread) is valid.

    You do not need a background colour. A spread always paints its own opaque background — a missing or unresolvable fill swatch falls back to Paper — so the output never contains transparent pixels and 'jpeg', which has no alpha channel, is safe to use directly.


    These build, draw and discard a preview in a single call. Use them unless you need the same preview more than once.

    PagePreview.data_url(spread, options?): string
    PagePreview.blob(spread, options?): Promise<Blob>
    PagePreview.file(spread, name?, options?): Promise<File>
    PagePreview.canvas(spread, options?): HTMLCanvasElement
    PagePreview.size(spread, options?): { width: number, height: number }

    file() names the result after the spread when name is omitted, and picks the extension from the format ('jpeg'.jpg):

    const file = await PagePreview.file(spread, 'cover', { format: 'jpeg' });
    // File { name: "cover.jpg", type: "image/jpeg" }

    Pass an explicit name whenever the filename matters. A spread's page_name often carries a generated suffix — an untitled page yields something like Untitled_3db29f4b-d155-4884-84b7-370a17f4260c.jpg.

    Construct once and repaint when the document changes — the canvas is created a single time.

    new PagePreview(spread, options?)

    .size // { width, height } — exact output size
    .canvas // the backing HTMLCanvasElement
    .redraw() // repaint after the spread changed
    .to_canvas(): HTMLCanvasElement // redraw, then return the canvas
    .to_data_url(format?, quality?): string
    .to_blob(format?, quality?): Promise<Blob>
    .to_file(name?, format?, quality?): Promise<File>

    format and quality given to a method override those from the constructor, so one instance can emit several encodings:

    const preview = new PagePreview(spread, { width: 256 });
    const thumb = preview.to_data_url('webp', 0.8);
    const upload = await preview.to_blob('jpeg', 0.95);

    const { PagePreview } = window.typograph;

    window.typograph.get_typograph_document().get_pages().forEach((spread, i) => {
    const img = new Image();
    img.src = PagePreview.data_url(spread, { width: 120 });
    img.title = `Page ${i + 1}`;
    strip.appendChild(img);
    });
    for (const spread of window.typograph.get_typograph_document().get_pages()) {
    spread.sync_page_records();
    for (let p = 0; p < spread.page_records.length; p++) {
    const url = PagePreview.data_url(spread, { page: p, height: 200 });
    // …
    }
    }
    const file = await PagePreview.file(spread, spread.page_name, { format: 'jpeg' });
    const form = new FormData();
    form.append('images[]', file);
    await fetch('/your/upload/endpoint', { method: 'POST', body: form });
    const preview = new PagePreview(spread, { width: 160 });
    container.appendChild(preview.to_canvas());

    // call redraw() whenever your application changes the document
    preview.redraw();

    typograph_edit_done (on document) is fired when a rich-text edit finishes. It is not a general "document changed" signal, so do not rely on it alone to keep a thumbnail current.


    PagePreview draws the spread as it currently stands. Fonts and images that have not finished loading are simply missing from the render — no error, just an incomplete image. This is the most common cause of a blank or half-empty preview.

    After opening a document, wait for the load events before taking previews:

    // images — on document, like every other Typograph event
    document.addEventListener('typograph_page_images_loaded', () => {
    const doc = window.typograph.get_typograph_document();
    const url = window.typograph.PagePreview.data_url(doc.get_active_page());
    });

    // fonts — on document
    document.addEventListener('typograph_fonts_loaded', () => { /* … */ });

    It fires once when the last pending image has painted, and once at open for a document that contains no images — so a single listener covers both cases.

    For a freshly opened document you generally want both events. Previews taken during normal editing, after the document has settled, need no special handling.

    If you already listen on window, you do not have to change anything yet. Earlier builds emitted this signal in two other shapes: 'typograph_page_images_loaded' on window (when the last image finished) and 'images_loaded' on document (when a document had none to load). Neither could catch both cases, which is why it was unified.

    Both older shapes are still emitted for backwards compatibility, and now fire in both cases rather than one — so existing code keeps working and gets more coverage than before. They are deprecated and will be removed once no integration depends on them; move to the document listener above when convenient.


    • Rendering is off-screen. Nothing is added to the DOM, and the canvas from canvas() / to_canvas() is detached — you choose where it goes.
    • Selection handles, guides, rulers and bleed marks are never drawn in a preview.
    • Output size is deterministic and does not depend on devicePixelRatio. Ask for a larger width when you want a sharper image, for example 2 * css_width for a retina thumbnail.
    • data_url() is synchronous; blob() and file() are async because the browser encodes off the main thread.