typograph-editor-documentation
    Preparing search index...

    Typograph — Fitting text into its frame

    This document describes Fit text: a rule a rich text frame carries saying how its text may be adjusted to fit the box, and in what order. The canvas applies it, in order, only as far as it must.

    It exists for one case above all: a host that refills a template's text for every order. The text is different every time and the box is not.

    Source of truth: autofit on PageElementRichText and refit() in the canvas package at src/page.ts; the rule shape is autofit_rules in src/story.ts; the schema is PageElement\Autofit.


    element.autofit = {
    enabled: true,
    steps: [
    { lever: 'hyphenation' }, // free — usually first
    { lever: 'point_size', to: 85, unit: 'percent', step: 0.25 },
    { lever: 'leading', to: 90, unit: 'percent' },
    { lever: 'letter_spacing', to: -20, unit: 'em' },
    ],
    };

    Read it as a sentence: you may break words; then take the type down as far as 85% of what I set; then close the lines up to 90%; then take up to 20/1000 em out of the letters.

    lever unit to means Notes
    hyphenation — — on or off; takes no extent at all
    point_size percent or pt no smaller than pt is measured against the smallest authored size in the frame
    leading percent or pt no tighter than pt against the smallest leading any line resolves to
    letter_spacing em take out up to this much a negative delta in 1/1000 em
    word_spacing em take out up to this much a negative delta in 1/1000 em

    step is the granularity in the lever's own unit, and is optional — point size defaults to a quarter point, so a fit lands on a size a designer reads rather than on 8.4173 pt.

    Horizontal glyph scale is not a lever. Unlike the five above it exists nowhere in the canvas or in print, so it would be a rule nothing could honour.

    Each lever is exhausted before the next is reached, and the search stops as soon as the text fits. So break words → smaller type → tighter lines is a different instruction from the same three in any other order: a designer who would rather lose a little leading than a point of type says so by putting leading first.

    A lever may be named once. Pulling the same one twice cannot find anything the first pull did not.

    ⚠️ Spacing is stated as a negative delta, never as a percent, and that is measured rather than stylistic: letter and word spacing default to zero on almost every run there is, so "tighten to 80%" would be 80% of nothing and the lever would silently do nothing. A delta works from whatever each run authored and preserves the differences a designer set between runs. A positive value would add space and make the overflow worse, so it is refused.


    Nothing is written back. native_font_size and every run's point_size stay exactly as authored; the fit is a set of multipliers applied while the text is composed.

    element.autofit the authored rule — on the wire
    element.autofit_factor the fit's result — not on the wire, recomputed

    Three reasons, and the first is why the feature was asked for:

    1. Variable data. A stored shrink is fitted to whatever text was there when it ran, so the next order — with shorter text — would keep the small type. A derived fit re-fits per text.
    2. A stored fit destroys the authored intent. Once 11 pt has become 8.4, nothing knows which of them the designer chose.
    3. It composes anyway, so the fit is measured where the text already passes.
    const authored  = element.native_font_size;                       // what the designer set
    const effective = authored * element.autofit_factor; // what it came out at
    const back = element.authored_point_size(effective); // the inverse, exactly

    ⚠️ A control that reports the wrong one disagrees with the document. The editor's size field shows the authored size — it has to, because typing into it writes one, and a field that showed 7.5 and then stored 7.5 would shrink the type again on every visit. The effective figure belongs where the rule is authored, which is where the Fit text panel puts it.


    refit() is called at three moments, and never per keystroke:

    • a resize — update_native_rect();
    • a text change — at the end of a typing burst, not per letter. Each probe is a full re-composition, so a fit per keystroke would pay several of them per letter. A paste or a programmatic set_text() is discrete and fits immediately;
    • the document load — which is the moment that matters for variable data, because a template refilled by a host may never be opened in an editor at all.

    A host that builds or rewrites text through the API and wants the fit recomputed sooner can call element.refit() itself. It returns the point-size factor now in force.


    It will not clip. A frame whose levers are exhausted keeps its overflow, so validate()'s text_overflow check goes on telling the truth — an order cannot reach print quietly missing its last lines. autofit_exhausted reports the same frame as a warning and says, in detail.spent, what was actually spent.

    It will not fit a linked frame. A chain's text belongs to the chain root, so every probe would have to reflow the whole chain — and shrinking in the first frame pulls text back from the third. A chained frame is left unfitted rather than half-fitted.

    It will not fight AutoSize. AutoSize grows the box to hold the text; this shrinks the text to hold the box. Run together they argue in a loop — the box grows, so the text fits at full size, so the factor returns to 1, so the box grows — and the frame's height would depend on which ran last. AutoSize wins, because the height is already its output.

    ⚠️ Both of those are reported, not silent: autofit_not_applied names the frame and says which of the two it is. Without it a designer who set a rule on such a frame would see no fit, no overflow explanation and no complaint.


    The rule is gated by its own edit option, separate from the size:

    { text: { size: true, autofit: false } }     // retype and resize the type, but the rule stays mine
    

    A host grants you may retype this while keeping and it still shrinks the way I set it — the rule is the designer's and the words are the reader's. viewer_v1_defaults() withholds autofit: everything else it grants is filling a template in.

    The canvas answers the question through can(element, 'text_autofit'), and the editor's Fit text panel does not appear when it says no.

    In the viewer (since 20261104.3, TYP-1411) the toolbar of a selected text frame offers a Fit text on/off button when both hold: the frame has a rule (autofit.steps is not empty) and the role allows it. The button switches autofit.enabled and keeps the designer's steps, in one undo step, and the frame refits at once. The viewer never writes a rule, and a frame without one shows no button. The editor's View ▸ Preview as viewer shows the button too. A host hides it with gEditorUI.hide('toolbar.autofit').


    The canvas emits its composed layout — per line and per word: position, text, family, style, size, width, letter spacing, word spacing, baseline — and document-generator consumes exactly that. The fitted size is therefore already in what print reads, with no second implementation in PHP.

    This is unusual: most geometry rules in this system are written twice, once here and once in the publisher. This one is not, because the fit is applied where the words are measured.


    There is no dedicated host method in this version — the rule is a field. A host that writes it should bracket the change so it can be undone, and refit:

    const doc     = window.typograph.get_typograph_document();
    const spread = doc.pages[doc.active_page_index];
    const element = spread.getElementByUUID(uuid);

    doc.set_undo();
    element.autofit = {
    enabled: true,
    steps: [{ lever: 'point_size', to: 70, unit: 'percent', step: 0.25 }],
    };
    element.refit(); // the rule changed, so the fit has
    doc.set_redo();

    doc.redraw();

    ⚠️ refit() records nothing of its own, and must not: a document whose text merely re-fitted is not a document the reader changed. What belongs in the undo history is the rule.

    // Before sending an order: which frames only fit because the type was shrunk, and by how much?
    const shrunk = [];

    doc.pages.forEach((spread, container_index) => {
    spread.page_elements.forEach(el => {
    if (el.type !== 'rich' || !el.autofit?.enabled) return;
    if (el.autofit_factor === 1) return; // fits at its own size

    shrunk.push({
    container_index,
    name: el.name || el.uuid,
    authored: el.native_font_size,
    effective: +(el.native_font_size * el.autofit_factor).toFixed(2),
    });
    });
    });

    // And the frames where it was not enough — the library has already worked this out:
    const exhausted = window.typograph.validate({ checks: ['autofit_exhausted'] });

    element.autofit {enabled, steps[]} — the authored rule, on the wire
    element.autofit_factor the point-size result, derived, not on the wire
    element.refit() recompute now; returns the factor
    element.authored_point_size(effective) the inverse — what a control should show
    Levers hyphenation, point_size, leading, letter_spacing, word_spacing
    Units percent, pt, and em for spacing (a negative delta in 1/1000 em)
    Runs on resize, text change (per burst), document load
    Never clips text, fits a linked frame, or overrides AutoSize
    Permission text.autofit — separate from text.size
    Print needs nothing; the emitted layout already carries the fitted values