Commit Graph
217 Commits
Author SHA1 Message Date
bflatastic 084f4f3240 Text: a line ending at the paragraph's start can't underflow 2026-10-29 20:36:07 +07:00
Taha-Mahmoodi 7511f84d5d Layout: end manual tate-chu-yoko at soft return, and use separator metrics for empty continuation line 2026-10-29 20:36:05 +07:00
Taha-Mahmoodi b6df1ef0b1 Type: handle soft return in no-break runs, event modifiers, trailing breaks, and tabs 2026-10-29 20:36:05 +07:00
Taha-Mahmoodi d89031848c Fix #2754: Shift+Enter inserts a soft return (forced line break)
In typography and Illustrator, Shift+Enter inserts a soft return (forced
line break) rather than ending the paragraph. Text wraps to the next line
without applying paragraph-level spacing (space before/after, first-line
indents), list continuation lines keep their parent item without repeating
markers, and justified text does not stretch lines ending with a soft return.

- Forward active modifier keys for Enter in ui-egui shortcuts so the tool
  detects Shift+Enter.
- Type tool inserts U+2028 (LINE SEPARATOR) on Shift+Enter.
- Text shaping and layout treat U+2028 as an invisible zero-advance control
  character, breaking the line in point and area type.
- Paragraph spacing and list markers apply once per paragraph as intended.
2026-10-29 20:36:05 +07:00
bflatastic e0c92a4d90 Keep a letter and its marks in one font when falling back (#4491) 2026-10-29 16:27:18 +07:00
NetterSilverandClaude Haiku 5.5 520f390c9c Map hyphenation exception break points to the original word's chars
Found while testing crates/text/src/hyphen.rs:74-90 (parse_exception)

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 14:36:33 +07:00
dotCooCoo bbf01223cb Snap to Glyph: the x-height guide stays on the glyphs' x-height
Character Rotation turned a glyph's baseline shift and scaling with the glyph, and the Skew path effect slants a glyph's height away from the path's normal, so the x-height guide moved off the glyphs. Each positioned glyph now carries the transform that removes its Character Rotation (PositionedGlyph::unturned), and the guide goes through the glyph's x-height point placed without it, measured across the baseline. Snap to Glyph also takes Smart Guides being on in the tools, as View › Snap to Glyph already does in the engine.
2026-10-29 11:18:38 +07:00
dotCooCoo f87b8af62b Snap to Glyph: points land on glyph anchors and line up with x-height, character widths and angles
With View › Snap to Glyph and Smart Guides on, a point drawn near a single
line of live type now also lands on the anchor points of its glyphs and lines
up with its x-height (raised or lowered by Baseline Shift) and with the edges
of each character's advance width along the baseline (proximity guides). In
rotated type the baseline, x-height and proximity guides run at the type's
angle, and type of one glyph also has angular guides along the glyph's slanted
straight segments; a segment that Shift keeps to its angle slides onto them.
A paragraph shows glyph bounds only, so its baselines are no longer guides.

FontFace::x_height gives a face's x-height in font units.
2026-10-29 11:18:38 +07:00
Dylan M. Taylor 86c81f21d2 Restore RTL Mojikumi condition, dropped layout comments and xtask root
- flow.rs: an opening bracket starting a wrapped line is set flush unless the paragraph is
  right to left, as before the split (it had become !vertical, so vertical text with
  LineEndHalf lost the flush and RTL paragraphs gained it). PlacementContext carries rtl.
- flow.rs, ruby.rs: put back the comments the split dropped (JLREQ, burasagari, Optical
  Margin Alignment, kashidas, ruby phases).
- xtask root(): the parent directory, not .../xtask/..
2026-10-28 17:20:12 -04:00
Dylan M. Taylor 0e2815629f refactor(text): split flow and ruby layout phases 2026-10-27 17:47:12 +00:00
Dylan M. Taylor dec8b90e3a refactor(text): clarify line adjustment inputs 2026-10-27 17:47:12 +00:00
aiongg 3d39bbb31e refactor(text): move layout.rs wrapping_tests to layout/wrapping_tests.rs 2026-10-26 22:01:25 +00:00
aiongg a8935c0ada refactor(text): move layout.rs items to layout/{bidi,breaking,driver,flow,on_path,region,ruby,vertical}.rs 2026-10-26 22:01:25 +00:00
aiongg de14f93c05 refactor(text): move fontdb.rs items to fontdb/{build,cache,db,dirs,face,glyph_pen,lookup,names,scan,traits,wanted}.rs 2026-10-26 22:01:25 +00:00
aiongg f1d427762b style: cargo fmt on files moved without formatting 2026-10-26 19:32:10 +00:00
aiongg 52fb56b438 refactor(text): move tests_*.rs to tests/<topic>.rs 2026-10-26 18:26:59 +00:00
aiongg 2d9e537dcc refactor(text): move thread.rs tests to thread/tests.rs 2026-10-26 18:18:18 +00:00
Caio Pontes 311b343219 Fix #4128: text.create and text.setStyle store a font style the family has
Given a family and no font style, text.create and text.createInPath
kept the default Regular, and text.setStyle kept the text's previous
style, even when the family has no such style. A family whose styles
are numbered (Hiragino W3, W6) has no Regular, so the file named a
font that isn't installed and opened with Missing Fonts.

They now keep the style when the family has it, else take the
family's closest one, as text.setRangeStyle already did. Its rule
moves to `textedit::fit_font_style`, which the three commands share;
Graph labels styled through text.setStyle follow it too. A style the
caller names is kept as given.

The test builds a family with W3 and W6 and no Regular in code
(`test_fonts::numbered_font`), so no font file is added.
2026-10-26 18:12:51 +07:00
bflatastic f5c528069f Revert "Merge pull request #4203 from storytold/fix/sonar-s2208-remaining"
This reverts commit 15e4054f7a, reversing
changes made to d7a2591dc0.
2026-10-26 09:56:24 +07:00
Dylan M. Taylor 96cd4b1b14 lint: explicit imports in remaining test and module files (S2208)
Replace glob imports (`use super::*`, `use crate::*`, `use sig::*`) with
the names each file uses, in scriptcheck, pdf, render, text and ui-egui.
No code changes.
2026-10-26 09:52:51 +07:00
ham9zah e56aa356b5 Type on a path: joined scripts bend along the path so their joins stay connected
On a curved path each glyph was placed rigidly (turned to the tangent at
its middle), so its baseline ends stood off the curve and neighbours
turned by different angles: in scripts whose letters join, every join
opened into a notch. Measured with Arial on circles: at 29 pt on a
radius of 110 pt, 0.64 pt at the baseline and 1.24 pt at the height of
the joining stroke (a quarter of the size); 14.7 pt at 72 pt on a radius
of 60. Placing glyphs on the chord alone closes the baseline but leaves
about h (a_i + a_{i+1}) / 2R above it (0.71 pt here), so joined glyphs
are bent instead.

- Shaping marks each glyph of a segment whose script joins (Arabic,
  Syriac, N'Ko, Mongolian, Adlam and the other cursive scripts), by the
  segment's script, so a word bends as a whole.
- With the Rainbow effect such a glyph's outline is mapped point by
  point: pen space (x, y) -> P(s + x step/advance) + (y + rise) N there,
  each segment cut into pieces short enough for 0.01 pt (capped). A
  glyph the path doesn't turn under stays exactly as it was; Skew, 3D
  Ribbon, Stair Step and Gravity, vertical type, inline art and colour
  glyphs are unchanged. `PositionedGlyph::bend` records it; `xf` stays
  the straight glyph for the caret, hit testing and tools.
- PDF export draws bent glyphs as outlines and writes them again as
  invisible text where they stand, so they can be selected, searched
  and copied. SVG export outlines them (right-to-left type already was),
  never as a <textPath>, and labels the group with its text
  (role="img", aria-label) for screen readers and search.

Tests (crates/text/src/tests_path_joins.rs; installed Arabic font, skip
without one): the gap at the baseline and at join height is <= 0.01 pt
at 12, 29 and 72 pt on radii 60, 110 and 300, on both sides of the
path, with finite outlines; Latin on a curve and joined type on a
straight path are byte-for-byte what they were; the chord-only
prediction is checked.

vectorcraft-cli perf (load average 43 before, 34 after: not trustworthy;
the synthetic document has no type on a path, so bending doesn't run):
render fit 113.8 -> 132.7 ms, pan 115.5 -> 140.0 ms, 400% 32.9 -> 43.3
ms, hit test 1.36 -> 1.59 ms, save 81.5 -> 88.2 ms, open 306 -> 401 ms,
SVG 426 -> 439 ms, Pathfinder 93.7 -> 85.5 ms, handle drag 5.5 -> 6.7
ms, segment drag 5.1 -> 6.7 ms.
2026-10-26 09:09:47 +07:00
Dylan M. Taylor 46ece73197 lint: method references and elided const lifetimes (S1612, S8863) (#4204)
Use function paths where a closure only forwarded its argument
(PoisonError::into_inner, OsStr::to_str, FindRange::is_empty), and drop
redundant 'static on associated and module consts. No behaviour change.
2026-10-25 22:08:44 -04:00
dotCooCoo 0520e9da86 Type: threaded frames do not repeat a continued paragraph's first-line indent or list marker
A paragraph split between two threaded frames took its first-line
indent again at the top of the next frame, and a list paragraph drew
its bullet or number there again. Each frame is laid out on its own,
and its first paragraph kept every paragraph attribute there.

A text object now marks a frame whose first paragraph continues from
the frame before (first_para_continues, not saved). The thread re-flow
sets the mark where each frame's share starts inside a paragraph and
measures each share with it, and a restored document reads the marks
from its saved text. Area type and type on a path set the first line of
such a paragraph as a wrapped line: no first-line indent and no bullet
or number. The paragraph after it takes the next number. Release
Selection, Remove Threading, Convert To Point Type and copies of a
frame clear the mark, so the first paragraph there has its indent and
bullet or number again. When Create Outlines, Expand or Expand
Appearance takes frames out of a thread, a frame that stays keeps the
mark only while it is threaded and the frames before it end inside a
paragraph. A frame whose mark changes lays out its first line again,
and when it is still threaded, its thread re-flows. When the outlined
frame is the last one, the text it has no room for is measured as the
frame lays it out: without the first-line indent when it continues a
paragraph, and with its numbers counted on from the frames before it.
The frame before it now takes exactly the text the last frame has no
room for. Duplicates, copies dragged in the Layers panel, the copies
that Release to Layers (Build), Release Opacity Mask and Edit Opacity
Mask make, and Draw Inside's clipping copy then have their cached
bounds computed again, as paste already does for pasted type.

Docs: docs/mcp.md, docs/target-app-parity.md.
2026-10-24 21:43:44 +07:00
dotCooCoo bc041e15ef Type: the Numbered List button numbers paragraphs 1, 2, 3
Squashed from #3960 onto main.
2026-10-24 19:02:08 +07:00
Dylan M. TaylorandClaude Opus 5.5 03d2f0e83c Security: file authority for stored paths, linked files, placed documents, font folders, recovery areas and atomic saves
CodeRabbit findings #7, #72, #92, #94, #95, #99, #100, #140, #158, #174:

- Link search hashes at most the size it looked at (a growing file no longer matches).
- Resolving links reads, decodes and previews each linked file once, shared by every key.
- Placed documents read inside one another resolve links at most 8 deep, never one already
  being read.
- A data set's image path and the placed-document rereader follow stored paths through
  `document_path` (no other computer's share).
- The Links panel looks at a linked file within the roots of the automation caller that
  opened the document.
- A fonts folder set by a confined caller is scanned without following links out of it.
- Recovery refuses an area folder that is a link or another account's.
- Atomic saves keep the replaced file's group and Linux access ACL, or make the replacement
  owner-only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 17:19:59 +07:00
Dylan M. Taylor 6c05c10fff chore(text): use is_some_and instead of unnecessary map_or (clippy) (#3929) 2026-10-23 22:39:51 -04:00
Dylan M. Taylor d1288e3019 Security: text layout, shaping, font name and embedding bounds (#3880)
* Audit: text layout, shaping, fonts and embedding bounds

* Audit: text bitmap chunk walk without overflow

* Test: PNG metadata chunk walk

* Security: keep the word minimum in wrap regions; allow uncompressed iTXt text

A wrap shape can narrow the first baseline of a rectangular region:
place_at_least then relaxed the minimum to 0 (the region still counted as
"all lines equally wide") and picked the narrow span, breaking a word on
that baseline even though a lower one had room for it whole. Treat the
width as constant only when the region has no wrap shapes.

Also, PNG iTXt chunks may be stored uncompressed: only genuinely
compressed text (flag 1, conservatively anything that does not parse)
counts as compressed metadata now.
2026-10-23 21:53:51 -04:00
Taha-Mahmoodi 6b52e54a1b Fix #3920: a family another thread is loading is found whole, so fallback text never takes the style of its first file
An installed family was added to the font database one file at a time, so a
lookup on another thread could find it with only some of its styles: text
falling back to Arial took Arial Bold Italic or Bold (its files sort first)
for upright Regular text, and the same text laid out a moment later took
Arial Regular. One Arabic word could even mix the two. Two threads loading
the same family could also add a face twice.

A family's files are now read and parsed first and their faces added under
one lock, skipping faces another thread added meanwhile. Loading a family
reports it loaded when another thread loaded it meanwhile, so the fallback
search doesn't go on to other families.

This made tests_bidi::a_change_of_paint_in_a_mixed_direction_line_moves_no_glyph
fail intermittently when another test loaded Arial at the same time.
2026-10-24 07:44:14 +07:00
bflatastic c2ad9f8bbb Type: emoji go with type that effects reshape and Expand Appearance
Squashed from #3879 onto main.
2026-10-24 04:49:06 +07:00
Jürg Lehni e372d00cb7 Type: tests pin colour glyphs in variable fonts, on paths and on the canvas
A variable font's COLR layers follow its instance, a colour glyph turns with type on a path, and the canvas draws an SVG glyph's art in its own colours and the type's fill.
2026-10-24 03:52:23 +07:00
Jürg Lehni 7efd8f941a Type: colour glyphs keep each fill's own colour, also between colours that look alike
The colour glyph cache keyed the text's colours by their 8-bit RGB look, so a fill that only looked like an earlier one (a grey and the same RGB grey, a spot colour and its process match) took that one's colour, and its separations. It now keys them exactly.
2026-10-24 03:52:15 +07:00
FractalEncrypt 25abd1f472 text: recheck concurrently loaded fallback faces
A system fallback search can add no new face because another lookup loaded it
first. Re-read the loaded faces regardless of the search's return value, so
concurrent first use does not produce a missing glyph and different advances.

Add an isolated generated-font regression with simultaneous cold lookups.
The original code missed 14 of 16 lookups; the fixed regression passed twenty
runs. Both previously failing Ruby tests pass unchanged.

Validation: Rust 1.98.1 cargo xtask ci, all 10 gates passed; workspace tests
9,635 passed, 0 failed, 14 ignored. Default Rust toolchain unchanged.
2026-10-24 03:52:15 +07:00
dotCooCoo 2a2e63856c Type: the Paragraph panel's Bullets button turns paragraphs into a bulleted list
VectorCraft had no lists. A bullet could only be typed as a character,
and the tab after it did not stop at the left indent. The reference app
turns paragraphs into a bulleted list from the Paragraph panel, the
context menu and Type › Bullets and Numbering › Apply Bullets.

text.setFormat takes list ("none" or "bullet") and bullet ("none",
"dot", "dash", "circle" or "square"). The layout draws the bullet before
the first line of each list paragraph that has text, in the font, size
and color of its first character, and starts the text at the next tab
stop, with the left indent counted as a stop. Wrapped lines start at
the left indent, and type on a path draws the bullet too. In a
horizontal right-to-left list paragraph the indents are mirrored: the
bullet hangs at the right, and the wrapped lines start at the right
where the first line's text starts after the bullet. The bullet is not
a character of the text. A paragraph whose left and first-line indents
are both 0 gets 18 pt and -18 pt when it joins a list. Return continues
the list, paragraph styles carry it, Reset Panel turns it off, and the
native file saves it only when it is set.

The Paragraph panel has a Bullets button with a list of the five
presets, and Type › Bullets and Numbering › Apply Bullets and the
context menu run type.applyBullets. Live SVG text writes the bullet's
character, and SVG writes type on a path with bullets as outlines. DXF
writes the bullet as a TEXT entity of its own, and legacy .ai export
warns that bulleted lists are left out. The caret, selections, Touch
Type and Show Hidden Characters skip the bullet.

Live SVG text of horizontally scaled type now writes each tab in a
tspan of its own. Before, the characters before a tab shared its tspan,
and its textLength made a viewer stretch them across the tab's gap.

Docs: docs/mcp.md, docs/target-app-parity.md.
2026-10-24 03:09:11 +07:00
Taha-Mahmoodi 52d93cfce5 Font menu: Font Technology › Variable lists only variable font families
As Illustrator's help describes (Filter fonts by classification, Font Technology › Variable), the font menu gets a Font Technology row whose Variable choice lists only the families with variation axes, together with the script, kind, favourites and search filters, and stays set from one opening to the next. A family is variable when any of its faces, loaded or scanned, has an `fvar` table, so a family installed as static files beside a variable one counts.

Spec: https://helpx.adobe.com/illustrator/desktop/design-with-text/fonts-and-scripts/find-apply-and-adjust-variable-fonts.html
2026-10-24 02:06:16 +07:00
fortesnow bfceca7b4d Fonts: a family and style split from a PostScript name find the face once it is installed
A PDF names fonts by PostScript name. When the font isn't installed at import, the importer
splits the name (`Honoka-Mincho` → family Honoka, style Mincho) and the document keeps that
pair. Installing the font later didn't help when its family name shares nothing with its
PostScript name (ほのか明朝): the pair stayed missing. FontDb::canonical now also looks the
pair up, joined, among the PostScript names, and answers with that face's own family and style.
2026-10-24 02:06:09 +07:00
Jürg Lehni c28ad99678 Type: OpenType SVG colour fonts draw and export as vector art
Each glyph's SVG document is read by the SVG importer, which the engine registers with the text crate.
2026-10-23 09:07:54 +07:00
BFlat 3e0c1d6870 Merge pull request #3735 from Silvertree2010/fix/3596-audit-fonts-the-face-count-of-a
Cap the face count of a font collection in enumerate_faces
2026-10-23 08:05:46 +07:00
NetterSilverandClaude Haiku 5.5 ffd831761e Cap the face count of a font collection in enumerate_faces
Fixes #3596

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-23 07:44:33 +07:00
Angelo Bernardini 22bdcae44d G4: hanging punctuation keeps its amounts under Glyph Scaling and burasagari
Squashed from #3362 onto main.
2026-10-23 07:35:24 +07:00
Jürg Lehni 9de4b40241 Type: COLR colour fonts draw and export as vector art
Glyphs whose paints documents have no form for (sweep gradients, compositing modes) are drawn as images of the glyph.
2026-10-23 05:14:47 +07:00
bflatastic 26b9cf84ad Type: colour emoji fonts with bitmap glyphs draw and export
Squashed from #3561 onto main.
2026-10-23 00:44:31 +07:00
dwetscherandClaude Opus 5.5 fd50bd6f84 AF.5: Affinity import — strokes, text decoration, text placement, outline effects, CMYK photos
- Strokes set to None no longer import as black 1px strokes.
- Underline and strikethrough are kept.
- Frame text starts at cap height; lines hanging out of a frame make it taller;
  mixed-alignment artistic text lines up within its longest line; line breaks
  inside a paragraph get no paragraph spacing.
- Table cell text taller than its cell is centred and stays visible.
- Outline effects on stroked shapes are drawn around the stroke.
- CMYK pixel layers go through the document's embedded CMYK profile; without
  one, opaque layers stay CMYK.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-22 18:24:57 +07:00
dotCooCoo 95f503367f Type: the vertical ruby test passes with craft-fonts' Shippori Mincho
tests_ruby::vertical_ruby_is_upright_to_the_right_of_the_column
checked that no reading glyph is more than 1 pt wider than tall. With
craft-fonts in the build, the kana come from Shippori Mincho, whose か
is 8.24 pt wide and 7.22 pt tall at 10 pt. The test failed on that
glyph although it stands upright. FreeBSD CI, which builds with
craft-fonts at a71c20e3, fails it on every branch since 14f804cd. It
also fails on Windows with that ref or an older craft-fonts checkout.

The test now compares each reading glyph's ink with its own outline at
10 pt. The same width and height mean the glyph is upright; a swapped
width and height mean it lies on its side. It also requires one
reading glyph whose outline is not square.
2026-10-22 04:34:32 +07:00
bflatastic 34cde7c3db Character: Character Direction sets the selected characters left to right or right to left
Squashed from #3436 onto main.
2026-10-22 04:18:35 +07:00
bflatastic 62776d5f46 Recolor: recolor.apply takes a model option for the new colour's model (#2170)
Squashed from #2520 onto main.
2026-10-22 02:01:57 +07:00
bflatastic b45bb3dd54 Character panel Tate-chu-yoko marks vertical type as a horizontal block
Squashed from #3399 onto main.
2026-10-22 01:06:24 +07:00
bflatastic adbe0566a6 G4: System Layout spaces every text object in whole points
Squashed from #3412 onto main.
2026-10-22 00:54:43 +07:00
Jürg Lehni 83a5978d6e Type on a path: right-to-left text that overflows shows its start
Its glyphs were put in visual order and then cut where the path ran out, which kept the text's end. Now the longest start of the text, in reading order, whose glyphs fit in visual order is laid out, as area type does with each line it breaks, by the same rules that place the line: steps where the glyphs sit, hanging punctuation and the path beyond the brackets.
2026-10-22 00:30:08 +07:00
bflatastic 14f804cdee Type: ruby (ふりがな) for live text
Squashed from #3448 onto main.
2026-10-22 00:10:56 +07:00
bflatastic 1cfeaabb2d Docs and format: one row per area after the batch, rustfmt
Rows two PRs of the batch both changed came out twice in localization-parity, target-app-parity and type-parity; the newest of each is kept. prefs_dialog.rs is formatted.
2026-10-21 23:38:01 +07:00