contributors/people.toml: union of both sides' entries.
crates/ui-egui/src/canvas.rs: main's Move cursor (arrow) plus the PR's Type path-cursor arm.
crates/ui-egui/src/type_tool.rs: main's has_transform in draw_overlay; the PR's earlier `shape` binding kept, the duplicate binding dropped.
crates/ui-egui/src/vector_ui.rs: main's draw_overlay (paths already show with any tool), the PR's vector_tool line dropped.
docs/gaps.md: header line from main, G8 text from main with the click-on-path clause, change-log lines of both.
Older Photoshop keeps each type layer's text object at
/DocumentObjects /TextObjects (named keys, run lengths under /Length) and
re-renders from it. type.edit refreshed TySh and the numbered /1 /1 list but
left the old text there, so the layer came back with its old text.
build_txt2_at now refits that entry like edited_object does for the numbered
ones: the model text, one ParagraphRun run per paragraph, every other run
array cut or extended to the new UTF-16 total. Entries are only edited in
place (never grown), a malformed run array leaves the entry as the file has
it, and an unchanged block is still written back byte for byte. A legacy
block holding both lists with a stale older copy is repaired on re-save.
A normal big-endian TySh that the typed reader rejects (an unknown text
descriptor version, a stub from another writer) opened as a text layer on
main, with the best-effort text and its TySh saved back verbatim. With the
PR it became a raster layer, and the PSD export strips TySh from rasters,
so open + save turned the type layer into pixels for Photoshop. Now only
foreign-order blocks and unsafe transforms become pixels; the old fallback
runs under the same transform gate (shared as
photocraft_text::psd::transform_is_safe). The warning no longer claims the
type data is kept.
Test: canonical_type_the_typed_reader_rejects_stays_type_and_saves_its_block
(descriptor version 17, ordinary matrix) fails without this change.
Co-authored-by: Sean Lavery <702713+TheSeanLavery@users.noreply.github.com>
Co-authored-by: Tanish Manku <13985398+tanish2k09@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
crates/text/src/psd.rs: keep both appended test modules (transform_safety_tests
and main's nonfinite_tests). docs/file-format-parity.md: add the #4758 item to
main's current change line.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The base run comes from the file and is cloned once per character run, so
it now has to fit the template node budget, and each font is looked up
once per name. Also names the reopened-layer tuple in psd_kerning for
clippy::type_complexity.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Photoshop reads the character style from Txt2 and quit on the bare run
(`/0 0 /11 mode`) that new objects got. New and edited objects now get one
style run per character run, built on one of the object's own runs with
every key kept, and font, size, faux styles, leading, scales, tracking,
fill colour, OpenType keys and auto-kern mode set from the layer. A font
the block's font list lacks is appended to it. Edited objects therefore no
longer keep the old text's features by position.
build_tysh no longer writes TextIndex 0 for a layer without one: the
export took it as a kept number, so a Type tool layer landed on object 0
(a deleted layer's, or the one of a layer above it). set_text_index puts a
missing TextIndex right before EngineData, where Photoshop writes it.
pad_frames pads to at most MAX_TEXT_INDEX + 1 frames.
edited_text_keeps_the_files_txt2_object (#7220) now expects style runs
that follow the layer's runs and size.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A new object kept the template's style runs, and PhotoCraft's reader takes
font features from Txt2, so a new type layer came back with the template
layer's features (salt, ss01). Its style runs are now generated like the
stub's; only the template's structure is copied.
New type layers were numbered past the kept TextIndex values only, so after
deleting the layer with the highest index a new layer landed on its object
and took its style. They are now numbered past the file's Txt2 objects too.
txt2_extras_survive_a_resave_of_unchanged_text now gives its layer the TySh
index a file's layer has; without one the layer is new and no longer lands
on object 0.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
new_object copied the template's rendered strikes, whose frame strike
(/99 /F) still named the template's frame in /10. It now names the new
object's frame, like the view's /0.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The stub filter applied only to the kept object, so an edited layer whose
slot held a stub from an earlier save went through edited_object and kept
the bare stub. The slot is now filtered before both lookups.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A type layer the file's Txt2 has no object for (one added in PhotoCraft)
was written as a bare stub with no view, no paragraph runs and no frame
in /0 /8 /0. Photopea and Photoshop refused the file once the objects
outnumbered the frames.
build_txt2 now gives such a layer an object shaped like one of the
file's own (refitted with edited_object from #7220), with its view
naming its own frame, and pads the frame list to one per object. A stub
written by an earlier save is replaced the same way, so re-saving an
affected file repairs it.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The installed-font scan read only /System/Library/Fonts, /Library/Fonts and
~/Library/Fonts (plus PingFang). Fonts macOS offers from anywhere else, such
as those a font manager, Adobe Fonts or Office activated, never reached the
font menus although other apps showed them.
load_system_fonts now also registers every file Core Text offers. The list
comes from fontique's Core Text backend, enabled for macOS only, used as a
scratch collection whose families are discarded: only its file paths are read
and registered in our own collection, so generic-family defaults and the merge
of installed faces into the bundled families are unchanged. The reserved UI
fonts under /System/Library/PrivateFrameworks are skipped (fontique cannot
draw their outlines).
On a stock macOS install the folder scan already listed all 183 Core Text
families; the gap is fonts Core Text knows from elsewhere. With Hack activated
from a scratch folder via CTFontManagerRegisterFontsForURL, the list had 313
families before and 314 after (Hack added, none removed), type.fonts lists it
and a type layer renders in it. The scan stays 37-57 ms.
Tests: font_files_of_lists_each_font_file_once (all platforms);
coretext_lists_the_system_font_files and every_family_coretext_offers_is_listed
(macOS). The last fails without the call when a font is activated elsewhere
("lacks: Hack") and passes with it.
Cargo.lock gains fontique's optional per-platform dependencies for the
`system` feature; `cargo tree` shows the feature only for aarch64-apple-darwin.
Still open: Windows fonts outside the two folders (#3336) and the web build.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
When a type layer's text changed, build_txt2 replaced the layer's object in
Photoshop's own Txt2 block with a bare `<< /0 << /0 text /6 runs >> >>`:
no paragraph runs (/5), no view (/1, which names the object's frame) and none
of the style keys. Photoshop 27.10 quits while opening such a file
(layer_effects.psd after type.edit), as it refused stripped objects in #2374.
An edited object now starts from the file's object at its slot: the text is
replaced, the paragraph runs are remade one per paragraph of the new text,
and every other run array of the model (style runs, ...) is cut or extended
to the new length before the auto-kern modes are applied. The view and other
keys are kept; only the old text's glyph pen positions (/21) are dropped.
An object whose run arrays aren't shaped as Photoshop writes them still falls
back to a fresh one.
Test: text edited_text_keeps_the_files_txt2_object.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
build.rs embeds every font in craft-fonts' fonts/manifest.txt, but
FontDb::new registered only the entries whose scripts include Jpan.
craft-fonts 8dcdacd5 adds Noto Sans CJK SC and Noto Sans Arabic, and
3d9ab3a1 adds Noto Sans Thai. Built with either checkout, the binary
carried those fonts, but the font list did not show them, text layers
did not fall back to them, and works_without_craft_fonts failed with
"Noto Sans CJK SC registered under its manifest name". Chinese and
Arabic set in Inter in a TextEngine::new() engine drew .notdef glyphs.
FontDb::new now registers every CRAFT_FONTS entry. A single-face file
registers under its manifest family, whatever family its name table
gives; each face of a collection keeps the family its name table gives.
With Noto Sans CJK SC built in and no Simplified Chinese system font,
Japanese text set in Inter takes Noto Sans CJK SC when the UI locale is
not Japanese, following cjk::script_order.
craft_fonts_cover_japanese_without_system_fonts now expects the first
CJK craft family in that order.
New tests register in-memory craft entries made from Inter (Hans, Arab
and Jpan entries, two whose name tables give another family, and a
collection), and lay out Chinese, Arabic and Thai in Inter when the
build has a craft font for the script.
A type render spends most of its time after the glyphs are drawn: composing each
colour's coverage over the premultiplied accumulator, dividing it back to straight
colour, and converting the tile bytes. All three are per pixel - no pixel reads a
slot another wrote - so they ran one after another on one core for no reason.
Measured on a 3.75 MP paragraph (4000 characters, 14 pt at 300 dpi, 1851 glyphs),
release, Windows 11, i5-10300H: rasterize 98.3-103.9 ms before, 60.3-61.8 ms after
(1.6-1.7x). Whole-render stage profile before: ink rect 4.1, draw 18.6-22.9, prefix
sum 7.0-8.1, composite 22.6-33.4, unpremultiply 11.2-14.0, write_region 23.8, prune
3.1 ms.
The glyph drawing, the winding prefix sum and the ink-rect pass stay on one thread:
the first two carry a running accumulator across pixels, and the band split that
would keep them bit-identical costs a second glyph traversal per band.
Native only (rayon, as the other crates): the web build keeps the serial loops.
Coverage::finish allocates its output while the accumulator it reads is
still live, so a plain pass peaks at two coverage buffers rather than
one. The budget charged one, which contradicted finish's own doc comment
and left the plain path under its real peak. The bold path's three
buffers were already right.
The exactness assertion in the budget test now pins 18 bytes a pixel for
a grey 8-bit plain render, and fails if the undercount comes back.
Caught in review by @FredApps.
A TySh warpValue, warpPerspective or warpPerspectiveOther beyond f32 became inf and the warp was silently dropped; they are now clamped to -100..100 like type.warpText.
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Horizontal scale was applied after line breaking, so paragraph text below
100% wrapped early and text above 100% overflowed the box. Break box text
at the available width divided by the paragraph's horizontal scale (the
largest one when runs mix), scale the first-line indent and alignment
offset back to the box, and keep the scaled trailing space out of the
line extent.
Fixesstorytold/photocraft#5565
Co-authored-by: Cursor <cursoragent@cursor.com>
parley's default overflow wrap only breaks at UAX #14 opportunities, so
a word longer than the paragraph box ran out past its edge and resizing
the box changed nothing. Paragraph text now sets OverflowWrap::BreakWord,
so such a word wraps at the box edge as Photoshop does; a space or hyphen
still wins over a mid-word break, a box that fits the word lays out as
before, and point text is untouched.
Unicode's line-breaking rules allow a break between any two Hangul
syllables, so Korean box text split words (설 / 정이나). Korean is set
with breaks at the spaces between words, as Photoshop does: runs of
Hangul now lay out with WordBreak::KeepAll. Chinese and Japanese keep
breaking between characters.
rasterize_warped guarded its size with MAX_PIXELS alone, which says nothing
about bytes. The buffers it keeps live at once are all as deep as the format:
the f32 accumulator and the output surface are both stride deep, a finished
coverage buffer sits beside them for the colour being drawn, and each faux-bold
pass holds a second Coverage and the vector its finish builds. A CMYK bold
render at f32 therefore reserves several times what a plain grey one does for
the same pixel count, and every one of those buffers went through vec! or
Vec::with_capacity, which abort instead of returning.
Admit the peak before reserving any of it: fits_raster_budget derives it from
the stride, the sample size and whether a bold pass runs, refuses an overflowing
size rather than wrapping, and is checked against MAX_RASTER_BYTES (4 GiB, 1 GiB
where usize is 32 bits, so the wasm build keeps room for the document). Coverage
now reserves fallibly and finish returns an Option, and the accumulator goes
through a fallible zeroed(). A refusal returns the empty Rendered the oversized
path already returned, so callers see the existing "too big to draw" result
rather than a crash, and the document and its cached appearance are untouched.
Ordinary text is unaffected: the budget only binds above what MAX_PIXELS
already allowed for narrow formats.
Test: the peak tracks channels, sample width and faux bold against small
injected allowances, an overflowing size is refused, and a buffer that cannot
be served reserves nothing.
CharStyle had no superscript/subscript attribute, so PSD import ignored FontBaseline and the first re-render (entering text editing) drew the run as plain text. Model it as CharStyle::baseline, read and write FontBaseline in EngineData, draw such runs at 58.3% of their size shifted by 33.3% of it (Photoshop defaults), and accept "baseline" in the character params.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A Type tool click on a shown path (the active shape layer's, else the work
path) starts type on that path at the point clicked, as in Photoshop: one
history step, placeholder selected, the paragraph alignment kept about the
click. Over such a path the pointer is the path-type I-beam, and the Type
tool shows the paths as the vector tools do.
- text: warp::arc_length_at, the arc length of the nearest point on a path.
- engine: type.attachToPath takes "at": [x, y] (a document point) instead
of "start".
- ui: the caret, selection and underline follow the curve; a click in path
type puts the caret at the glyph there; glyphs past the path end are not
highlighted.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(text): split hyphenless PostScript style suffixes
A PostScript name without a hyphen kept its style in the guessed family:
`GmarketSansBold` became the family "Gmarket Sans Bold" at weight 400.
The guess now drops a trailing capitalised style word (Gmarket Sans, 700).
A lowercase ending ("Daylight") is not a style, and a style word after the
vendor tag MT stays in the legacy family ("Arial Rounded MT Bold").
Hyphenated names keep the last-hyphen parsing from #3878.
PSD import stores this guess when the exact face is absent. A legacy family
that ends in a style word without MT before it ("CooperBlack") is stored as
the shorter family (Cooper, 900) until the face is installed.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(type): missing-font checks follow layout's PostScript resolution
When a run's stored family is not installed, layout draws it with its exact
PostScript face, or with the family guessed from that name if installed.
Missing-font detection compared only the stored family label, so such a run
was reported missing, and Replace Missing Fonts rewrote it whenever an
unresolved run shared the label. missing_fonts, missing_fonts_in_layer and
the replacement now use layout's decision.
#1406 already corrects the label when a PSD or Affinity file is opened with
the face installed, and #1844 finds exact faces outside the family prefix.
The label still stays on a run reopened from .pcraft after its font was
installed, on fonts that arrive after opening, and on a run created with
both "font" and "postscriptName". The tests take the .pcraft and command
paths.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: kijoong <gijoong.gim@gmail.com>
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>