The engine already supported style.paragraph.edit and
style.paragraph.delete, but the UI never exposed a delete action and
edit was only reachable by double-clicking a style row, with no
visible affordance.
- Add "Edit Paragraph Style…" and "Delete Paragraph Style…" entries to
the paragraph style row context menu (hidden for built-in styles).
- Add a new deleteParagraphStyle confirmation dialog that detects
whether the style is currently in use and, if so, lets the user pick
a replacement style (defaulting to [Basic Paragraph]) for affected
text before deleting.
- Add engine tests covering paragraph style edit (attribute merge and
rename propagation), based-on cycle rejection, delete with text
reassignment, and rejection of built-in style deletion.
- Add UI-level tests covering the edit dialog persisting changes and
the delete dialog removing a style and reassigning text, including
the default replacement style.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
New designcraft-pptx crate (L3) and file.exportPptx command (File ›
Export PowerPoint…, CLI --export x.pptx). Each page becomes a slide of
native objects: shapes with fills, gradients, strokes and effects,
pictures cropped like their frames, native tables, and text boxes.
Text keeps DesignCraft's composition by default: composed lines end in
line breaks, justified spacing becomes character spacing, and leading
becomes percentage line spacing (PowerPoint rounds exact spacing to
whole points; its 100% line is 1.2 em for every font). Threaded
stories split at their frames and wrapped lines get their own boxes.
lineBreaks: reflow writes one box per frame in native columns instead.
TrueType fonts are embedded as Embedded OpenType when their licences
allow. Objects PowerPoint can't represent (placed PDF/EPS, type on a
path, some effects) are rasterised through the existing
rasterize_where path.
Checked in PowerPoint for Mac and LibreOffice: on the sample magazine,
word positions match DesignCraft's PDF to within 0.8 pt horizontally
and 1.6 pt vertically.
Placing an RTF or Tagged Text file dropped every character outside the
Basic Multilingual Plane: emoji, mathematical letters and the rarer CJK
ideographs such as 𠮷 (U+20BB7, used in Japanese names). The text
around them came in, so the loss was silent. It also broke our own round
trip: Export Text writes these characters correctly and Place lost them.
Both formats carry such a character as a UTF-16 surrogate pair, two
units in a row: \u-10179?\u-8704? in RTF, <0xD83D><0xDE00> in Tagged
Text. Both readers passed each unit to char::from_u32 on its own. Half a
pair is not a character, so each half was dropped.
A small shared Utf16Units joins the units: a first half waits for the
second, and the pair becomes one character. Half a pair with no partner
is still left out, as before.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A placed PNG, GIF or WebP that is cut off (an interrupted download) or
has a bad checksum made the whole PDF export fail with
"PDF writer error: Image(Image(ImageRepr {..}), None, "unexpected end of
file")", which names no file. A damaged TIFF or BMP in the same place is
skipped with the warning "image `name` could not be decoded and was
skipped", and the rest of the document is exported.
The exporter decides whether an image can be embedded by asking krilla
to open it. krilla opens PNG, GIF and WebP lazily: it reads the header,
accepts the file, and decodes the pixels only when the PDF is written,
where an error ends the export. TIFF and BMP are decoded by us first, so
they took the skip path.
lossless() now decodes the image first for every format. A file that
does not decode returns None and takes the existing skip-with-warning
path. This costs one extra decode per PNG, GIF or WebP asset: the sample
magazine (two PNGs, 820 KB) exports in 0.19 s instead of 0.17 s in a
debug build.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
File ▸ Close, its shortcut (Ctrl/Cmd+W) and the tab's × closed a document
with unsaved changes at once. Closing also discards the document's
recovery data, so the edits were gone for good.
The three user paths now go through menus::close_document. A document
with unsaved changes opens a dialog with Save, Don't Save and Cancel:
- Save runs the same Save the menu does, then closes. If the document
is still unsaved afterwards (the file picker was cancelled), it stays
open.
- Don't Save closes it.
- Cancel (or Escape) does nothing.
A document with nothing to save closes as before. The engine command
file.close is unchanged and never asks, so scripts, MCP and the control
channel's engine.execute keep working unattended. ui.menu.invoke without
params acts like the menu item, as it does for other dialogs.
Quitting the app is not covered here. Recovery data survives a quit, so
unsaved documents come back on the next start.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
In every workspace that shows the Control panel (Advanced, Typography,
Printing and Proofing, Book), the second row of fields was cut off at the
panel's lower edge: Y and H in object mode, leading, tracking and the
indents in text mode.
The panel lays its groups out in a row that centres each child
vertically. The two-row grids were placed as if they were one row tall
and then grew downward, so they started 13.5 pt below the panel's top
instead of at it and ended 5.5 pt past the 59 pt panel.
The row is now top-aligned, which is where the font menus, the
separators and the reference-point proxy already sat.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
DrawCraft was renamed VectorCraft, and its repository is now
storytold/vectorcraft. Three places still used the old name:
- AGENTS.md and CLAUDE.md told contributors and agents that the sibling
checkout lives at ../drawcraft. A clone made from the README link lands
in ../vectorcraft, so that path does not exist.
- The About window's sibling list used the app-page slug "drawcraft".
getartcraft.com/apps/drawcraft still redirects to /apps/vectorcraft,
so the link worked, but the README (which the list says it matches)
uses /apps/vectorcraft.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
In the browser, pressing AltGr while editing text on the canvas inserted
the word "AltGraph" into the story.
eframe's web backend (0.36.2, web/input.rs text_from_keyboard_event)
turns a keydown into a text event unless the key is on a fixed list of
names. Browsers report many more named keys than that list holds:
AltGraph, Dead, Unidentified, the media keys. Each one arrived as text.
Panel text fields are not affected, because a focused egui text field
takes its text from the browser's input events instead of keydown.
The web shell now drops a text event that is a key name (ASCII letters
and digits, starting with a capital, longer than one character) unless
an egui text field has the keyboard. One key press never types such a
word; a text field's input events can, so they are left alone.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Three commands took a count from their parameters and used it unchecked.
Parameters come from MCP, the control channel and scripts, so the count
is hostile input (never-crash: cap input-sized allocations, checked
arithmetic).
- layout.createGuides made one or two guides per row and column with no
upper limit: 30 million rows took 1.2 GB, and 2^32 - 1 would exhaust
memory, which aborts the process past the command guard. A count over
2^32 wrapped to a small one. Both counts are now capped at 100.
- edit.stepAndRepeat multiplied rows by columns as usize. In debug builds
the overflow panicked; in release builds 2^32 x 2^32 wraps to 0, passes
the 1000-copy limit and starts a grid of 2^64 copies. The product is
now checked, and a grid that large falls back to `count` copies like
any other grid over 1000.
- file.new stored any column count in the page (4 billion, or a wrapped
value past 2^32). Page::column_rects builds one rect per column each
time the columns are laid out. The count is now clamped to 1..=216,
the range layout.marginsAndColumns already uses.
Each case has a regression test that failed before the fix.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Rust 1.98 adds the clippy lint chunks_exact_to_as_chunks. It flags two
loops in the PSD decoder, so `cargo xtask ci` stops at the clippy step on
unmodified main for anyone on current stable.
Both loops now use as_chunks / as_chunks_mut (stable since 1.88, below the
workspace rust-version 1.90). The chunks are arrays, so the try_from that
could never fail goes away too. Decoded pixels are unchanged; the existing
PSD tests cover both paths.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Restore the previous preset and character-class coverage. Remove regional punctuation and Tsume policy changes while retaining cluster, tracking, and spacing-allocation repairs. Preserve finite unsupported rows for diagnostics instead of rejecting whole documents.
Resolve imported composite fonts by resource identity and preserve literal
display names. Export AppliedFont as an object reference instead of registering
a composite definition as an ordinary font family.
Share composite lookup and typed font usage between composition, font inventory,
and preflight. Include nested cells and notes, and report only component fonts
selected by the document's characters.
Add synthetic coverage for reference round trips, footnote character mapping
and metrics, nested font inventory, component replacement, and undo.
Resolve forward story references before importing embedded frames and groups.
Compose embedded stories, retain anonymous-story objects for screen and PDF
rendering, and invalidate layout caches when embedded frame geometry changes.
Measure inline objects by geometry, fill open contours, reject cyclic frame
references, and distinguish unplaced stories from located overset text.
Add synthetic round-trip, composition, preflight, pixel-rendering, and PDF
regressions. Update the roadmap with the supported import and rendering paths.