Skip to content

tui / svg-edit integration - #1512

Open
davrot wants to merge 19 commits into
overleaf:mainfrom
davrot:transplant-image-plugins
Open

davrot wants to merge 19 commits into
overleaf:mainfrom
davrot:transplant-image-plugins

Conversation

@davrot

@davrot davrot commented Aug 27, 2026

Copy link
Copy Markdown

Description

Integration of image modification options
https://ui.toast.com/tui-image-editor
and vector graphics options
https://github.com/SVG-Edit/svgedit/tree/master
into overleaf

TUI:
TUI_1

TUI_2 TUI_3

SVG-Edit:

svg_1 svg_2 svg_3 svg_4 svg_5 svg_6

Related issues / Pull Requests

Contributor Agreement

  • This is human guided AI (mainly qwen3.8; a bit of VS code copilot AI code might be left) and hence can not be copyrighted. You can do with the code what what ever you want.

davrot added 19 commits August 25, 2026 15:33
Adds two editor plugins for Overleaf CE, both fully self-contained:
no iframes, no CDN, no new server-side dependencies.

Diagram editor (modules/drawio)
- .drawio documents hold a plain-text mxGraphModel XML (human-readable,
  diffable, compatible with the free Draw.io desktop app; maxGraph
  registers mxGraphModel/mxCell/mxPoint codec aliases for that).
- Canvas editor renders on @maxgraph/core (Apache-2.0, the mxGraph
  successor that powers draw.io), bundled into Overleaf's own JS.
  Toolbar: rectangle/ellipse/text insert, two-click arrows, freehand
  pencil, delete, undo/redo, zoom/fit, live stroke/fill/width styling;
  keyboard: Delete, Ctrl+Z / Ctrl+Shift+Z / Ctrl+Y.
- Save writes the model back into the document (sync + version history)
  and re-creates companion diagram.png / diagram.pdf (vector, via
  svg2pdf.js + jsPDF in the browser; .svg fallback if PDF fails).
- Edit again / export again works by design: the source is the model
  file itself.

Image editor (modules/toast-image)
- Raster image editing via tui-image-editor (npm-bundled), with a
  self-hosted icon sheet (theme 'common.bi.image' override) so nothing
  is fetched from uicdn.toast.com (blocked by the IDE CSP; the stock
  fetch crashed editor initialization).
- Save uploads the edited image and refreshes the file tree.

Shared wiring
- settings: 'drawio' in textExtensions; createFileModes /
  sourceEditorComponents for the .drawio editor; new fileViewButtons
  module-import slot consumed by file-view-header (the Edit button,
  shared with toast-image); 'toast-image' + 'drawio' in
  moduleImportSequence.
- i18n keys in locales/en.json and frontend/extracted-translations.json
  (kept in sync, alphabetical).
- package.json: @maxgraph/core, tui-image-editor, jspdf, svg2pdf.js.

Tests: vitest unit tests for the pure logic (model normalisation,
companion file names, file-tree helpers, image save/CSRF flow, upload
payloads) — 30 passing.
…rash

Diagram editor (modules/drawio)
- Rebuild the UI on the classic mxGraph grapheditor example layout
  (jgraph/mxgraph javascript/examples/grapheditor www): top toolbar,
  left shape palette (General: rectangle/ellipse/rhombus/cylinder/
  triangle/cloud/actor/text; Connectors: straight/arrow/elbow/
  orthogonal — click, then click start and end element), canvas with
  A4-landscape page sheet, grid, status bar (zoom %, cursor position).
- Palette items support click-to-add and drag-onto-canvas placement;
  new shapes take the current stroke/fill colour and line width.
- Toolbar: New (clear, undoable), Undo/Redo, zoom -/+/Fit,
  Select/Freehand tools, Delete, grid toggle, colour/width controls,
  Save. Keyboard: Delete / Ctrl+Z / Ctrl+Shift+Z / Ctrl+Y.
- Shape styles mirror the grapheditor default stylesheet (white fill,
  black stroke, fontSize 12).

Toast image editor (modules/toast-image)
- Fix 'n is not a constructor' init crash: TUI must be constructed with
  loadButton:false and downloadButton:false (the proven CE+ working
  configuration); with the built-in buttons enabled the bundled build
  crashes at construction. Our toolbar (Save/Close) drives the UI.

i18n: refresh the drawio_* key set in locales/en.json and
frontend/extracted-translations.json (kept in sync).
…mage: fix real TUI init crash

Diagram editor (modules/drawio -> modules/diagram):
- Replace the maxGraph palette UI with the @svgedit/svgcanvas 7.4.2 canvas
  (MIT, zero dependencies, bundled into Overleaf's own JS; no iframe/CDN).
- Native modes: select, rectangle, ellipse, line, circle, pencil (freehand);
  add-text, recolour (fill/stroke/width), to front/back, delete, undo/redo,
  zoom, clear.
- Document format is now the SVG source itself (editable, round-trippable);
  Save writes the SVG back into the document and re-creates companion
  name.png and vector name.pdf (svg2pdf.js + jsPDF) for \\includegraphics.
- Wiring: settings.defaults createFileModes/sourceEditorComponents ->
  modules/diagram; textExtensions += svg (keep drawio for legacy files);
  moduleImportSequence 'drawio' -> 'diagram'.
- Deps: + @svgedit/svgcanvas ^7.4.2, - @maxgraph/core.
- i18n: drawio_* -> diagram_* (en.json + extracted-translations, both files).
- Unit tests updated for the SVG model helpers (33 pass).
- Plan/evidence: references/svg-diagram-svgcanvas-plan.md.

toast-image (TUI crash):
- Root cause proved in tui-image-editor 3.15.3 dist: menu contained 'guide'
  (not in the v3 SUB_UI_COMPONENT registry -> new undefined =
  'n is not a constructor'), and the code called editor.loadImage() which
  does not exist in v3 (public API: loadImageFromFile/FromURL only).
- Restore the old working plugin's exact init: includeUI.loadImage +
  valid menu [crop,flip,rotate,draw,shape,icon,text,mask,filter] +
  initMenu 'filter' + theme + usageStatistics:false, no explicit load call.
- Readiness/save gating: ready only when 'executeCommand' === 'Load' fires
  (ui.initializeImgUrl set) via ui.on invoker events + bounded poll;
  never uploads a blank canvas over an existing project file.
- Retry hygiene: previous instance destroyed + container cleared.
…t/branding

Diagram editor (modules/diagram):
- Register as Overleaf visual-editor provider (visualEditorProviders) so the
  canvas renders in the editor pane instead of under a full-height CodeMirror
  editor; SVG opens in Visual by default, Code/Visual toggle available.
- Create the initial layer (SVG-Edit app does this): the canvas core's
  mouseDown bails when #svgcontent has no <g>, so fresh canvases were
  undrawable and shape tools never received events.
- Heal canvas geometry (NaN zoom/dimensions) before import, drawing and
  export so saved SVGs keep valid width/height and re-imports render.
- Add Text now uses the outline (ink) colour instead of the shape fill, so
  text is visible on white backgrounds; selects the new element.
- svgToPdfBlob: use the svg2pdf.js v2 API svg2pdf(element, pdf, {x,y,width,
  height}) and fit the diagram on A4 preserving aspect ratio; PDF export now
  produces vector PDFs (was 'svg2pdf.js is not available' before).
- Defensive capture-phase wheel-zoom on the canvas (the core's own wheel
  handler assumes a zoom component that is absent in this build).

Image editor (modules/toast-image):
- fetchImpl: bind window.fetch (detached reference threw Illegal invocation).
- Keep TUI's fabric hidden textarea inside the modal so OL's focus trap no
  longer swallows text-tool keystrokes.
- Add explicit Zoom -/+ and Reset (1:1) buttons driving the TUI zoom
  component's real zoom() API (built-in icons only toggled wheel-zoom mode).
- Hide TUI's canvas header (brand logo + Load/Download) by structure.

i18n: add reset_zoom (zoom_in/zoom_out already existed).
…(iframe + bridge)

- Vendor svgedit 7.4.2 (dist/editor) as a same-origin static subtree at
  services/web/public/static/svgedit/ (Editor.js, svgedit.css, images,
  components, extensions, LICENSE); assets + runtime extension imports
  resolve against /static/svgedit/.
- Host page (embed.html+css/js): full SVG-Edit UI; ext-storage excluded
  (no prompt/cookie/auto-load/beforeunload), no_save_warning off,
  branding neutralised (home item removed, main menu -> 'Diagram',
  'Created with' comment stripped from serialised output).
- Bridge: window.__olSvgEmbed { ready, load(svg), getSvg(), onChanged }
  (same-origin, debounced); parent keeps the SVG document in CodeMirror
  (OL sync/history unchanged) and drives the Save (PNG + vector PDF
  companions via svg2pdf.js/jsPDF).
- .svg opens in Visual mode by default (showVisualForFile: provider-
  claimed files default to visual when no preference stored).
- Drop @svgedit/svgcanvas dependency (no longer imported by web code);
  svgDimensions() helper + tests; i18n keys; README rewritten.
- toast-image: silence TUI __hostObserver type error (teardown of the
  text-editing fix that ships in this build).
…expose TUI fabric hook for E2E

SVG-Edit's context menus (se-cmenu_canvas-dialog, se-cmenu-layers) are
positioned at cursor page coordinates clamped by screen.width/screen.height
(the host OS screen — standalone svg-edit runs full-window). Embedded in
the Overleaf editor pane that clamp is wrong: the menu lands far from the
cursor or past the iframe's right/bottom edge where it is clipped and its
items become unreachable.

Fix in the host page (embed.js, no vendored-bundle edits): a document-level
contextmenu/click listener (bubble phase) runs after the bundle has written
its inline top/left; it then re-anchors each visible menu at the cursor,
pinning flush to the iframe viewport edge when it would overflow.

toast-image: expose window.__olTuiFabric() so E2E can verify keystroke
capture on the real fabric canvas (read-only debug hook, removable).
TUI inverts the selected tool item to a white background
(.tui-image-editor-item.active { background-color:#fff }); the theme had
set menu.activeIcon.color to #fff, so the active icon was white on white
and invisible.

- theme: menu.activeIcon.color #fff -> #1e1e1e (matches TUI's own intent:
  standard theme uses a dark active icon on the white active background)
- CSS belt in toast-image-editor.css: force dark icon (svg use fill/stroke)
  and dark label on active menu items, so a future theme regression cannot
  hide the selection again
…r-fs-access)

Every extension module statically imports ../_virtual/_vite/preload-helper.js
which was absent from the vendored tree, so all extensions except
self-contained ext-markers 404'd on load and the panning/shapes/
polygon-star/eyedropper/connector/layer-view/grid tools were missing.
ext-opensave additionally needed its node_modules browser-fs-access
bundle. Both files restored byte-identical from the official 7.4.2 dist;
verified via headless A/B test that the full reference toolbar set now
loads.
…tch reference

ext-eyedropper inserts at a fixed left-bar index while ext-connector
appends, so their relative order is an async race. The reference build
shows connect-then-eyedropper; the rebrand step now enforces that order
after the extensions land.
…olygon/connect/eyedropper)

The extension inits resolve asynchronously and each inserts by fixed
index or append, so the tail order is a race. Broaden the rebrand
normalisation from the last two buttons to the whole contested tail
(tool_text < tool_shapelib < tool_image < tools_polygon < tool_connect <
tool_eyedropper); verified 14/14 identical to svgedit.netlify.app.
…te inserts

An extension can insert (or re-insert by fixed index) its button after a
rebrand run and undo the normalisation, leaving the tail order to the
race. A MutationObserver on the left bar's direct children re-runs the
normalisation 120ms after the last change; the pass is a no-op once the
order is stable, so it converges. 3/3 boot runs now match the reference
sequence exactly.
- prune the opensave menu's disk-backed ops (New/Open/Save/Save as):
  New would sync a cleared canvas into the document, Open would
  overwrite it with a local file, Save/Save-as are browser downloads
  that masquerade as project saves. Import (data-URI/inline SVG embed)
  and drag-and-drop are kept.
- the removed items' bare N/S shortcuts keep firing through
  document-level listeners that survive detachment, so a capture-phase
  keydown blocker replaces them (verified: no dialogs, no orphan
  errors, T still works).
- a MutationObserver on the editor menu re-prunes if ext-opensave
  attaches its items after the last rebrand timestamp.
- replace the core Image tool (typed URL/path prompt -> broken
  <image href> references) with a local-file-dialog tool (user
  decision, option a): SVG inlines, raster embeds as data URI —
  portable content in the project document.
…erbox)

The companion PDF was always A4 with the diagram fitted inside, which
changed the apparent size of the document (a 400x300 px diagram came
out on a 595x842 pt page) and distorted \\includegraphics sizing. Now
the page is exactly the SVG's own size (px -> pt at 96 dpi, zero
offset), with the A4 margin-fit kept only as a fallback for SVGs
without usable dimensions. Pure helpers (pxToPt, pdfPageSize,
fitContent) extracted and unit-tested (31/31 module tests green).
diagram: record the Overleaf-owned-document hardening (pruned file
ops, key blocker, image-tool file-dialog decision with option b as a
future note, left-bar order normalisation + verification against the
live reference) and the new PDF page-size rule.
toast-image: document the TUI text-input focus-trap fix (fabric
hidden-textarea container + reparenter), the visible selected-tool
button theme fix, and the __olTuiFabric debug hook.
…t 96-dpi px)

The live canvas of the A4-shaped test diagram is 722.75x510.73 units and
the user expected the companion PDF at that document size; the px->pt
(0.75) mapping shrank it to 542x383 (about 60% of the A4 reference).
SVG-Edit's coordinate space is points (standard A4 canvas = 842x595), so
the page now maps 1 user unit to 1 pt and the 96-dpi conversion is gone.
31/31 module tests green.
…pages otherwise)

jsPDF 4.x defaults to portrait and normalises a custom [width, height]
pair into portrait — a 722.75x510.73 (landscape) canvas came out as a
510.73x722.75 page, i.e. the PDF was transposed relative to the diagram.
The export now sets orientation explicitly (w>=h → landscape, else
portrait); both branches verified against jsPDF 4.2.1 behaviour, with a
regression test pinning the swap quirk. 32/32 module tests green.
…target size)

Bug hunt: the PNG was created at 2× canvas size but the SVG raster was
drawn at its natural 1× size (ctx.drawImage(img, 0, 0)), leaving the
diagram in the top-left quadrant with empty space around it — while the
PDF companion renders the same content 1:1 (verified: canvas content
bbox 17.6..639.5 x -5..250.4 units = 622x255.6; PDF shows it at
14..640 x 0..252 pt = 87%x49% fill; the PNG showed only 43%x25%).
Drawing with an explicit destination (img, 0, 0, width, height) makes
the PNG 1:1 the diagram canvas — no additional space around it, same
content placement as the PDF.
…er on the canvas

The wired Image-tool file-dialog handler called finish(100,100) on 'error',
adding a 100x100 <image> with no href. Now the error path reports the failure
without mutating the canvas.
The core add path (addSVGElementsFromJson) appends the element but never
touches the undo stack, so a PNG imported via the Image tool stayed on the
canvas through Undo/Redo. Register the finished element in the editor's own
history (BatchCommand + InsertElementCommand + addCommandToHistory) — the
same mechanism the core SVG-file import (importSvgString) uses — and notify
the changed listeners like the core flow, so the parent document stays in
sync. Best-effort: if the API shape ever changes, the import still lands.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant