Zhuyin, Pinyin and emphasis marks over horizontal text were placed
against an em box bottom-aligned to Skia's tight glyph rect, which spans
the hhea ascent and descent. In CJK faces the descent reaches past the
em box, so Zhuyin columns sat about 0.17 em below their base, and over
annotations relied on a hand-tuned clearance.
Place them against the ideographic em box (OS/2 typo metrics) of the
font that shaped each base, centred on the line baseline, the same
metrics vertical text uses for upright glyphs.
AI-assisted-by: claude-opus-5-5
Skia keys a line's style runs by UTF-8 byte offset, while line indices
are UTF-16. Decoration segments searched those runs with UTF-16 offsets,
so with CJK text every underline or strike on a line took the paint of
the first run, and a decorated span wrapped onto a new line lost its bar.
Look up each segment's run by the span's UTF-8 style anchor, falling
back to the line's first run for a span continued from an earlier line.
AI-assisted-by: claude-opus-5-5
Each ruby reading attached its lowest ink to the base, which suits kana
but not Latin letters: the descender of "y" lifted "yǒu" above "liáo"
over 友聊, and under ruby moved with ascenders and tone marks.
Western letters in a reading now take the font's descent (over) or cap
height (under) instead of their ink, so Pinyin and romaji readings share
one baseline and descenders stay clear of the base. Upright glyphs keep
the ink edge.
AI-assisted-by: claude-opus-5-5
SkParagraph moves each line right by half the letter-spacing of its first
character. That centres span tracking, but the spacing added to single
characters (distribution and justification gaps, squeezing, compression
and ruby room) must only open the gap after them. A distributed label of
two characters started half a gap in and overflowed the box by as much:
"国家" in a 200px box drew 国 92px from the left edge.
Builders of horizontal text record how far each adjusted character's
letter-spacing departs from its span tracking, and `LayoutParagraph` takes
back half of that on each line, next to its baseline shifts: painting,
glyph and placeholder rects, line metrics, hit testing and text paths.
Span tracking keeps its half spacing before the line.
AI-assisted-by: claude-opus-5-5
Four fixes to how `digits` tate-chu-yoko composites lay out in vertical
text:
- A lone digit combines too, as CSS `digits <n>` combines runs of n or
fewer, so it stands upright instead of turning sideways. The renderer
(`split_digit_runs`) and the export path (`digit-combine-segments`)
share the rule.
- Composites centre on the OS/2 typographic metrics, like upright glyphs.
The hhea ascent of CJK faces exceeds the em, which set digits about
0.07em below the kanji beside them.
- Letter-spacing stays outside the composite, as in CSS. Counting the
trailing spacing in its width moved the digits left by half of it.
- The regional Chinese profiles no longer add the Han/Western gap around
a composite: it sets in one ideograph's cell, as JLREQ already does.
AI-assisted-by: claude-opus-5-5
Add distributed alignment to text styles, sidebar controls, CSS export,
and the Plugin API. Spread characters across horizontal and vertical
lines, including the final line, while preserving graphemes and annotations.
Keep fractional line heights consistent across painting, hit testing,
selection, and text paths to prevent baseline drift between lines.
Add tests for distribution, nonbreaking text, and fractional line geometry.
AI-assisted-by: gpt-6
Keep bounded content and selection history within each editing session.
Group committed compositions and styled insertions, omit IME previews,
and reset history when external content changes the editing baseline.
Route keyboard, workspace menu and browser history input to the editor.
Restore UTF-16 selections through a new WASM binding, preserving styles,
readings and paragraph settings without reaching earlier canvas edits.
Verify 1,587 frontend tests, 671 renderer tests and both WASM builds.
Add four editable showcase examples and verify them after reopening.
AI-assisted-by: GPT-6
Recover merged annotation styles on the base character's own line, using
separate UTF-16 line anchors and UTF-8 style keys. Include emphasis marks
in horizontal text bounds so standalone exports keep each mark whole.
Add regression coverage for adjacent readings, wrapped text, emoji line
anchors, raster output and export bounds across all text grow modes.
Verify 670 renderer tests, formatting and both WASM builds. Add four
editable showcase examples and check their layout and exports.
AI-assisted-by: GPT-6
Retain legal line starts at the original wrap width before allowing an
indivisible punctuation or annotation unit to overflow together. Share the
layout plan and alignment offset across painting, editing and exports.
Cover mixed styles, nonbreaking controls, graphemes, annotations, alignment,
strokes and resize. All 666 renderer tests and both WASM builds pass.
AI-assisted-by: GPT-6
Add typed one-character Zhuyin readings with upright right-side symbols
and separate neutral and non-neutral tones in both writing modes.
Persist the mode through the editor, WASM bridge and Plugin API. Preserve
annotation clearance and canvas, PNG, PDF and SVG geometry.
Validate syntax and selection boundaries, and cover schema, bridge,
rendering, UI and API behavior. Native IME and reader review remain open.
AI-assisted-by: gpt-6
Carry optional span positions through the schema, WASM header, editor,
Plugin API and CSS output. Expose Chinese controls with language defaults
and independent overrides for horizontal and vertical text.
Reserve automatic clearance per side and stack underlines, ruby and
emphasis. Preserve reading groups during decoration edits and retain
canonical position values in DOM editor style data.
Add protocol, geometry, editing and API tests, including the prior Japanese
mixed-font gutter behavior. Verify both WASM targets and live exports.
AI-assisted-by: GPT-6
Support typed character and group readings through the shared ruby model.
Expose Chinese labels, presentation controls and selection guards without
changing Japanese controls or generating pronunciation.
Preserve complete source ranges for glyphs sharing a combining cluster.
Add pinned horizontal and vertical tone-mark fixtures and editor tests.
AI-assisted-by: GPT-6
Cover Chinese regional profiles and Korean wrap modes across fitting
widths, style boundaries, punctuation, numeric units and decomposed Jamo.
Check stable offsets and finite geometry in narrower horizontal boxes,
and keep indivisible punctuation units together in short vertical boxes.
AI-assisted-by: GPT-6
Add named Chinese punctuation presets and a Korean keep-words mode for
horizontal and vertical layout. Preserve the saved spaces-only mode and
keep source offsets stable through safe emergency wrapping.
Apply bounded CJK spacing and justification, expose the new choices in the
sidebar, and document preset bounds and remaining release checks.
Verify renderer, frontend, common and exporter tests, both WASM builds,
and sample save/reload plus PNG, PDF and SVG exports. Existing CI failures
remain documented in the layout notes.
AI-assisted-by: GPT-6
Choose paragraph layout rules from an optional language tag. Pass language
to horizontal and vertical shaping, add basic Chinese punctuation rules,
and add experimental Korean spaces-only wrapping.
Keep text boundaries grapheme-safe and preserve paragraph profiles across
editor splits and merges. Add reproducible font fixtures for localized
glyphs and regression tests for the shared WASM protocol and layout paths.
Validation: renderer, common JVM/JS, frontend and exporter tests pass.
Both WASM targets build. Full CI still reports baseline lint and format
errors and one backend multipart-upload test failure.
AI-assisted-by: GPT-6
Lay out horizontal and vertical Japanese text with kinsoku, ruby,
emphasis, warichu, tate-chu-yoko and proportional font features.
Carry the layout through glyph paths, SVG export and text selection.
Update the Rust and ClojureScript binary formats together, including
the editor bridge and headless exporter. Resolve CJK fallback fonts
and add renderer tests and font fixtures.
When render-wasm is active, generate SVG for the Code inspector and
Copy as SVG from the WASM export path instead of classic SVG codegen.
Add multi-root render_shapes_svg so multi-select stays one document.
Closes#12567
A text image fill was always laid over the whole text box, so a pasted
image span showed another part of the picture in a box of another size.
Text image fills now honour the image transform, as shape fills do: the
image covers the transform rect instead of the whole box. Paste gives
each pasted image span a transform that keeps the image at the same
pixel offset from the span, like the gradients.
The SVG export emits one linked image per span transform, rather than
one per image id, and the style panel keeps the transform it reads back.
AI-assisted-by: claude-opus-5-5
Move the backend JDK to its latest maintenance build within Java 26 and
refresh the dependency versions pinned by hand in the development and
production images.
- Backend: Zulu JDK 26.0.2 to 26.0.2.1. The jlink module list keeps
jdk.management, so the prometheus process metrics stay exported.
- Node.js 24.22.1 in .nvmrc, the development image tarball and the two
plugins deploy workflows that pin it.
- pnpm 12.15.0 in the four Dockerfiles, the 34 packageManager fields and
the eleven lockfiles.
- Development tools: clj-kondo 2026.10.20, Babashka 1.13.226, Pixi
0.83.0, GitHub CLI 2.103.0, AWS CLI 2.38.5 and uv 0.13.5.
- Infra services: RustFS 1.0.3, Mailpit v1.32.0, and Valkey pinned to
8.1.10 instead of the floating 8.1 tag.
Fix SERENA_UPDATE_VERSION in the compose file. A value there makes the
start script reinstall that Serena release on every start, overriding
the one baked into the image; it was replacing 1.7.0 with 1.6.1.
The hardened base image updates are left to Renovate.
Closes#12599
AI-assisted-by: deepseek-v4.1-flash
Signed-off-by: Andrey Antukh <niwi@niwi.nz>
When render-wasm is active, shape exports (single file, ZIP, and Export
boards multi-page PDF) run on the shared worker WASM module so the UI
stays responsive. A progressive PDF session (begin/add-page/finish)
updates progress per page; cancel settles on the client. The server
exporter path remains for the SVG renderer.
Text image shaders are baked into Skia paragraphs at layout time.
touch_shapes_with_image only scanned shape.fills/strokes, so text
spans were never invalidated after store_image. Detect text content
image fills and force a layout rebuild before touching tiles.
Closes#12563
* 🐛 Keep text gradients in place when pasting at the caret
Text gradients are stored as fractions of the whole text box. Paste
copied those fractions unchanged, so in a box of another size the
handles and colours moved away from the pasted text.
Copy from the v3 editor now records where each span with a gradient or
image fill starts in its text box, and the box size, in the clipboard
payload. A new WASM export, text_editor_get_range_origin, gives the
start of any text range.
Paste at the caret finds where those spans start in the target and
rewrites their gradients so the handles keep the same pixel offset from
the span. Auto-sized texts use the box size they get when the editor
closes.
AI-assisted-by: claude-opus-5-5
* 🐛 Rebuild text gradient shaders when the text box resizes
Text gradient and image shaders are built into the cached Skia
paragraphs against the text box size. A resize with no content change
reused those paragraphs, so the colours stayed matched to the old box
until a reload.
The text now remembers the box size its shaders were built for and lays
itself out again when that size changes and a span has a gradient or
image fill. Text with only solid fills keeps its cache.
AI-assisted-by: claude-opus-5-5
* 🐛 Keep text gradients in place when pasting on the canvas
A text selection pasted on the canvas becomes a new text whose box
differs from the source, so its gradients moved like they did for caret
paste.
After adding the new text, lay it out, find where its gradient spans
start and move their gradients to keep the same pixel offset from the
span, in the same undo step. The range origin export now takes a shape
id, so it works on texts that are not being edited.
AI-assisted-by: claude-opus-5-5
* 🐛 Keep text gradients in place when pasting copied texts at the caret
Text shapes copied whole, rather than as an editor selection, reached the
caret paste without any geometry, so their gradients still moved away from
the pasted text.
A shape copy now records where the gradient and image spans of each
selected text start, and the box size, by shape id. Paste joins them in
the order of the joined paragraphs, with no origin for texts copied
without one.
Copied texts each have their own box, so every span origin now carries
its own box size instead of one box for the whole payload.
AI-assisted-by: claude-opus-5-5
* 🐛 Fix render_to_pdf call in the ToUnicode test
The test passed a scale argument that render_to_pdf does not take, so
the render-wasm test build failed to compile.
* 🐛 Draw masked group inner shadows in PDF and multi-element export
The CPU renderer behind PDF and multi-element raster export drew a
masked group's drop shadow but never its inner shadow, so the masked
content came out flat. Wrap the masked content in a layer that draws
the inner shadows over it, as the GPU single export does.
* 🐛 Export every visible inner shadow of a masked group
The workspace skips shadows too small to see at the current zoom, to
render faster. Export has no such need, so draw every visible inner
shadow instead of filtering by scale.
WASM find_guide_at only checked distance on the guide axis, so board-bound
guides stayed interactive outside their board. Respect frame_range in
GuidePool hit testing so interaction matches the drawn solid segment and
the SVG hit area.
Closes#12322
Skia maps ligatures and other substituted glyphs to U+0000 in the PDF
ToUnicode maps. Rebuild those maps from the laid-out paragraphs and
append them as an incremental update.
Closes#12393
Inner shadows were joined into extrect via the drop-shadow filter, so a
Y offset grew the PNG/SVG export surface. Only drop
shadows expand bounds, matching classic export. Closes#12386.
A modified shape is a cached copy, and edits through get_mut did not
reset it. After Enter the preview stops sending modifiers while the size
holds, so the canvas kept painting the old text.
AI-assisted-by: claude-opus-5-5
Headless raster/PDF used the board selrect so overflowing children stayed
cropped, but that also clipped the board's own drop shadow. Use
export_root_bounds: own strokes/shadows/blur without joining children.
Closes#12388
Bump the pnpm pin to 12.14.0 in the devenv and the production images
and stamp the same version into every package.json, realigning the
drift between the devenv (12.9.1) and the rest (12.6.0).
Bump Node to v24.22.0, the newest patch of the Krypton LTS line, in
.nvmrc, the devenv and image Dockerfiles and the plugins-deploy
workflows.
The pnpm arch checksums are those of the GitHub release tarballs, not
of the npm package. Lockfile changes are metadata only.
AI-assisted-by: deepseek-v4.1-flash
* 🔧 Fail CI when render-wasm JS glue exports invoke_*V
Add check-no-invoke-v (path from _build_env) and run it in CI: WASM
after a production frontend build so JS longjmp + SIMD ABI
regressions are caught in the pipeline.
* 🐛 Fail closed in render-wasm invoke_* glue check
* 🔧 Check exporter wasm glue for any invoke_* wrapper
---------
Co-authored-by: Elena Torro <elenatorro@gmail.com>
Forward Cargo arguments as an array and honor explicit profiles.
Build and copy each renderer target once under the repository lock,
with watch rebuilds using the same wrapper.
Select this checkout's compiler artifact and matching generated enum
table from Cargo JSON. Record actual features, profile and output paths,
and allow callers to override publication destinations.
Run the existing Cargo parser tests before the renderer's native tests
and stop on parser failures.
AI-assisted-by: gpt-6.1-sol
Rust 1.91 links emscripten with JS exception handling, which cannot
mix with SUPPORT_LONGJMP=wasm. Bump the devenv to Rust 1.99 (native
Wasm EH) and align EMCC_CFLAGS / SKIA_BINARIES_URL with a Skia
-ljmp-wasm prebuild so +simd128 no longer hits invoke_*V TypeErrors.
With a range selected, Left and Right were moving one character from
the focus instead of collapsing to the selection start or end. Match
standard text-field behavior and keep Shift+arrows extending.
Closes#12207