* Skip compositing for geometric measurements
* Preserve simple PDF shapes and glyph source ranges
* Preserve text and shapes in PDF export (#10269)
- Export topmost simple text layers as vector glyph outlines with selectable text
- Embed ActualText unicode span and invisible text operators (mode 3 Tr) for 100% extraction
- Support stroked shapes in vector PDF export (centered solid strokes, dashes, line caps/joins)
- Retain glyph style indices in text rendering outlines
* Drop a product name from the glyph outline doc comment
* PDF export: keep path text and synthetic bold raster
* Fix PDF Unicode selection and respect font embedding permissions
* Render the text cache in PDF raster fallback fixtures
* Fix PDF vector selection eligibility and finite text matrices
* Preserve upper PDF vectors and reject overflowing curve controls
* Fix text shape import in PDF regression test
* Cover PDF Unicode map blocks and leading selection whitespace
* PSD: keep stylistic sets in Txt2 for PhotoCraft-native documents.
needs_txt2 now covers Txt2-only OpenType flags (ss01–ss20 and non-default TXT2_FEATURES). Fresh documents get a framed point-text object with full style runs so Set 1/5 round-trip on save/reopen (#10308).
* Txt2: seed fill colour and font list on PhotoCraft-native blocks.
Fresh style runs get a /53 paint dict, and an empty font list gets a CoolTypeFont entry so index 0 resolves (CodeRabbit on #10313).
* Txt2: tag SimplePaint and ensure numbered /0 on legacy blocks.
CodeRabbit: write /99 /SimplePaint on fresh /53 dicts, and create the numbered resource dictionary before pad_frames when a legacy block lacks it.
* Txt2: treat a missing stylistic-set mask as 0 when comparing styles
A style run without key 85 used to agree with any ssNN mask, so enabling
ss01 could leave the file's runs in place on save. Align with import's
default of 0. Also restore the G8 gaps note after rebasing onto main.
* style(io): rustfmt psd_kerning stylistic-sets test
* Txt2: keep Photoshop /0 stable when seeding fonts and frames
Only seed a CoolTypeFont list and inject /0/8 when the block is
PhotoCraft-native or already had those keys / needs new frames, so
unedited Photoshop Txt2 saves stay byte-identical (#2374).
* style(text): rustfmt oversized-template assert
* PSD: keep the Txt2 scaffolding to PhotoCraft-native blocks
A file's own Txt2 block (any Photoshop era) is written as before: no
numbered /0 or /0/8 frame list is injected, a layer without a template
object gets its stub, a run without /53 isn't given one, and no
minimal style base is used. Only a block PhotoCraft creates itself
(prev.is_none()) gets fresh point-text objects, minimal runs, a seeded
font list and a frame list.
Fresh objects now write one /5 paragraph run per paragraph (UTF-16
lengths, checked conversion), as edited objects are refitted.
Tests: Photoshop blocks with an empty or missing /0 get no native
scaffolding; the oversized-template test is back to main's assertion;
fresh paragraph runs; the PSD round trip checks ss01 and ss05 on every
run.
---------
Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
The rasterizer found each glyph's colour with a linear search of the
colours seen so far, then walked every glyph and decoration twice per
colour (bounds, then coverage) to draw that colour's items. A text layer
with a colour per letter cost colours x glyphs before drawing anything.
Build each colour's glyph and decoration lists in one pass, keyed by the
colour's bits, and draw each pass from its own lists.
* Type: add OpenType Numerals submenu (figure styles)
Type › OpenType › Numerals sets lnum/onum/pnum/tnum as a radio style
(Default through Tabular Oldstyle). Checkmarks follow the selection
start; TYPE-218-5 scorecard/gaps updated; parity floor 640.
* refactor(engine): one style-at-character lookup for the OpenType menus
feature_on_at and numerals_style_at each walked the runs the same way;
they now share style_at. NumeralsStyle::from_id and numerals_style_of
read the existing tables instead of repeating them.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(text): persist OpenType numeral tags through PSD EngineData
Map lnum/pnum/tnum as EngineData bools and the width×height combos as
FigureStyle (Krita/Illustrator), so Numerals choices survive PSD round-trip.
* docs(gaps): note Numerals PSD figure-tag round-trip
* revert(text): keep PSD EngineData to the keys Photoshop writes
Backs out the PSD half of 18610324 and its gaps note (58813de3); the
engine reformat in that commit stays.
- `LiningFigures`, `ProportionalFigures` and `TabularFigures` are not
Photoshop EngineData keys (the commit says Photoshop only has
`OldStyle`), so writing them puts keys of our own into users' PSDs.
- The `/FigureStyle` code table (1-4) has no oracle: no Photoshop-authored
file in photocraft-corpus, and neither ag-psd nor psd-tools reads it.
AGENTS.md rule 3 asks for public specs or observation. PSD persistence
of the numeral styles can come back with a Photoshop-authored oracle.
The Numerals menu still round-trips through .pcraft.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* test(text): lock PSD EngineData to Photoshop OldStyle for numerals
Document AGENTS.md clean-room: only onum survives TySh round-trip; lnum/pnum/tnum stay .pcraft-only until a Photoshop-authored corpus oracle.
---------
Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
The reference application marks a paragraph box that can't hold its text
with a black "+" inside an enlarged lower-right handle; nothing is drawn
below the box. The PR drew a red disc 0-9 px below the box at a fixed
screen offset, so on a rotated layer it came loose from the box, a click
on it either grabbed the corner handle or ended the edit, and its hover
card popped up while dragging that same corner to enlarge the box.
Now the lower-right handle grows to 11 px and carries the black "+",
centred on the transformed corner, and the whole enlarged handle grabs
that corner, also in a rotated view. The disc, the hover card (state, its
tooltip row in every catalog), the GPU pixel test and its pollster
dev-dependency are gone; detection (`TextLayout::overflows`) and the
`typeBoxOverflow` inspect flag stay. docs/ui-parity.md: the overflow row
no longer claims box threading, the Guides row the PR duplicated is
removed, and the history has one row in date order.
Tests: `an_overflowing_box_shows_a_plus_in_its_enlarged_lower_right_handle`
(upright and rotated box; fails on the PR head with a 7 px handle) and
`the_whole_enlarged_overflow_handle_grabs_in_a_rotated_view`.
Co-authored-by: VladimirPozdnyakov <49595925+VladimirPozdnyakov@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Main now carries #9031 (Halftone) and #8923; this branch keeps only what it adds on top: the guard for a setting beside a Gallery stack's effect list, its test, and the PSD open warning for patterns that paint nothing.
Preserve the PR's intended behavior and regressions while retaining upstream module extraction and changes. Formatting and independent static review passed.
Local verification: 247 text tests passed (5 ignored), including reordered duplicate-font metadata regressions. Clippy and WASM did not start because the local disk safety threshold was reached.
Local UI verification is incomplete. Hosted CI must establish the remaining gates; this commit does not claim the PR is ready to merge.
Merge #9104 (raw EngineData strings are written back as the same bytes) so a style-only type edit does not re-encode Photoshop's raw strings as UTF-16, with tests for the numbered and the legacy restyle paths. The over-budget pattern warning is written without a panic path and explained where the lock is held.
A pattern past the per-call budget is converted under one lock and shared while in use, with a one-time warning, and the desktop budget keeps every pattern a call uses (1 GiB). The web build caps a pattern's working copy at 128 MiB and a call at 256 MiB, and the pattern readers reserve their memory fallibly on every target. The legacy text restyle writes and compares BaselineShift, FontCaps, FontBaseline, UnderlinePosition, StrikethroughPosition and the ligatures per run, keeps an automatic leading's own value, and import reads the legacy AutoKern; a corpus test edits the size in psd-tools' text.psd and checks the rest of the legacy object is unchanged.
Align measures shape extents at the document's own sample type, a render keeps two of the largest patterns converted per call instead of rebuilding the second for every tile, a pattern too large to paint is named by every command that would use it, by pattern.list and by a PSD open warning, and a style-only edit restyles the older Photoshop's legacy text objects too, checked against the real psd-tools text.psd.
* fix(text): lay out a variable font's named instance from its PostScript name
A PSD type style set in a named instance of a variable font, such as
Bahnschrift Light Condensed, keeps the instance only as its PostScript
name ("Bahnschrift-LightCondensed"); its variations are empty. When
PhotoCraft re-laid the layer out, FontDb::select_named_face looked for a
face with that PostScript name, or one whose style and weight matched. A
variable family is a single face, so neither matched, and the text was
laid out at the font's default instance: about 35% wider than the
instance Photoshop drew with Bahnschrift.
select_named_face now falls back to the family's variable faces when the
PostScript name matches no face. It reads each face's fvar named
instances and compares the name with the instance's own PostScript name,
or, when the instance has none, with the name Adobe Technical Note #5902
derives (variations prefix or family name, a hyphen, then the subfamily
without spaces). Names compare without punctuation or case. On a match,
that face is selected and the instance's axis values are added to the
style's variations, keeping any axis the style already sets. Static
faces still match exactly as before, and PSD import is unchanged.
Tests:
- font_names_tests::postscript_name_of_a_named_instance_selects_its_axis_values
rebuilds the bundled Inter at test time as a variable font (fvar with
wght/wdth axes and two named instances, one with its own PostScript
name and one derived) and checks the axis values each name selects, an
unknown name, and a style's own axis value. It needs no installed font
and no font file in the repository.
- font_names_tests::exact_static_face_gets_no_instance_axes checks that
an exact static match gets no axis values.
- tests::postscript_name_of_a_variable_instance_lays_out_at_that_instance
lays out "Hamburgefonstiv 0123" with Bahnschrift (Windows) or Ubuntu
Sans (Ubuntu) when installed and checks that the instance's PostScript
name gives the same width as its axis values. With Ubuntu Sans before
the fix: UbuntuSans-CondensedLight 980.3 px (= the default instance),
its axes 766.4 px; after the fix both are 766.4 px.
A style with only a weight (no PostScript name) is still not turned into
a wght axis value on a variable font; the issue reports that too, but its
fix and test are about the PostScript name, so that is left for a
separate change.
Fixes#8797
* fix(text): try a variable font's named instance before the style fallback
Review pointed out that select_named_face ran the style-and-weight
fallback before the named-instance lookup. A style that carries a named
instance's PostScript name and also a populated font_style matching the
variable face itself ("Regular", 400) selected that face at its default
instance, and the instance's axis values were never added.
Faces are now tried in order: exact PostScript name, named instance,
then style, weight and italic.
Review also noted that the layout-width test only runs when Bahnschrift
or Ubuntu Sans is installed. The synthesized variable font in
font_names_tests now carries an HVAR table that narrows every advance on
wdth, so layout width shows which instance was used. Two tests run
everywhere:
- named_instance_wins_over_a_matching_font_style
- postscript_name_of_a_named_instance_lays_out_at_that_instance
Both fail before this change. The installed-font test is kept as a check
against a real font.
---------
Co-authored-by: John Turnbach <jturnbach@users.noreply.github.com>
The rasteriser made one coverage pass per distinct colour over the whole
text's ink rect (nine with faux bold), so text with many coloured runs
cost colours x area: 400 colours of faux-bold 48 pt text took 13.0 s.
Each colour's pass now covers only that colour's glyph bounds (all bold
offsets and decorations included) and composites into that part of the
accumulator: 163 ms for the same text. A test compares the visible
(premultiplied) pixels with full-size passes for plain, faux bold,
underlined, rotated and warped text in RGB and CMYK.
Part of #8602
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Photoshop reads a type layer's character style from the document's Txt2
block, not from the layer's TySh. build_txt2_at kept a Txt2 object as it
was whenever its text still matched the layer, and built new style runs
only for edited or new objects. So a type.setStyle with no text change
(font, faux bold, size, ...) was saved into the TySh but not into Txt2,
and Photoshop reopened the layer in its old style. The same setStyle
together with a type.edit did reach Photoshop, because the object then
took the edited-object path.
An object is now kept only while its style runs agree with the layer's
character runs on every key the writer regenerates: font (by name through
the block's font list), size, faux bold and italic, leading, scales,
tracking, fill colour and the OpenType keys. The auto-kern mode is still
refitted separately. A key the file's run lacks, or holds as another
type, is skipped, so an unedited save still returns the file's block byte
for byte (#2374). When a key disagrees, the object goes through the
edited-object path: the file's structure is kept, its style runs are
rebuilt from the layer, and the glyph pen positions laid out in the old
style are dropped.
Regression test: text::tests::style_only_edit_restyles_the_kept_txt2_object
builds a Photoshop-shaped Txt2 (Arial-Black, faux bold on) and checks that
an unedited layer gets the block back byte for byte, that turning faux
bold off writes /2 false, and that a font change plus faux bold off names
AppleSDGothicNeo-Heavy in the runs. It fails on main (faux bold stays
true) and passes with the fix.
The io helper add_type_layer in psd_kerning.rs now applies the file's
Txt2 to its kept layers, as opening the file does. Before, those layers
contradicted their own Txt2 objects (no salt where the object had it on),
which the old keep-whatever-the-file-had behaviour hid.
Fixes#8415
main now has its own Recent Fonts (#3559), so this branch drops its separate
recent list (prefs `recentFontList`, `type.font.noteRecent`, `type.font.clearRecent`,
the five-font cap) and its font menu uses main's: the list is
`PhotocraftApp::note_recent_font`'s, the rows and arrow keys main's `font_menu_rows`,
and every applied family is recorded, as main does. Favourites, the class and
similar/variable filters, the style expander, format badges and the live preview
stay. Faux Bold keeps the one-pass thickened outline on main's fallible coverage
buffers (#6001), budgeted as a plain pass. Both sides' strings and icon credits are kept;
the scorecard is regenerated.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
refine_legacy_point_origin moved the layout's logical box (line x0,
baseline - ascent) onto the TySh boundingBox, but Photoshop writes
boundingBox as the tight ink box (Arial 120 pt "..." has bounds top
-102.98, boundingBox top -11). Legacy point text whose ink starts below
the ascent line ("...", "_", lowercase) opened up to ~116 pt too low.
Match like with like: use the layout's unrounded text-space ink bounds
(new render::ink_bounds). The PR's tests built boundingBox from the
logical box, so they now write bounds = layout box and boundingBox =
ink box and pin the exact anchor; legacy_origin_lines_up_ink_with_the_
ink_bounding_box covers "Name", "..." and "_" and fails without this.
Co-authored-by: bogusyogi <102761952+bogusyogi@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Main merged #5940, #6472, #7838 and #7922 directly. Kept from the batch:
the snapshot/History-follow identity at the history limit (#5940,
c7ef3676a; main still had the 3-tuple HistorySpot) and the strokeless
shape at a 0 px width step (#6472, 962539607; main still added the
stroke). Main already carries the other two fix-ups (34c9d0ea4, dff8bfd0e).
Overlaps with main resolved:
- #5842 vs main's #5568: one path, apply_color, tool-gated as #5842
has it, with #5568's float fill, kept fill opacity and lock skip;
shape_fill_ui's duplicate foreground_changed/gesture dropped; #5568's
tests run with a shape tool.
- #6230 vs main's #5180: one Type tool path click (path_target), no
second type.attachToPath; vertical type does not start type on a path,
as main has it; main's path_at/path_cursor and canvas arm dropped.
- #5452 moved the shape swatch to shape_paint_ui.rs: main's hex field
(#6201) ported there.
- type_extra_cmds::with_text takes main's dpi form, pub(crate) for
type_area_cmds.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Conflicts, both sides kept: main's .act swatches next to the batch's .asl
styles (Open filters, preset extensions), main's Render smart filters next
to the batch's Distort ones (53 mapped), parity doc status lines.
docs/scorecard.md regenerated.
Follow-ups the merge needed: the Distort smart filter readers pass their
enum type to main's stricter enum_name (the type their writers use), and
the CMYK effect tests set main's new StrokeFx overprint and Bevel
gloss_anti_alias.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Integration: resolve with main for area type (#6230)
- Image Size with a new resolution (#7124, canvas_geom::set_resolution) now scales an area type's
path with the text space like a box or a type path, so the same words wrap onto the same lines;
test a_resolution_change_keeps_area_type_wrapping_where_it_was fails without it.
- The Paragraph panel's justify buttons (#7713) stay available on area type, which is paragraph
text in Photoshop; test justify_buttons_work_on_area_type.
- Test files: main's tests plus #6230's; layout.rs imports joined; docs and scorecard regenerated.
- TextLayout::overflows returns false for empty text: a box too short for
even one paragraph has nothing to drop, so an empty box never shows the
port (test added).
- The port's hover clock clears wherever the port doesn't draw this frame —
dead sessions, a fitting box, point text — so a returning overflow waits
for a fresh hover instead of showing the tooltip instantly.
- The tooltip measures its card and flips left near the screen's right edge
and above near the bottom edge, keeping the guidance on screen.
- ui.inspect answers typeBoxOverflow, so automation can read the promised
flag without a screen; tests/type_overflow_port.rs probes the adapter
separately so a build panic fails the test instead of reading as a skip.
- text::TextLayout::overflows: a box's layout drops the overflowing lines, so
any character past the last line's byte range is dropped content; trailing
breaks that fit gain their own empty lines and never read as overflow.
- type_tool::draw_overlay: boxed text overflows draw a red + port on the box's
bottom edge near the right corner half below it, with the cursor-outline
white rim; hovering it for the tooltip delay paints a card explaining the
fix (theme tokens, tooltip preferences honoured); leaving clears the hover
clock and the delay's tail repaints, so the hint never shows late.
- ui.type_box_overflow (serde skip): automation reads the overflow without a
screen; set each overlay frame, false without a session.
- Tests: text overflows_says_whether_a_box_holds_its_text; ui unit
the_overflow_port_flags_a_box_that_cannot_hold_its_text (true / fitting
false / point false); GPU integration tests/type_overflow_port.rs checks the
red disc on screen and the hover card (skips without an adapter). The tl!
string is translated in all twenty catalogs per the i18n gate.
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.
When area type overflowed a closed shape, a word too wide for the narrow
bottom rows of an ellipse stayed in its span and was drawn past the outline
(a 260x160 bubble at 12 pt drew "misunderstanding" from x 110 to 212 over a
110..149 span). The shape's wider rows would hold such a word, so the shape
has just run out of room: that line and the rest are now hidden as overflow.
The check looks at every row of the shape, not just the paragraph's own (a
later paragraph starts below the wide top of a funnel), and also runs when
the line-breaking pass budget is spent. Only a word no span in the shape
holds still overflows its span.
The TYPE-218-10 note said such a word overflows "the way box text does";
since #6694 box text breaks it at the edge. Corrected, and the other known
gaps (first line about 0.24 em lower than box text, PSD area type opening as
box text, no handles for the area path) are listed.
Co-authored-by: Silly Silk <31193727+SillySilk@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <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>
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>