Support exporting a board to multiple formats (PNG, PDF, JSON) #203
Labels
No labels
prio_critical
prio_low
type_bug
type_contact
type_issue
type_lead
type_question
type_story
type_task
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
lhumina_code/hero_whiteboard#203
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
There is currently no way to export a board. Users should be able to export the current board to common formats so work can be shared, archived, or used outside the app.
Scope
Add an Export action (toolbar/menu in the board view) offering at least:
.jsonfile, suitable for backup and for a future import feature.All exports are client-driven where possible (the canvas is Konva-based, so PNG can be produced from the stage; PDF can wrap that image). JSON export should reflect the same data the server returns for the board so it round-trips cleanly later.
Requirements
Acceptance Criteria
Notes
The whiteboard frontend is vanilla JS modules under
crates/hero_whiteboard_admin/static/web/js/whiteboard/with a Konva stage set up incanvas.js. Any third-party library for PDF generation should be vendored/served as a static asset consistent with how existing frontend libraries are delivered (no external CDN at runtime). Implementation should reuse existing object/connector data access already present in the frontend rather than introducing a parallel data model.Implementation Spec for Issue #203
Objective
Add an in-board Export control that lets a user export the current board to PNG, PDF, or JSON. PNG/PDF capture the full content extents (bounding box of all objects + connectors, not just the viewport) at reasonable resolution, restore the user's original view afterward, and are produced entirely client-side. JSON serializes the same board metadata + objects + connectors the server returns, suitable for a future import. No new network listeners or server endpoints are added.
Requirements
Files to Modify/Create
crates/hero_whiteboard_admin/static/web/js/whiteboard/export.js— replace the existing singleexportPngstub with full PNG/PDF/JSON logic (full-content capture, transform save/restore, filename sanitization, embedded minimal PDF writer).crates/hero_whiteboard_admin/static/web/js/whiteboard/app.js— addgetBoardId/getBoardName, replaceexportPngwithexportBoard(format)delegating toWhiteboardExport, keepexportPngalias.crates/hero_whiteboard_admin/static/web/js/whiteboard/canvas.js— add a minimalsetExportMode(bool)accessor (hides grid/cursor/ui layers during capture).crates/hero_whiteboard_admin/templates/web/board.html— replace the single "Export PNG" navbar button with an Export button + a small PNG/PDF/JSON menu. No script-tag change (export.js already loaded before app.js).Implementation Plan
Step 1: Add export data accessors in app.js
Files:
app.jsDependencies: none
getBoardId()returning the module-privateboardId.getBoardName()reading#board-nametextContent (present in board.html and board_view.html).exportPngto delegate; addexportBoard(fmt)->WhiteboardExport.exportBoard(fmt); keepexportPngalias. Export all fromWhiteboardApp.Step 2: Add canvas export-mode accessor
Files:
canvas.jsDependencies: none (parallelizable with Step 1)
setExportMode(on)togglinggridLayer/cursorLayer/uiLayervisibility; expose inWhiteboardCanvasreturn object (mirrors existing layer getters).Step 3: Full-content PNG capture with transform save/restore
Files:
export.jsDependencies: Steps 1, 2
computeContentRect(): after neutralizing stage transform, unionobjectLayer.getClientRect()andconnectorLayer.getClientRect(), add ~24px padding; empty board -> toast "Nothing to export" and abort.withFullContentCapture(fn): save{x,y,scale}; set scale{1,1}+ position{0,0};WhiteboardCanvas.setExportMode(true); runfn; infinallyrestore scale/position, callWhiteboardCanvas.syncScaleFromStage(),WhiteboardCanvas.drawGrid(),setExportMode(false).buildPng(): adaptivepixelRatio(2 normally; 1 when area > ~4e6 px²; reduce further so each dimension*pr ≤ 8000);stage.toDataURL({x,y,width,height,pixelRatio,mimeType:'image/png'}); composite onto a white-filled offscreen canvas; return{dataURL,width,height}.Step 4: JSON export reusing server data shape
Files:
export.jsDependencies: Step 1
buildJson():board={id: getBoardId(), name: getBoardName()};objectsviaWhiteboardSync.serializeForServer(entry.group)overWhiteboardObjects.getAllObjects()(exact server RPC payload shape);connectorsfromWhiteboardConnectors.getConnectors()mapped to{id, from_id, to_id, line_style, stroke, stroke_width}; wrap{board, objects, connectors, exported_at, version:1}; download asapplication/jsonBlob.Step 5: Minimal hand-rolled single-image PDF writer
Files:
export.jsDependencies: Step 3
toDataURL('image/jpeg',0.92)), embed with/DCTDecode(no zlib needed). Build a 5-object one-page PDF (Catalog, Pages, Page with MediaBox = image px at 1px=1pt, Image XObject/DeviceRGB, content streamq W 0 0 H 0 0 cm /Im0 Do Q) with a byte-accurate xref; assemble asUint8Array; download asapplication/pdf. Justification: a one-page single-image PDF is ~40 lines and avoids vendoring jsPDF/pdf-lib (~350KB) — honors the no-CDN/vendored constraint with zero added asset weight.Step 6: Filename sanitization, download helper, progress/disable
Files:
export.jsDependencies: Steps 3, 4, 5
safeName(ext): board name (fallbackboard) +-id, lowercased,[^a-z0-9._-]+→-, collapse/trim, cap ~80, add extension.download(blobOrUrl, filename)anchor helper (revokeObjectURL after click).exportBoard(fmt): re-entrancy flag_busy; disable#export-btn+ spinner;WhiteboardApp.showToastfor "Exporting…"/done/error; defer heavy raster via rAF+setTimeout so disabled state paints;try/finallyalways re-enables.Step 7: Wire the Export UI into board chrome
Files:
board.htmlDependencies: Step 6
btn btn-smtrigger#export-btn(bi bi-download) + a small menu: PNG/PDF/JSON callingWhiteboardApp.exportBoard('png'|'pdf'|'json'), matching adjacent navbar button/menu patterns. The control lives in.wb-navbar, which the existingbody.wb-presentingCSS rule already hides during presentation — no extra CSS. Not added to read-onlyboard_view.html.Acceptance Criteria
boardmetadata,objectsviaserializeForServer, andconnectors, matching server data shape.wb-presentingrule.Notes
static/web/+templates/web/change; these are embedded byrust_embedinsrc/assets.rs, socargo build --release -p hero_whiteboard_adminmust be re-run (no edit to assets.rs needed since no files added/renamed). Verify via the admin Unix socket.WhiteboardSync.serializeForServer+WhiteboardConnectors.getConnectors()— no parallel serializer.finally, includingsyncScaleFromStage()+drawGrid()so the zoom indicator and grid match the restored view.board.html.wb-navbar; presentation auto-hides it via existing CSS.Test Results
Note: this feature is JS/HTML-only (no Rust source changed); the workspace lib tests are run as a regression guard.
Implementation Summary
Board export to PNG, PDF, and JSON, fully client-side.
Changes
crates/hero_whiteboard_admin/static/web/js/whiteboard/export.js— replaced the stub with the full export module: full-content bounding-box capture (computeContentRect), stage transform neutralize/restore in afinally(withFullContentCapture), white-composited PNG with adaptivepixelRatio(2, reduced for very large boards, dimension-capped at 8000px), a hand-rolled single-image PDF writer (JPEG via/DCTDecode, byte-accurate xref — no third-party library), JSON export reusingWhiteboardSync.serializeForServer+WhiteboardConnectors.getConnectors(), sanitized filenames (board name + id), re-entrancy guard, and progress/disable feedback.crates/hero_whiteboard_admin/static/web/js/whiteboard/app.js— addedgetBoardId(),getBoardName(), andexportBoard(fmt)delegating to the export module; keptexportPng()as a backward-compatible alias.crates/hero_whiteboard_admin/static/web/js/whiteboard/canvas.js— addedsetExportMode(on)to hide the grid/cursor/UI layers during capture (object + connector layers stay visible).crates/hero_whiteboard_admin/templates/web/board.html— replaced the single Export button with an Export control (#export-btn) and a PNG/PDF/JSON menu, matching the navbar's existing inline-style button pattern (no Bootstrap JS is loaded in this template). The control lives in.wb-navbar, which existing CSS already hides during presentation.Behavior
{ board, objects, connectors, exported_at, version }using the server's data shapes for round-tripping into a future import.board_view.htmland auto-hidden during presentation; no new network ports/listeners or server endpoints.Test results
cargo test --workspace --lib(matches CI): compiled cleanly, no failures (feature is JS/HTML-only; run as a regression guard).node --checkpassed forapp.js,canvas.js,export.js.export.js,app.js,canvas.js, and the board page verified to contain the new code and the Export control.Notes