Commit Graph
3612 Commits
Author SHA1 Message Date
andeecollardandClaude Opus 5.5 caabf08f3e Image Trace: Shapes keep abutting neighbours watertight
With Method Abutting and Shapes on, a region recognised as a shape had its
outline replaced by the exact shape, but the region across that edge kept
its fitted curve unless it was a hole filled by one colour, and even then
the hole was recognised on its own, with its region's combined hardness.
A shape touching a junction (a third colour, or its own colour at a
corner) or the image's border was replaced on one side only, so abutting
colours parted or overlapped along the shared edge, and a square on the
image's corner could reach past the frame.

Cause: Layers::layer recognised an island with no holes
(`[only] => shapes::recognise`) regardless of what lay around it, and a
hole only by `one_colour_within`, each from its own outline and hardness.

Fix: under Abutting a shape is applied only where both faces take it
(`Layers::shared_shape`): the loop has no junction on it and does not
touch the border, the island has no holes and the hole holds one colour,
and the shape stays inside the image. The shape is recognised from the
outline turned one way and started at its leftmost anchor, with that
loop's own hardness, so the island and the hole it fills get the very same
shape. Overlapping is unchanged.

Test: shapes_keep_abutting_neighbours_watertight traces three code-drawn
anti-aliased colour images (a disc in a square plus a disc across its
edge, shapes on the border, tilted tiles) and checks every outline lies
within 0.001 px of a neighbour and a 4 x 4 per-pixel grid is covered
exactly once. Before: gaps of 0.50, 15.1 and 0.73 px, 183 and 276 points
uncovered or doubled; after: 0 px, 0 points. Isolated shapes are still
recognised.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018tBcdwNhptqtmX32JCGFzB
2026-10-17 14:47:03 +07:00
andeecollardandClaude Opus 5.5 989768207f Image Trace: Flat Logo traces line art instead of a blank result
Flat Logo found its colours only from solid pixels (a uniform 3 x 3
neighbourhood). Line art on paper has no solid area but the paper's: a
stroke one or two pixels wide never fills one, so its colour was a handful
of pixels among tens of thousands of white and was cut as noise (under
0.1% of the solid pixels). The palette came back empty and trace_logo
returned an empty TraceResult, which imageTrace.make reported as a success
with paths 0, anchors 0, colors 0.

Cause: logo::palette::auto_palette had no way to find a colour that has no
solid area, and logo::pipeline::trace_logo treats an empty palette as
nothing to trace.

Fix: when the solid pixels give no colour but white, auto_palette reads
the colours from the strokes' cores instead (line_art_palette): the opaque
ink pixels at least as far from white as each of their four neighbours,
the middle of each stroke where it is least blended with the paper.
Two guards keep that fallback to line art:
- Ink is a colour at least 48 below white in one channel, and the page
  must have ink on at least 1% of its opaque pixels. Light grain and a few
  sparse marks on otherwise blank paper stay blank, as plain white does.
- The lighter shades along a stroke's anti-aliased edge are local maxima
  of their own; a colour that lies on the line from white to a darker
  colour of the palette (within 20) is a blend of that ink with the paper
  and is folded into it (fold_fringes), the ink keeping its colour.
The rest of the pipeline (unmixing, the flat-colour check and its NotFlat
refusal) is unchanged, and images with a solid colour other than white
never reach the fallback.

Tests (code-drawn inputs):
- thin_line_art_on_paper_is_traced_not_blank: four 2.4 px dark strokes
  and two red marks on white. Before: palette [], no paths. After: dark
  and red paths along the middle of every stroke and mark.
- anti_aliased_coloured_lettering_has_only_its_inks: rings, slants and
  bars 1.6 px wide in red, blue and green. Without the fold: six colours
  besides white; after: exactly the three inks.
- sparse_marks_on_paper_are_not_promoted_to_colours: 24 grey 12 x 2 px
  marks of 239, 223 or 191 on a 256 x 256 page (under 1% ink). Without
  the guard: palette [white, #efefef]; after: empty palette, no paths.
- grainy_blank_paper_has_no_ink: paper grain 243-255 traces to nothing.
- edge_shades_fold_into_their_ink_and_other_colours_stay (unit).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018tBcdwNhptqtmX32JCGFzB
2026-10-17 14:46:21 +07:00
andeecollardandClaude Opus 5.5 6bbf30e52a Image Trace: a simpler contour walk with no precomputed direction table
contour::trace_mask built a table of outgoing directions for every
pixel corner in a raster pass and then followed it. It now works out
each turn during the walk from the two pixels ahead of the current
corner: right if the one ahead on the right is outside, straight if the
one ahead on the left is, else left (so at a saddle it still turns
right and diagonal pixels stay apart). Loop starts are found by
scanning the pixels for a top edge on the outline, with one flag per
pixel for a top edge already walked. The walk is bounded by the grid
size, and a mask of the wrong length now gives no loops instead of
failing an assert_eq!.

The output is identical: the_walk_equals_the_stored_direction_walk
compares it with a test-only copy of the previous walk on all 65,536
4x4 masks and 400 seeded 23x17 masks.

Time (best of 7, release, 4 cores): the walk alone on 2000x2000 masks
19.1 vs 26.6 ms (smooth pattern), 81.3 vs 93.2 ms (12% random pixels),
223.1 vs 238.1 ms (50% random); whole 1000x1000 traces 73.1 vs 77.1 ms
(Black and White), 249.6 vs 251.7 ms (Color, 6 colours), 214.1 vs
218.6 ms (Color, 6 colours, Noise 1), same segment counts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018tBcdwNhptqtmX32JCGFzB
2026-10-17 14:46:21 +07:00
Marco Arnulfo 2f24495ffe Color effects recolour the points of a freeform gradient (fixes #2390) 2026-10-17 14:46:21 +07:00
jmvergesandClaude Opus 5.5 1946ffca9c #2405: an envelope's mesh in an .ai isn't read as art
An envelope is written on `%_` lines as a group holding the mesh that shapes
it, with the distortion (`AIDeformStyle`, `DeformValue`) in the group's
dictionary; the distorted art the PDF part draws follows on plain lines. The
reader kept that group, so the mesh drew as a grey gradient mesh behind the
art. A group on `%_` lines that holds only meshes and carries a distortion is
now dropped; a mesh with no distortion is still art.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P6znVFRqGqvB9f7njwZ46r
2026-10-17 14:46:20 +07:00
dotCooCoo 3f81c70ade .ai import: type keeps its own opacity and blend mode
A .ai or EPS opened from its editing data lost the opacity and blend
mode of its text objects. text_slot made the slot without the graphics
state's opacity and blend mode, which paths, meshes and images take, so
an Xy before a text object was lost. An Xy after it reached the slot,
but fill_slots then put type made from the text document in the slot's
place with only its visibility and name, and style_marker dropped the
transparency that a text object with an appearance carries on its look.

text_slot now takes the state's opacity and blend mode. fill_slots
gives made type the slot's opacity, blend mode, isolation and knockout.
The page's type gets them too, in place of its own transparency when
that is none, the slot's, or the slot's combined with that of the
objects around it; otherwise the page's type and the strokes around it
keep their own (a fill's, for example). When the type has transparency
and comes with other art (the strokes the page draws around it, or page
type that goes with it), a group of them takes the transparency. A text
object with an appearance takes the transparency from its look; where
the look draws more than the type and has transparency, the look and
the type go into a group that has it, and that group is not taken as
the look of a later object. The import fuzz's .ai tokens now include a
commented text object, 1 /FreeUndo , and a group's state marker
(6 () XW).

Docs: docs/mcp.md, docs/target-app-parity.md.
2026-10-17 14:46:20 +07:00
dotCooCoo f3072fc748 Paths: Shift keeps a reshaped segment's handles perpendicular and equal in length
Shift pressed while dragging a segment had no effect on the curve:
Direct Selection's curved-segment and spine-segment drags, the Anchor
Point tool and the Pen with Alt sent the pointer offset alone, and the
segment bent freely. The reference app keeps the handles in the
perpendicular direction and of equal length while Shift is held.

path.reshapeSegment and object.blend.spine.reshapeSegment take
perpendicular (default false, which keeps today's behavior). With it,
both of the segment's handles stand at right angles to the line between
its anchors, on the same side and at the same length, and the point at
t lies as far from that line as the moved point does. A smooth anchor
at either end stays smooth, as it does in a drag without perpendicular,
and a segment whose anchors coincide bends as it does without
perpendicular. With Direct Selection, Shift pressed once a curved
segment's drag has started sends perpendicular; Shift held at the press
still toggles the path's selection, as before. Direct Selection's
spine-segment drag and the Anchor Point tool's segment drag send Shift
as perpendicular too, so the Pen with Alt does as well. A straight
segment dragged with Direct Selection already moves at 45° steps with
Shift.

Docs: docs/mcp.md, docs/target-app-parity.md.
2026-10-17 14:46:20 +07:00
dotCooCoo 0e26b258eb Fields: Shift+Enter applies a panel field's value and keeps the field active
Shift+Enter in a panel or Control bar field ended its editing, as Enter
does. The reference app's default keyboard shortcuts list "Apply value
and keep text box active" under Use panels and "Apply a value and keep
focus in edit field" under Transform panel, both Shift+Enter on Windows
and Shift+Return on macOS.

- A focused single-line field of the shared widgets (num_field,
  mixed_field, plain_field, range_field, spin_field, spin_field_auto,
  spin_plain, text_field, exact_text_field, opt_field and hex_field)
  takes Shift+Enter before its text box reads it, applies the text as
  Enter does and keeps the focus. In the next frame it shows the value
  as it now is, all selected, so typing replaces it.
- A text box of several lines keeps Shift+Enter as a line break.
- While a dialog is open, Shift+Enter applies the field's value and
  confirms the dialog, as before. The app sets widgets::set_dialog_open
  at the start of each frame.

Docs: docs/target-app-parity.md.
2026-10-17 14:46:20 +07:00
Marco Arnulfo 877aac130f EPS import: an Illustrator 3 file without layers opens its paths on one layer (fixes storytold/photocraft#6036) 2026-10-17 14:46:20 +07:00
dotCooCoo d7026367f7 Paragraph: Reset Panel also resets the Hyphenation options
The Paragraph panel menu's Reset Panel turned Hyphenate off but left
the Hyphenation options: Words Longer Than, After First, Before Last,
Hyphen Limit, Hyphenation Zone and Hyphenate Capitalized Words. A
paragraph kept its custom hyphenation limits after a reset.

Reset Panel now also sets those six options to the values of
HyphenationOptions::default(), in the same text.setFormat call. The
reset paragraphs then save no hyphenation options.
2026-10-17 14:46:20 +07:00
dotCooCoo e37fbe3849 Effects: Smart Blur's Radius widens the edge search in Edge Only and Overlay Edge
Smart Blur's Edge Only and Overlay Edge modes marked a pixel when one
of its four nearest pixels differed from it by more than Threshold,
and Radius changed only the blur. The reference app's documentation
says that Radius sets the size of the area searched for dissimilar
pixels.

A pixel is now on an edge when one of the pixels the search reads
within Radius differs from it by more than Threshold and its
channels, alpha included, add up to at least that pixel's. The search
picks its neighbors as the Normal blur does, over the whole pixels of
Radius and within it, and always reads the four nearest pixels.
A color step gives a line about as wide as Radius on the side with
the larger sum, and a Radius of one pixel or less gives the same
edges as before. The edge modes' reach() includes the blurred copy
that a wide Radius reads.
On a 2,000 by 2,000 px image, Edge Only takes 0.49 s at Radius 10 and
0.34 s at Radius 100, and Normal takes 0.73 s and 0.39 s. Normal
draws the same pixels as before.

Docs: docs/target-app-parity.md.
2026-10-17 14:46:20 +07:00
Marco Arnulfo 9739eeb333 AI import: a compound path whose subpaths' group is marked like a look keeps its holes (fixes #980) 2026-10-17 14:46:20 +07:00
bflatastic 77f7f02acc Docs: file-format-parity keeps one EPS and one PostScript row 2026-10-17 14:46:20 +07:00
dotCooCoo a070d83be3 EPS: images encoded with CCITTFaxDecode draw
The PostScript interpreter that reads EPS and PostScript files had no
CCITTFaxDecode filter. Reading a file whose image data went through it
stopped at an error naming the filter, and the file opened from a
fallback, such as its preview or the art drawn before the image, or
did not open.

CCITTFaxDecode now decodes CCITT Group 4 (ITU-T T.6) and Group 3
one-dimensional and mixed (ITU-T T.4) data to rows of 1-bit samples.
It reads K, Columns, Rows, EndOfBlock, EndOfLine, EncodedByteAlign and
BlackIs1 (PLRM 3rd ed., §3.13; ISO 32000-2, §7.4.6). With EndOfBlock
the data ends at its EOFB or RTC regardless of Rows, and the RTC of
mixed Group 3 data, six EOLs each with a tag bit, is found by the
filter. Damaged data keeps the rows decoded before it, and damage in
the first row is an ioerror. Columns outside 1 to 65,535 and Rows
below 0 are a rangecheck, and data that would decode to more than
256 MiB is refused. hayro-ccitt 0.3.0, already in Cargo.lock through
hayro-syntax, becomes a dependency of vectorcraft-eps.

The tests encode their images in the test, with the fax crate's
Group 4 encoder and T.4 run codes (a dev-dependency, already in
Cargo.lock through tiff), and fail without the change. The import fuzz
tests read a Group 4 and a Group 3 image coded by hand from the T.4
and T.6 tables, read the same file with random damage, and add the
filter and its parameters to the hostile programs.

Docs: docs/file-format-parity.md, docs/target-app-parity.md.
2026-10-17 14:43:14 +07:00
Marco Arnulfo 025ce255ae PDF export: a CMYK image's own soft mask is written as its /SMask, not a luminosity mask (fixes #2159) 2026-10-17 14:43:14 +07:00
Marco Arnulfo 8e69146160 Recolor Artwork: dragging a wheel handle of CMYK art keeps the linked handles in place (fixes #1733) 2026-10-17 14:43:14 +07:00
EricKuckandClaude Opus 5.5 7add248dc3 Gradient mesh: an opaque patch is backed by its outline, so patches that narrow to slivers leave no holes
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 14:43:14 +07:00
Marco Arnulfo cb968353a5 Missing Fonts: a folder the search refuses says why in the dialog, not "Not found" (part of #2200) 2026-10-17 14:43:14 +07:00
dotCooCoo 08dc614938 File formats: PostScript (.ps) and .epsf files open and place
The EPS reader read PostScript by any name, but the Open and Place
dialogs hid .ps and .epsf files, and a .ps or .epsf file whose header
is only %! could not be opened or placed ("not a format VectorCraft
reads"). The reference app lists .eps, .epsf and .ps under Open and
Place.

- A read-only PostScript format (ps) joins the format table. A .ps
  file opens and places through the EPS reader and reports the
  ps format. document.export does not write .ps; file.print does.
- .epsf is an EPS extension.
- OPEN_EXTS and PLACE_EXTS list epsf and ps, so the dialogs, the CLI
  help and the MCP open_file description list them.
  UNASSOCIATED_EXTS lists them too: the macOS bundle declares neither.
- A .ps or .epsf file whose header is only %! now pastes as EPS when
  copied in a file manager, as one with a %!PS header already did, and
  clipboard.importEps reads PostScript whose header is only %!, which
  it rejected before.

Docs: docs/mcp.md, docs/file-format-parity.md (.ps row),
docs/target-app-parity.md.
2026-10-17 14:43:14 +07:00
Marco Arnulfo 63b95c8dd3 Type: emptying the text while editing keeps its character style (fixes #2119) 2026-10-17 14:43:13 +07:00
dotCooCoo b275eb04d0 Guides: a Cmd/Ctrl+Shift double-click releases the guide under the pointer
A double-click on a ruler guide with the Selection, Direct Selection or
Group Selection tool did nothing, whatever keys were held. A guide could
be released only with View › Guides › Release Guides.

With the Selection, Direct Selection or Group Selection tool, a
double-click with Cmd/Ctrl and Shift held now runs view.guides.release
on the ruler guide under the pointer (index) or, when no ruler guide
is there, on the guide path under it (ids). Nothing is released while
guides are hidden or locked, and a double-click without both keys does
what it did before.

Docs: docs/mcp.md, docs/target-app-parity.md.
2026-10-17 14:43:13 +07:00
dotCooCoo 4650ccbe26 Character: Reset Panel resets every attribute the panel shows except the font and size
The Character panel menu's Reset Panel reset tracking, leading,
kerning, baseline shift, the scales, rotation, underline,
strikethrough and All Caps, but left Small Caps, superscript or
subscript, Character Alignment, Proportional Metrics and Digits at
their custom values.

Reset Panel now also sets those five to their defaults in the same
text.setFormat call, with the values taken from CharStyle::default().
It resets them in the same characters as the other attributes: those
of the selected type, or, while the Type tool edits text, its
selection (all of that text at a caret). It resets them whether or
not Show East Asian Options and Show Indic Options (Preferences ›
Type) are on.
2026-10-17 14:43:13 +07:00
dotCooCoo 4f1e4979fc Layers: the panel menu's Save Selection submenu saves, lists and updates saved selections
The Layers panel menu had no saved-selection items, and the Select
menu was the only menu that listed saved selections. The reference
app's help also names a Save Selection menu in the Layers panel that
lists the saved selections and holds Save Selection, Update Selection
and Edit Selection.

The Layers panel menu now has a Save Selection submenu after Locate
Object. It holds Save Selection…, Update Selection and Edit Selection…,
which are enabled and run as in the Select menu, and below them the
active document's saved selections. Choosing a name runs select.recall
with that name, as the Select menu's list does.

Docs: docs/mcp.md, docs/target-app-parity.md.
2026-10-17 14:43:13 +07:00
webdevtodayjason 3895694a9b View: Show Transparency Grid is saved with the document
Native files remember the grid like the saved view, so reopening restores it.
Toggling it is still view state: no undo step, document not marked modified.

Fixes #1968
2026-10-17 14:43:12 +07:00
BFlat e6b58f5142 Merge pull request #2649 from storytold/pr/batch-1017-i18n-docs
i18n and docs: rows and ROADMAP entry for the community PRs landed on 2026-10-17
2026-10-17 14:40:56 +07:00
BFlat afa1c992bf Merge pull request #2404 from marcoarnulfo/fix/transform-percent-size
Transform: a percentage typed alone in W or H scales the size it shows
2026-10-17 14:39:18 +07:00
BFlat 8548cf272d Merge pull request #2400 from marcoarnulfo/fix/transform-menu-flip-origin
Transform panel: the menu's Flip Horizontal and Flip Vertical keep the reference point
2026-10-17 14:38:41 +07:00
BFlat d890ab337d Merge pull request #2507 from dotCooCoo/fix/rgba-within-decode-limit
Images: sampling and recoloring decode images past 512 MB, up to 2 GB
2026-10-17 14:38:07 +07:00
BFlat c1f543dd75 Merge pull request #2567 from dotCooCoo/feat/artboards-duplicate-copy-cut-selected
Artboards: Duplicate Artboards, Copy and Cut take every selected artboard
2026-10-17 14:37:15 +07:00
BFlat dbef720363 Merge pull request #2590 from dotCooCoo/fix/i18n-variable-site-rows
i18n: file filters, preset names and mark styles are translated in every complete language
2026-10-17 14:36:58 +07:00
BFlat 67fbb3c18d Merge pull request #2597 from dotCooCoo/fix/save-as-template-extension
Templates: Save as Template always writes a file with a template extension
2026-10-17 14:36:45 +07:00
BFlat a2aa0fc303 Merge pull request #2617 from dotCooCoo/fix/effect-color-param-refuses-unreadable
Effects: effect.apply and effect.setParams refuse a color the effect can't draw
2026-10-17 14:36:33 +07:00
BFlat 99220d50e1 Merge pull request #2621 from dotCooCoo/fix/pattern-cache-keeps-decode-warning
Render: every raster export warns about an undecodable image inside a pattern
2026-10-17 14:36:21 +07:00
BFlat 770213b295 Merge pull request #2501 from dotCooCoo/fix/para-style-redefine-default-attrs
Type: redefining a text style back to a default updates the text that uses it
2026-10-17 14:36:04 +07:00
BFlat f72d5060f2 Merge pull request #2504 from dotCooCoo/fix/batch-data-set-file-names
Actions: Batch keeps files named after data sets inside the destination folder
2026-10-17 14:35:47 +07:00
BFlat 3c2fa5917d Merge pull request #2508 from dotCooCoo/fix/character-small-caps-super-subscript-buttons
Character: Small Caps, Superscript and Subscript buttons and menu items set the text
2026-10-17 14:35:29 +07:00
BFlat 7ddc8d9c8a Merge pull request #2513 from dotCooCoo/feat/ai-drop-shadow-darkness
.ai import: a Drop Shadow with Darkness comes in with its Darkness
2026-10-17 14:35:05 +07:00
BFlat 85baa19884 Merge pull request #2554 from dotCooCoo/fix/kerning-pairs-across-style-runs
Type: a change of paint, baseline shift or rotation on one character keeps the font's kerning
2026-10-17 14:34:51 +07:00
BFlat f4000408a2 Merge pull request #2574 from dotCooCoo/fix/svg-justify-all-letter-gaps
SVG export: justified and Mojikumi lines keep their letter spacing in live text
2026-10-17 14:34:32 +07:00
BFlat 2cdf8465b9 Merge pull request #2578 from dotCooCoo/feat/zh-hans-messages
i18n: Simplified Chinese translates status-bar and error messages
2026-10-17 14:34:16 +07:00
BFlat 8f42ac1892 Merge pull request #2580 from dotCooCoo/chore/type-parity-hyphenation-row
Docs: type-parity and the roadmap list the Hyphenation options as done
2026-10-17 14:34:05 +07:00
BFlat ab2f745cd3 Merge pull request #2598 from dotCooCoo/fix/tiff-planar
TIFF: planar grayscale files with extra channels, and planar CMYK files, open and place
2026-10-17 14:33:49 +07:00
BFlat 7a97db06b7 Merge pull request #2602 from dotCooCoo/test/devanagari-shaping
Type: a click in a Devanagari syllable goes to its nearer end, and shaping is tested
2026-10-17 14:33:35 +07:00
BFlat 17a1c9835b Merge pull request #2608 from dotCooCoo/feat/dataset-import-tabs-no-name
Variables: Import Data Sets… reads tab-separated files and files without a name column
2026-10-17 14:33:24 +07:00
BFlat 0990c7bbd3 Merge pull request #2607 from dotCooCoo/feat/print-preset-by-name
Print: Print, Print Preview and Print Setup take a print preset by name
2026-10-17 14:33:02 +07:00
BFlat 41d79128f5 Merge pull request #2623 from dotCooCoo/feat/swatch-alt-click-stop
Swatches: Alt-click on a swatch recolors the selected gradient stop
2026-10-17 14:32:47 +07:00
BFlat eff0baad56 Merge pull request #2611 from dotCooCoo/fix/icon-buttons-accessible-names
Accessibility: buttons, toggles, checkboxes and sliders have names for screen readers
2026-10-17 14:32:32 +07:00
BFlat f7d1c354fd Merge pull request #2547 from vangel76/fix/2168-persist-presets-profiles
Persistence: saved PDF presets and loaded ICC profiles survive a restart (#2168, #2174)
2026-10-17 14:32:20 +07:00
BFlat 66374d0835 Merge pull request #2546 from vangel76/fix/2165-mcp-cancel-export
MCP: cancel long exports, answer while one runs, report progress (#2165, #2171)
2026-10-17 14:31:57 +07:00
BFlat e5745c4fc1 Merge pull request #2522 from vangel76/fix/2162-rgb-in-cmyk-warning
PDF export: report RGB colours and images in a CMYK document (#2162)
2026-10-17 14:31:40 +07:00