Commit Graph
57 Commits
Author SHA1 Message Date
73ed08ddbe PSD type: read styles from the run's sheets and place text at its Txt2 frame (#3795, #1317) (#3819)
* PSD type: resolve paragraph and character styles from the run's own sheets (#3795)

apply_engine_data filled a run's missing attributes only from the Normal
sheets (TheNormalParagraphSheet / TheNormalStyleSheet). A run that takes
its alignment or size from a named sheet (by Parent index or Name), or
from its paragraph's DefaultStyleSheet, was read with Normal's values, so
a centred paragraph style came back Left and starting an edit re-laid
the text half a line to the side.

Resolve run -> named sheet -> paragraph's default character style ->
Normal, ignoring missing or out-of-range references. On export, runs
whose sheet reference is rewritten also get the attributes they
inherited, so they keep their look whatever they now point to.

* PSD type: place text at its Txt2 frame and keep box lines whose descent overhangs (#1317)

Recent Photoshop keeps part of a type layer's position in its Txt2 frame: a
frame-to-text-space matrix and, for point text, the anchor, with EngineData's
PointBase left at the origin and BoxBounds in the frame's space. Import ignored
them, so box text landed far off the canvas once an edit started (the text
'disappeared') and point text moved up a line and off its alignment anchor.
Apply the frame when its origin lands in the TySh bounds (and not already
without it, so a TySh PhotoCraft rewrote is not moved twice).

Photoshop also keeps a box line whose baseline is inside the box while its
descenders hang below it; layout dropped such lines (and some single-line boxes
rendered nothing).

---------

Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 09:28:31 -04:00
Reynold Hu b0554b8178 Type: wire missingGlyphProtection (script fallback toggle) (#3863)
* text: missing glyph protection can turn the script fallback stack off (#1909)

* engine: apply type.missingGlyphProtection to the live text engine

* docs: scorecard, settings that do nothing 30 -> 29 of 154
2026-10-12 08:34:22 -04:00
David MannandClaude Opus 5.5 b071929ecb Type: editing text keeps its ligatures and OpenType alternates (#3617) (#3747)
Photoshop keeps Stylistic Sets, Contextual Alternates, Swash, Fractions, Ordinals, Titling and
Stylistic Alternates and Ornaments only in a PSD's Txt2, not in EngineData. Import now reads them
into the runs' font features, so the layout drawn while the type is edited keeps Photoshop's
glyphs instead of falling back to the default ones.

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-12 07:48:24 -04:00
Nicolas Falesy 0c2ca488ae fix(text): scale faux bold dilation with glyph scaling (#3705) (#3719)
When drawing faux-bold glyphs, the offset dilation was composed as
`Xform(ox * r, oy * r).mul(&glyph)`. Because `Xform::mul(o)` applies `o`
first (`self ∘ o`), the translation offset was added after the glyph's
scaling transform, leaving faux bold dilation unscaled by horizontal
and vertical font scaling.

Applying the glyph transform to the offset transform (`glyph.mul(&offset)`)
ensures dilation is performed in font-local coordinates and scaled
proportionally with anisotropic glyph scaling, matching Photoshop.
2026-10-12 07:13:06 -04:00
Brandon ThomasandClaude Opus 5.5 6f1c4481bf fix(psd): type layer bytes match Photoshop (TySh bounds, AntiAlias, version info) (#4041)
- TySh: the four values after the warp descriptor are big-endian f32, not
  i32. Written as Photoshop writes them: zeros for unwarped type, the
  text-space bounds for warped type (corpus: warp-arc-50.psd, unwarped
  text oracles).
- EngineData AntiAlias numbers as Photoshop writes them, on write and on
  read (when there is no AntA descriptor): 0 None, 1 Crisp, 2 Strong,
  3 Smooth, 4 Sharp (corpus text/antialias-*.psd). Platform modes, which
  no corpus file confirms, write Smooth and keep their exact mode in AntA.
- Resource 1057: writer "Adobe Photoshop", reader "Adobe Photoshop 2026",
  no NUL terminator, byte for byte what all 258 Photoshop 2026 oracles
  write.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 06:06:46 -04:00
Sir Starch 82c58fabab Use Fontconfig for Linux font discovery (#2330) (#2961)
* Fix #2330: discover installed fonts through Fontconfig

* Fix wasm cfg for Fontconfig fallback

* Add Linux font discovery regression tests

* Reject Fontconfig matches without CJK coverage
2026-10-12 01:05:56 -04:00
Scott C Hughes ee05bc833b fix(io): a fresh Metrics type layer writes no empty Txt2 stub (#3514) (#3529)
PSD export always wrote a document-level Txt2 block, and for a fresh document its
document-resources dictionary (/0) was the empty `<< >>` Photoshop never writes. Photopea's
reader fails on it ("Cannot read properties of undefined (reading '_Resources')"), so a PSD
saved with a new type layer stopped opening there.

Write the block only when it carries something: the file already had one (round-trip its pen
positions and extras), or a layer uses the Optical auto-kern mode - the one mode EngineData's
AutoKerning cannot distinguish (#1348). A plain Metrics document now writes no block, as 0.3.0
did.

Tests: text needs_txt2_only_for_optical; io fresh_metrics_text_writes_no_txt2_block.
2026-10-12 00:18:14 -04:00
MrCloudy2andClaude Opus 5.5 7e5d2ce6da Warp presets: Photoshop's Bezier control grids instead of approximations (#3444)
The 15 warp presets (Edit > Transform > Warp, smart object warps, Warp
Text) were our own closed-form approximations, up to 70 px off
Photoshop's: Arc kept its centre fixed and pulled the ends in where
Photoshop keeps the inner edge's corners and raises the middle, Flag
waved the wrong way at a third of the height, Squeeze shrank instead of
pinching.

Measured on Photoshop 27.11 (scripted renders of a grid image whose
colours encode each pixel's position, 102 cases: every preset at bend
±50 and ±100, both distortions, vertical orientation, square, wide and
tall boxes), every preset is a single bicubic Bezier patch (fits within
0.5 px). Its control grid:
- Arc: each row a circular arc of b·π around one centre, the inner edge
  keeping its chord; Arch, Bulge, Arc Lower/Upper and Shell built from
  the same arcs. Circular in pixels on any box.
- Flag, Wave, Fish, Rise, Fisheye, Inflate, Squeeze, Twist: control
  points moved linearly with the bend, in box units.
- Vertical distortion scales the top and bottom rows about their
  midpoints and blends the rows between; horizontal distortion scales
  each column. Vertical orientation swaps the axes and the two
  distortions.
All 102 cases land within 1 px (median) of Photoshop. On the corpus's
Photoshop-made warped text, warp-arc-50 now spans 340x85 at (-3, 3)
against Photoshop's 350x88 at (-7, 3), where it was 278x74 at (28, 62).

Presets also convert to custom meshes exactly now (to_mesh splits the
patch instead of fitting). Tests that pinned the old shapes now assert
Photoshop's; presets_match_photoshop checks measured points.

Still open: Warp Text warps over our line box, while Photoshop uses the
type layer's stored `bounds`; with those the five corpus warp files land
within a few px (arc, flag, bulge with distortion, twist, vertical rise).

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 23:56:17 -04:00
1051265f75 Type: fall back to installed Khmer, Lao and Burmese fonts (#218) (#3259)
Text in these scripts drew .notdef boxes although the OS had a font for
them. Detection was never the problem: the system font scan loads them,
and asking for one by name shapes correctly. The fallback stack is a
list of family names intersected with the installed fonts, and it had no
entry for any of the three, so an installed Khmer, Lao or Burmese font
could not be reached from a Latin-only face.

- photocraft-text: KHMER_FAMILIES, LAO_FAMILIES and MYANMAR_FAMILIES join
  fallback_candidates next to THAI_FAMILIES; the web build's served-font
  scripts gain Khmr, Laoo and Mymr (with their ranges and languages).
- photocraft-ui-egui: the UI's lazy loader had one non-CJK slot (Thai).
  It becomes a LazyScript slot table, so the three scripts are data
  rather than a third and fourth copy of the same state machine.
- Tests assert detection and selection separately, so a future failure
  names its layer, and skip when a machine lacks the script's font.

Co-authored-by: Raccoon <235524086+RaccoonPie-314@users.noreply.github.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 21:03:22 -04:00
14733b5223 Moving type keeps its rendering: Artboards to Files no longer drops PSD text (#3218)
translate_layer re-rendered a type layer on every whole-pixel move. For type
from a PSD that replaced Photoshop's rendering with PhotoCraft's: paragraphs
re-wrapped, and text in a font that isn't installed vanished. Artboards to
Files moves each board to the origin this way, so a Steam store template's
"How to use" artboard lost its "Hello!" title. The Move tool, nudges, layer
comps and moving artboards went through the same path.

A whole-pixel move now shifts the rendered pixels, as smart objects and the
Move tool's live preview already do, and moves the TySh position with
photocraft_text::psd::translate_tysh: only the transform's translation
bytes change, so the rest of Photoshop's type data stays byte for byte. Type
with nothing rendered yet is still rendered.

On that template, 17 of 20 artboards now match Photoshop's own composite
exactly (the "How to use" board went from a mean difference of 4.5 to 0.02);
the remaining differences are shape layers (same cause, separate path) and
pre-existing 1-level rounding on two smart objects.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-11 19:46:29 -04:00
f155374ac3 fix(type): navigate adjacent line with large leading (#3066) (#3141)
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 16:38:14 -04:00
7aba2bd305 Type: scan system fonts off the UI thread (#3021) (#3030)
* Type: scan system fonts off the UI thread (#3021)

The first `photocraft_text::shared()` call scans every installed font, and
nothing made that call before the Type tool's options bar drew its font
menu, so the scan ran inside a UI frame (over 60 s on some Macs, #2518).

- photocraft-text: `warm_shared()` starts the scan on a background thread;
  `try_shared()` returns the engine without blocking (None while the scan
  runs, started there if nobody warmed it; built in place on the web, in
  tests, if the thread can't start or the worker died). `shared()` keeps
  its blocking behaviour for engine commands.
- The desktop app warms the fonts at start-up.
- Font menus that draw every frame (options bar, Character, Glyphs and the
  Character/Paragraph Styles family menus) show a disabled "Loading fonts…"
  box and repaint until the scan is done. "Loading fonts…" is in all 16
  catalogs.

Complements #2644 (CoreText discovery on macOS): this changes where the
scan runs, not how fonts are discovered.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Type: axis sliders wait for the background font scan too

#2996 (landed on main) added axis_sliders, which called the blocking
photocraft_text::shared() every frame; use try_shared() so the Character
section doesn't stall while the scan runs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-11 16:25:57 -04:00
Valentin KleinandClaude Opus 5.5 139df7ae85 Type: variable font axis sliders; optical size follows the type size (#2985) (#2996)
The Character section (Properties and Window > Character) shows one slider
per axis of the style's variable font below the font style (Weight, Width,
Slant, Italic, or the font's own tag), setting type.setStyle variations; a
drag is one history step. Optical size has no slider: the layout now sets
opsz to the type size unless the style sets it, as CSS font-optical-sizing
auto. Picking a font style or weight resets the wght axis to it, so an
earlier slider value no longer overrides the style the dropdown shows.

Scorecard TYPE-218-4 -> done.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 14:46:26 -04:00
1234ba4c6f Type: wire Show Font Names in English (#204) (#2981)
Preferences > Type > Show Font Names in English (default on) now drives the
font menus (Type options bar, Character panel, style and Glyphs panels). With
it off, a family whose name table carries a non-English family name (e.g.
"MS Gothic" -> "MS ゴシック", "Microsoft YaHei" -> "微软雅黑") is listed by
that native name, sorted by the shown label, and the search matches either
name. Choosing it still applies the English family, so documents and PSD
output never change with the preference.

- text: FontDb::localized_family_names() reads each family's first face
  (typographic family name, else family name) and a pure localized_name()
  picks the first non-English record.
- ui-egui: sync_font_names() publishes the labels to the egui context once a
  frame (cached per font generation); family_picker_in shows them.
- prefs: the row is no longer hidden; scorecard: dead settings 43 -> 42.

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-11 09:21:55 -04:00
aa509ca966 UI fonts: lazy fallback for Thai, Arabic, Indic and other scripts (#2275) (#2758)
Layer names (and any other UI text) in scripts the bundled Inter lacks
showed missing-glyph boxes even when the canvas found a fallback font.
The lazy UI loader only knew CJK plus a short hard-coded list of Thai
font paths, so any other script, or a Thai font at another path, never
got a fallback.

photocraft_text::scripts classifies 20 scripts and picks candidate files
by name (Noto, Windows, macOS and common Linux families) from a listing
of the platform font directories, made once per process on first need.
The UI loader tries a script once when one of its characters is missing,
reads at most a few candidates, and registers the first face whose cmap
really maps the character. Latin text loads nothing; the web build has
no candidates and does nothing.

Co-authored-by: Fabio Lussi <198260595+itrycodinganditsbad@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 05:55:39 -04:00
2785699464 fix(type): use the correct line at soft-wrap caret boundaries (#3174)
* fix(type): use the next visual line at shared wrap boundaries

At soft-wrap and forced-break character offsets, resolve the caret's visual line from the last matching line rather than the preceding one. Make line-edge navigation share that choice and add regression tests for wrapped, forced and paragraph breaks.

* test(type): use actual break characters in caret boundary regressions

* fix(type): preserve the visual line when hit testing a shared wrap offset

Keep caret index preference on the following line, but use pointer geometry for click-hit line identity at an adjacent line's end.

* Update the caret round-trip test for wrap-boundary lines; cargo fmt

The existing round-trip helper expected the first line containing a
shared soft-wrap offset; the caret now reports the following line, where
it is drawn and where hit testing lands.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 03:37:27 -04:00
skylar taylor-barrick 77f50330c6 UI.9: refresh shared panel integration against current main 2026-10-10 19:30:56 -06:00
87f74cc8d2 Type: tabs advance to Photoshop's default tab stops (#1881) (#2733)
#1942 stopped tabs from shaping as .notdef by laying them out as a
space, so a tab was only one space wide. Layout now widens each tab to
the next default stop (every 36 pt, from the anchor of point text or the
left edge of the box). Paragraph text breaks lines with the real tab
widths: it starts from a safe upper bound (space + one interval) and
re-breaks until the widths agree, so expanded tabs never overflow the
box and lines don't wrap early. The text keeps its \t, so PSD round
trips are unchanged. Tab in the Type tool now types a tab.

Co-authored-by: Fabio Lussi <198260595+itrycodinganditsbad@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 18:38:53 -04:00
skylar taylor-barrick 99a3970399 UI.9: preserve PhotoCraft controls in shared UI migration 2026-10-10 12:51:56 -06:00
0384b1af6d Warp Text: the dialog opens with the layer's warp and previews it live (#218) (#2539)
* Warp Text: the dialog opens with the type layer's current warp

Type > Warp Text... always started at Arc with Bend 50, whatever warp
the layer had, so OK without a change replaced a Flag warp with Arc.
The dialog is now the schema dialog of type.warpText: it starts from
the shown type layer's style, bend, distortions and orientation (None
for a layer without warp, as in Photoshop) and keeps the targets it
opened on (selected type layers or the inline edit). Remembered dialog
values never override the layer's warp.

* Warp Text: the dialog previews the warp live on the canvas

The Warp Text dialog showed its result only after OK. It now has the
schema dialogs' Preview checkbox and live canvas preview; Cancel leaves
the document as it was and OK stays one history step.

The preview of a large document runs on a reduced proxy, but Warp Text
renders the type again from the layer model, at full size: the preview
showed the text k times too large and out of place. Warp Text runs on
the full-size copy and its result is reduced instead.

---------

Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 13:38:17 -04:00
8fd4aa4cc2 fix(psd): an unedited save writes Photoshop's Txt2 back unchanged (#2374) (#2427)
* fix(psd): an unedited save writes Photoshop's Txt2 back unchanged (#2374)

Since #1517 every PSD save with type layers rebuilt the document's Txt2
block: it replaced everything under /1 with the text-object list, emptied
objects no layer names, and cut each kept object's style runs down to the
auto-kern mode alone. Photoshop then refused to open an unedited re-save
("Could not complete your request because of a program error").

build_txt2 now starts from the file's own block: the other /1 keys and
unnamed objects are kept, a kept object's style runs keep every style key
and only get /11 set (split where a mode changes inside a run), and a block
whose content comes out unchanged is returned byte for byte.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(psd): Photoshop type files keep Txt2 byte-for-byte on an unedited save (#2374)

Corpus test over corpus/photoshop/text: import, export, re-import, and
compare the Txt2 global block. All 56 Photoshop-authored type files pass;
the previous build_txt2 fails on the first.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: vivekprojects-GIT <179340152+vivekprojects-GIT@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 13:11:42 -04:00
Brandon ThomasandClaude Opus 5.5 0c72d95425 Web: store translation catalogs and bundled fonts deflated, unpacked on first use (wasm back under the 24 MiB gate)
The 0.6.0 web build was 26.9 MB, over packaging/web/package.sh's 24 MiB gate (Cloudflare's per-file
cap is 25 MiB, #198). The i18n .tsv catalogs (1.85 MB -> 0.64 MB) and the Inter/JetBrains Mono fonts
(1.52 MB -> 0.73 MB) are now deflated by build.rs and inflated on first use, so wasm is 25,141,080
bytes. No visible change. A catalog that fails to inflate falls back to English, a font that fails
falls back to egui's default font; neither panics. Headroom is only ~25 KB: the next size cut is
needed soon (fetching catalogs on demand would save another ~0.64 MB).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 22:11:13 -04:00
Ahmed Moussa 1cfc56db56 fix(Text): served fonts become script fallbacks on the web (#2109)
A host's font manifest can now list the scripts each font covers (`scripts` field). The first served family for a script the bundled fonts lack (Arabic, Hebrew, Devanagari, Thai, Japanese, Chinese, Korean) becomes that script's fallback. When a layout contains text in that script, it requests the family. The font database adds the family to the fallback stack once it arrives, and type layers drawn before then are re-rendered.

The web shell also fetches the fallbacks for the browser's languages (`navigator.languages`) at startup, so an Arabic reader gets the Arabic font before typing anything.

The manifest's SHA-256 is now turned into a Subresource Integrity value (`sha256-<base64>`) and passed with the font fetch, so the browser rejects any file whose bytes don't match.

Also adds a contributor entry for hamada147 to `contributors/people.toml`.

Signed-off-by: Ahmed Moussa <moussa.ahmed95@gmail.com>
2026-10-09 20:04:04 -04:00
David MannandClaude Opus 5.5 51598bb061 Text: fall back to Thai-capable fonts for missing glyphs (#1909) (#2030)
Thai typed in a font without Thai glyphs (any newly chosen font, which
drops the PSD PostScript name that found the original Thai face) fell
through the fallback stack to .notdef: none of the candidate families
cover Thai on Windows (Segoe UI), and Arial Unicode MS is rarely
installed. Add the platform Thai families (Leelawadee UI / Leelawadee /
Tahoma, Thonburi / Sukhumvit Set, Noto Sans Thai) to the candidates,
ahead of the CJK and last-resort fonts, as Photoshop's missing glyph
protection substitutes an installed font. parley picks the fallback per
cluster, so base consonants and their marks stay in one font.

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-09 13:56:09 -07:00
Nigel Ihza FirdausyandClaude Opus 5.5 3eef2c6bb4 Type: imported PSD tabs render as whitespace, not a box (#1942)
A literal tab in PSD text shaped as .notdef. Layout now shapes it as a space, keeping byte, style
and caret offsets intact. Tab stops are still to come. Part of #1881.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 15:34:38 -04:00
shmuelbiller1andClaude Opus 5.5 decc7715b9 PSD: Optical kerning survives a round trip (#1517)
PSD export regenerates the document Txt2 block with one text object per TextIndex carrying each run's auto-kern mode (manual, metrics, optical), which EngineData's AutoKerning can't distinguish, so Optical no longer reopens as Metrics (#1348). A kept object with unchanged text keeps the file's extras (glyph pen positions); duplicate or out-of-range TextIndex values are renumbered and capped, so a crafted file can't size the save. io corpus tests stay at their floors.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 15:09:10 -04:00
Minseo LeeandClaude Opus 5.5 9f23d65262 Type tool: list fonts' real style names, including numeric subfamilies (#1844)
The style menu now shows each face's actual OpenType subfamily (name 17, else 2) instead of a name guessed from its weight, so fonts whose faces share one weight (Yoon's numeric 20/30/40/50) list and render distinct faces: FontDb::select_named_face registers a runtime-only alias for the exact face, while the document keeps its public family and PostScript name. Variable faces still list the standard weights their wght axis covers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 14:12:49 -04:00
AntonandClaude Opus 5.5 66b022cddd Web: fonts served next to the app, loaded on demand (#1875)
A host lists fonts in fonts/manifest.txt beside index.html (craft-fonts' manifest format, paths
kept inside the site); the web shell lists those families in the font menus and fetches a family
when it is picked or a layout needs it, then re-renders the affected type layers without a history
step. No manifest changes nothing; package.sh copies one in with PHOTOCRAFT_WEB_FONTS_DIR. The
style menu also lists the weights of a variable font's wght axis.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 13:14:47 -04:00
McClabe c58d20bd39 Fix small caps rendering across fonts (#1644)
* fix: synthesize small caps across fonts

* fix: use native small caps when available
2026-10-09 18:00:47 +07:00
McClabe 9b7349d0e5 Keep legacy PSD text origins visible when editing (#1560)
* fix: keep legacy PSD text origins visible when editing

* fix: normalize legacy TySh origins on import
2026-10-09 16:06:03 +07:00
d2ad238e82 PSD type layers without pixels are rendered on import; PostScript font names resolve to the installed family (#1406)
* PSD type layers without pixels are rendered on import; PostScript font names resolve to the installed family (#1405)

ag-psd, GIMP and other writers leave a PSD type layer without image data and
let Photoshop re-render it on open. The importer made those empty pixels the
layer's cache, so the canvas showed nothing until the layer was edited, and
the family guessed from the PostScript name ("CC Wild Words" for
CCWildWords-Regular) never got corrected to the installed family
("CCWildWords"), so Missing Fonts replaced an installed font.

- crates/io/src/psd_import.rs: a type layer's record pixels are the cache only
  when there are some.
- crates/io/src/text_import.rs (new, run by import): resolves each run's
  PostScript name with FontDb::resolve_postscript, as the layout does, and
  writes the installed family, weight and italic back into the run; renders
  every type layer that has no cache from its model.
- crates/io/tests/psd_type_bounds.rs: a type layer exported without pixels
  imports rendered, on the engine's own pixels; "JetBrainsMono-Regular" (guess
  "Jet Brains Mono") imports as the family "JetBrains Mono".

Fixes #1405

* PSD import: cap the automatic type render for untrusted sizes

text_import::prepare now renders a type layer without pixels only when its
ink fits MAX_RENDER_PIXELS (64 Mpx); the renderer's own guard is 256 Mpx,
which for an absurd type size or scale in a hostile PSD meant several GB of
f32 accumulation buffers on open. Over the cap the layer keeps empty pixels
until edited, as before. Test: hostile_type_sizes_without_pixels_import_without_a_huge_render
(an ink between the two caps, plus 1e30 pt, a 1e12 scale and f64::MAX).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* rustfmt (module order, the hostile-size test)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* PSD import: keep empty type pixels when the engine text is blank

cargo xtask test-corpus: rendering every pixel-less type layer regressed the
Photoshop oracle text/path-wave-open.psd (133 -> 132, below the floor).
Photoshop wrote that layer with no pixels because its EngineData text is
blank (the characters sit only in the descriptor's Txt ): it draws nothing,
and the import must not draw it either. New photocraft_text::psd::
engine_text_is_blank; the oracle passes again (133).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 20:22:25 -04:00
Minghang Chamling 925bdfe335 fix(text): keep tiny EngineData reals parseable (#979) (#1085) 2026-10-08 16:38:41 -04:00
Nigel Ihza Firdausy 1a9a39eadf fix(text): discover CJK system fonts through Flatpak host mounts (#1204) 2026-10-08 16:19:32 -04:00
Rage 7bb971f77c Fix PSD text fill after edits in Photopea 2026-10-09 01:57:17 +07:00
Alex GorevskiandCopilot e9142754a4 Fix text ink bounds overflow (#750)
* Fix text ink bounds overflow

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: e77cd34a-ce3e-4981-996f-4d37d49df90d

* Validate type size and tracking parameters

Reject out-of-range text metrics at type command boundaries to avoid silently empty renders.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: e85c024b-ace5-4415-9208-b8d4e4684505

---------

Copilot-Session: e77cd34a-ce3e-4981-996f-4d37d49df90d
Copilot-Session: e85c024b-ace5-4415-9208-b8d4e4684505
2026-10-07 17:12:55 -04:00
2baa95d684 Type: hitTest, caret and navigate commands so agents can edit text by position (part of #219) (#623)
* Type: hitTest, caret and navigate commands so agents can edit text by position (part of #219)

Stateless queries in document pixels. The word, line and hit-test helpers move from type_tool.rs into photocraft-text, and the Type tool calls the same code.

* Regenerate docs/scorecard.md after rebase

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 17:02:09 -04:00
9a448fe23b Type: a forced line break (U+0003) breaks the line instead of drawing a box (#644)
PSD type stores a line break inside a paragraph (Shift+Return) as U+0003,
and the layer text keeps it as is. The layout passed it to the line
breaker as an ordinary character: it isn't a break opportunity there, so
the line ran on, and the font has no glyph for it, so a missing-glyph box
was drawn in its place. Nothing showed until the text was re-laid out,
because an opened layer draws its saved pixels; any edit of the text, an
Image Size or an export at another scale then turned a two-line headline
into one long line with a box in the middle.

The layout now hands U+0003 to the line breaker as a newline (same byte
length, so every text offset stays put): the line ends, the next one
starts with the paragraph's leading and no paragraph spacing, and no
glyph is drawn. The text itself is unchanged, so PSD save writes it back.

Test: forced_line_break_starts_a_line_in_the_same_paragraph (fails
before: one line), covering point, centred and box text and rendering at
8, 16 and 32 bits.

Co-authored-by: EthanSuquet <292885762+EthanSuquet@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 16:31:33 -04:00
a4e96991f1 i18n: add French (fr) UI translation (#410)
* i18n: add French (fr) UI translation

A complete French catalog (menus, dialogs, panels, plurals), registered with its
plural rule (0 and 1 take the singular). Covers the latest commands, Stamp Visible
and Stamp Down included. A test checks the bundled UI fonts carry every French glyph.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* i18n(fr): translate labels added on main since this PR (from #582 by neko233-com)

Co-Authored-By: neko233-com <242357072+neko233-com@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* i18n(fr): translate the labels added on main since this catalog

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* i18n(fr): Camera Raw scopes and the Patch tool

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: neko233-com <242357072+neko233-com@users.noreply.github.com>
2026-10-07 07:49:30 -04:00
Brandon Thomas c161eaec73 Fonts: use craft-fonts as an optional build input; move the Japanese fonts out (#353) (#353) 2026-10-06 18:21:46 -04:00
Petr Leo Compel 5a6dd79852 i18n: add Czech (cs) UI translation (#328) (#328) 2026-10-06 11:29:44 -04:00
midasdfandmidasdf e5e2a8f2eb Enable dictionary segmentation for Japanese type (#250) (#250)
Co-authored-by: midasdf <205497066+midasdf@users.noreply.github.com>
2026-10-06 07:44:08 -04:00
Brandon Thomas c08322b7bc PSD manual and optical kerning: import, export, render, edit (#206) (#259) (#259) 2026-10-05 19:31:53 -04:00
Brandon Thomas c454af8d96 Text: deterministic font family order; hide private system families (fixes a flaky test) (#264) (#264) 2026-10-05 17:47:20 -04:00
Brandon Thomas ae80df2eb4 Vertical type (tategaki): lay out, render, edit and round-trip (#199) (#246) (#246) 2026-10-05 11:48:14 -04:00
Brandon Thomas 7f3d9ea68a Packaging: Linux runtime deps + start-up library check (#201); wasm 26.3 -> 18.8 MiB (#198) (#237) (#237) 2026-10-05 10:52:20 -04:00
Brandon ThomasandClaude Opus 5.5 5e6db41fc2 FreeBSD: serialise canvas GPU tests, CI template, RAM detection, CJK font paths (#227) (#227)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 09:58:58 -04:00
Brandon ThomasandClaude Opus 5.5 f5c6f4bb65 UI and Type tool CJK fallback: Japanese, Simplified/Traditional Chinese and Korean system fonts, lazily loaded in locale order (#149) (#185)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 07:23:03 -04:00
Brandon ThomasandClaude Opus 5.5 231bf5fe81 Flatpak single-file bundles as release artifacts (#163)
* Flatpak single-file bundle as a release artifact (#140)

packaging/linux/flatpak/ai.storyteller.photocraft.bundle.yml repackages the
release's Linux tarball (no Rust build, no network) on the freedesktop 26.08
runtime; packaging/linux/flatpak-bundle.sh builds it with flatpak-builder and
flatpak build-bundle and smoke-tests it. release.yml gains a Flatpak job per
arch that feeds the draft release and SHA256SUMS. The Flathub manifest moves to
26.08 with the same documented finish-args (portals + Pictures/Documents), and
packaging-lint checks the two manifests agree. Font scanning also looks in
/run/host/fonts and /run/host/user-fonts so host fonts show up in the sandbox.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Docs: installing the Flatpak bundle (#140)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Flatpak job: install the SVG pixbuf loader for appstream compose

Without librsvg2-common (absent on the x86_64 runner image) the host's
appstreamcli compose fails with file-read-error on the scalable icon.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* release.yml: the Flatpak job runs without the release environment

It only repackages the Linux job's tarball and signs nothing, so it doesn't need (and
shouldn't hold) the signing secrets while running third-party tooling.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 04:15:21 -04:00
Brandon ThomasandClaude Opus 5.5 3c54849ba6 Type tool: caret/selection land on the glyph under the pointer; font size is live, one undo step, ~60x faster (#123, #124) (#142)
* Canvas: tool gestures start at the press origin, not where egui recognised the drag (#123)

A drag is only recognised once the pointer has moved past egui's click
distance; the Down event used the pointer position at that moment, so a
Type tool drag-selection's anchor landed a glyph or more away from where
the button went down. Adds a text-engine test that the caret geometry
(clusters, lines) sits on the rendered glyphs for tracking, horizontal
scale, mixed sizes and wrapped, centred paragraph text.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Type tool: PSD type edits on the layout it shows; size edits are live, one step per drag, sized as shown (#123, #124)

- Clicking into a PSD type layer whose pixels are still Photoshop's
  re-renders it with our engine first (inside the edit session's history
  step; Cancel restores Photoshop's pixels), so caret and selection sit on
  what is shown. Hit-testing a type layer also accepts its shown pixels.
- type.edit / type.setStyle damage only the layer's old and new pixels, so
  the canvas recomposites that area instead of the whole document.
- Scrubbing a size (or any) type field coalesces per drag: live preview
  every frame, one history step per drag.
- Options bar and Properties show and edit font size and leading as the
  layer appears (size x transform scale, like Photoshop), and the options
  bar shows the size at the selection.
- examples/type_bench: 6000x4000, 50 type layers, size step went from
  297 ms to 4.6 ms (CPU canvas), 489 ms to 10 ms with drop shadows.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Crop test: the frame starts at the press origin now (#123)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 01:54:15 -04:00
Kentaro TsutsumiandClaude Sonnet 5.5 08512df255 Japanese support: IME input in the Type tool and CJK glyphs in the UI (#101)
Japanese could not be typed or displayed in the UI.

- Type tool: handle egui IME events (Preedit/Commit). The preedit text is
  written into the layer and replaced on every update, underlined while
  uncommitted, and the caret rect is reported through output.ime so the OS
  enables the IME and positions its candidate window. Without output.ime,
  egui-winit never calls set_ime_allowed, which is why only paste worked.
- UI fonts: append the first installed OS Japanese font (Hiragino, Yu Gothic,
  Meiryo, Noto Sans CJK JP, IPA...) to the end of every egui font family, so
  Latin text keeps Inter / JetBrains Mono and only missing glyphs fall
  through. No font is bundled; a missing font is not an error. Native only.
- Text engine: try Japanese fallback families before the SC ones.

Not covered: the web build has no CJK font, UI strings are not translated,
and text layers do not set a "ja" locale yet.

Tested: IME preedit/commit/cancel/stale-range unit test, font fallback unit
test, ui-egui and text crate tests, clippy -D warnings, cargo xtask layers.
Verified by hand on macOS (UI glyphs, IME composition, candidate window).

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-05 01:02:52 -04:00