From f69b8cb2decbd278f760310981a163de06c2ab67 Mon Sep 17 00:00:00 2001 From: bg-l2norm <136485665+bg-l2norm@users.noreply.github.com> Date: Sun, 12 Apr 2026 14:25:28 +0000 Subject: [PATCH] feat: add setMeasureContext for SSR support Adds `setMeasureContext` function to allow injecting custom canvas context for text measurement in SSR environments like Node.js or NextJS where DOM and OffscreenCanvas are not globally available. Also updates the README with an SSR integration guide. --- README.md | 19 +++++++++++++++++++ src/layout.ts | 3 +++ src/measurement.ts | 4 ++++ 3 files changed, 26 insertions(+) diff --git a/README.md b/README.md index 88b598c..2922ff2 100644 --- a/README.md +++ b/README.md @@ -210,6 +210,7 @@ Other helpers: ```ts clearCache(): void // clears Pretext's shared internal caches used by prepare() and prepareWithSegments(). Useful if your app cycles through many different fonts or text variants and you want to release the accumulated cache setLocale(locale?: string): void // optional (by default we use the current locale). Sets locale for future prepare() and prepareWithSegments(). Internally, it also calls clearCache(). Setting a new locale doesn't affect existing prepare() and prepareWithSegments() states (no mutations to them) +setMeasureContext(ctx: CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D | any): void // allows setting a custom canvas context for text measurement. Particularly useful in SSR environments (like NextJS or Node.js) where DOM and OffscreenCanvas are not available. ``` Notes: @@ -222,6 +223,24 @@ Notes: - `measureNaturalWidth()` returns the widest forced line. Hard breaks still count. - `prepare()` and `prepareWithSegments()` do horizontal-only work. `lineHeight` stays a layout-time input. +## Server-Side Rendering (SSR) + +To use Pretext in a server-side environment (like Node.js or NextJS), you'll need to provide a canvas implementation, as `document` and `OffscreenCanvas` are not globally available. + +You can use the `canvas` package from npm and `setMeasureContext` to initialize the measurement context before calling `prepare()` or `prepareWithSegments()`: + +```ts +import { setMeasureContext } from '@chenglou/pretext' +import { createCanvas } from 'canvas' + +// Initialize the measurement context once, ideally at module scope +const canvas = createCanvas(1, 1) +const ctx = canvas.getContext('2d') +setMeasureContext(ctx) + +// Now you can use prepare() and layout() as normal +``` + ## Caveats Pretext doesn't try to be a full font rendering engine (yet?). It currently targets the common text setup: diff --git a/src/layout.ts b/src/layout.ts index ec8e0e8..136dce0 100644 --- a/src/layout.ts +++ b/src/layout.ts @@ -53,6 +53,7 @@ import { import { type BreakableFitMode, clearMeasurementCaches, + setMeasureContext, getCorrectedSegmentWidth, getSegmentBreakableFitAdvances, getEngineProfile, @@ -785,3 +786,5 @@ export function setLocale(locale?: string): void { setAnalysisLocale(locale) clearCache() } + +export { setMeasureContext } diff --git a/src/measurement.ts b/src/measurement.ts index 8ef956d..378d76b 100644 --- a/src/measurement.ts +++ b/src/measurement.ts @@ -31,6 +31,10 @@ const maybeEmojiRe = /[\p{Emoji_Presentation}\p{Extended_Pictographic}\p{Regiona let sharedGraphemeSegmenter: Intl.Segmenter | null = null const emojiCorrectionCache = new Map() +export function setMeasureContext(ctx: any): void { + measureContext = ctx +} + export function getMeasureContext(): CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D { if (measureContext !== null) return measureContext