Enable hypher/norwegian in designcraft-compose so no/nb/nn map to the bundled hyph-no.tex patterns. Add unit + composition regression tests, NOTICE attribution, and update parity docs.
Fixes#535.
An article's closing tombstone - a one-character text frame, here a 7.6 pt
Noto Emoji symbol in a 10.5 pt box - was overset (red +, nothing drawn)
whenever its spread had a text-wrap object anywhere, while InDesign sets it.
Two causes in the wrap code:
- Every text frame took every wrap object on its spread as an exclusion,
whether or not the object reached the frame. Now only objects that
overlap the frame wrap its text.
- free_slot skips gaps narrower than 1.5 em of the paragraph's size
(slivers beside a wrapped object), but applied that to the whole column
too when nothing cut it: with a 14 pt paragraph style, no line could go
in a frame narrower than 21 pt. The minimum now only applies to gaps a
wrap object cut; a band no object reaches gets the whole column, however
narrow.
On the document that showed it (an InDesign 2026 IDML with 12 tombstones and
two unrelated wrap objects on the spread), the 12 tombstone stories are set
and no other story's layout changes (54 -> 42 overset stories, all of the
difference the tombstones).
Tests: small_single_character_frame_sets_beside_unrelated_text_wrap and
narrow_frame_sets_lines_no_wrap_object_reaches fail before (overset) and
pass after.
Probably also #476 (Wingdings bullets overset in 20 x 21 pt frames).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CgGsJkVe5GYG9qK6VFdECa
#557 built an anchored object's exclusion as its offset bounding box; next's Exclusion also has a side and a contour, built by wrap_obstacles for page items. wrap::obstacles keeps the object and its parent → spread transform, and wrap::exclusions runs wrap_obstacles with it, so anchored objects get sides (spine sides by page), contours (Same As Clipping from the clip), custom boundaries, Jump Object and the wrap of a graphic inside, each tagged with the anchor byte (after). Without this the merge doesn't compile. Test: anchored_object_wraps_with_its_contour_and_side.
Conflict: ROADMAP.md: both progress lines
Conflict: docs/target-app-parity.md: next's header, anchored objects wrapping text added to its change list
Conflict: docs/typography-parity.md: next's header (dated 2026-10-12) with #557's change added; Text wrap row: next's text and caveat, plus "on page items and anchored objects"
Conflict: crates/compose/src/lib.rs: next's Exclusion (side, contour) with #557's after field (Exclusion::wraps_line; next's constructors and the tests' helper set None); next's contour_path and wrap_obstacles in frame_specs; next's line loop (carry, grid, wrap_slot) with the line's start byte passed to wrap_slot; wrap_slot, free_slots and estimate_slots take line_start and skip exclusions that don't wrap the line (Jump Object test and in-band edges included); #557's re-break trigger stays in the line loop
Conflict: crates/compose/src/cache.rs: next's ComposeOptions (section_marker) with #557's neighbour compositions
Conflict: crates/compose/src/tests.rs: both test blocks
Conflict: crates/render/src/damage.rs: #557's test before next's patched_diff helper
An anchored object with text wrap pushes aside the text of other stories
whose frames it overlaps on its spread, and the lines of its own story
after the line holding its anchor (custom and above-line objects). Its own
story is composed again with the objects where the previous pass placed
them until they settle, at most four more times. The composition cache
keys a story wrapped by other stories' anchored objects on their
signatures, and canvas damage repaints the stories they wrap.
#384's contour wrap followed the frame's path for Same As Clipping, which was all a graphic could clip to; #450 gives graphics clipping paths. contour_path takes the clip region (Graphic::clip_region) when the graphic has one. Test: a triangle clip widening downwards, Same As Clipping, left side: lines beside the narrow top run further in than those at the base.
Conflict: ROADMAP.md: both progress lines (next's, then #556's)
Conflict: crates/compose/src/shape.rs: #556's empty-marker zero-width glyph; next's digit mapping, styles.at(i)/styles and sub.upright() in both arms
Conflict: ROADMAP.md: took #83's (main's restructured roadmap); the Threading inventory line goes into docs/target-app-parity.md
Conflict: crates/compose/src/lib.rs: kept this branch's undrawn trailing spaces after warichu; comment covers both the spaces and the drop cap
Conflict: crates/idml/src/tests.rs: took #83's (drop cap style as a Dropcap nested style, table style fills through the cascade); this branch has no changes of its own there
Parent-page text now uses the absolute page index as its cache key, so repeated page numbers and renumbering do not reuse another page's composition.
Validation: targeted canvas and PDF regressions, plus all six cargo xtask ci gates on main after #514 (1,179 native tests passed, 3 ignored; 15 crates passed WASM compilation checks).
Task: #523
Conflict: ROADMAP.md: took main's restructured roadmap; this branch's inventory changes (Keep Lines Together default, IDML defaults, drop caps, IDML Kashidas) go into docs/target-app-parity.md, where the inventory lives now
Conflict: crates/format/src/lib.rs: load runs the format upgrade, then merges duplicate assets; kept both sets of tests
Honor the capitalized-word hyphenation setting for uppercase words and retain the current roadmap entries.
Validation: all six cargo xtask ci gates passed on the current main merge candidate; 1,170 native tests passed and all 15 WASM packages checked.
Task: #538
Co-authored-by: skylartaylor <26588250+skylartaylor@users.noreply.github.com>
Text set in Portuguese ("Portuguese: Brazilian", "Portuguese", pt_BR) got
no hyphenation, so justified Portuguese columns could only stretch their
word spaces (up to 335 % of the normal space in a 56 mm column of 10 pt
Times).
Turn on hypher's "portuguese" feature, beside Spanish: hyph-pt.tex 1.4
(2024-07-13) from hyph-utf8, BSD-3-Clause, credited in NOTICE.
Lang::for_language already maps the Portuguese names and locale codes.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
A word typed in capitals was never hyphenated, so a heading such as
FERRUM COMMODI UNTI moved the whole word to the next line where InDesign
breaks it (COM-MODI). Capital words now follow Hyphenate Capitalized
Words like words with a capital first letter: on by default, and turning
it off keeps both, acronyms included, whole.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Keying parent-page compositions by page index (previous commit) dropped the
page name from the cache key, and the signature only counts sections, so a
page-number marker on a parent page kept its old text after a section's
start number, style or prefix changed (renaming the page without changing
its index). The page's name is now hashed into the signature whenever a page
is given, so renumbering recomposes it as it did before, while two pages that
share a name stay separate.
Test: parent_page_number_follows_section_renumbering (marker on a parent
page, composed for page 3; the first section restarted at 100) fails before
(still "3", 1 glyph) and passes after ("102").
Reported by aiongg on #523.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CgGsJkVe5GYG9qK6VFdECa
A running header (or any text variable) in a parent-page frame was empty or
wrong on pages whose page number repeats an earlier one: with two sections
that both number from 1 (front matter 1-10, body 1-...), body pages 1-10
showed nothing. The renderer and PDF export passed only the page's name to
the composition cache, which looked the page up as the first page with
that name - in the front matter, before any heading.
CompositionCache::get now takes the absolute page index (None for a story's
own frames) and derives the name for page-number markers from it; the cache
is keyed by index, so two pages with the same name no longer share a
composed header. The renderer and PDF export pass first_page_of_spread + i.
Test: running_header_on_parent_page_with_repeated_page_numbers (pages named
1, 2, 1, 2; the only heading on the third page). Before the fix the third
page's header was empty (0 glyphs); with it, "Epsilon" (7).
Found with an InDesign IDML (a dictionary with two sections and Match
Character Style running headers on its parent page), together with #301.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CgGsJkVe5GYG9qK6VFdECa
Text set in Portuguese ("Portuguese: Brazilian", "Portuguese", pt_BR) got
no hyphenation, so justified Portuguese columns could only stretch their
word spaces (up to 335 % of the normal space in a 56 mm column of 10 pt
Times).
Turn on hypher's "portuguese" feature, beside Spanish: hyph-pt.tex 1.4
(2024-07-13) from hyph-utf8, BSD-3-Clause, credited in NOTICE.
Lang::for_language already maps the Portuguese names and locale codes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Conflict: crates/compose/src/lib.rs: the custom boundary goes into next's shared wrap_exclusion(); free_slots keeps next's JumpObject handling outside it and takes the branch's combined BoundingBox/Contour arm
Conflict: crates/engine/src/cmd/object.rs: params doc has next's content flag and the branch's custom-boundary note
Conflict: crates/engine/src/tests.rs: both tests
Conflict: crates/idml/src/export.rs: the boundary goes into next's text_wrap_pref()
Conflict: crates/idml/src/import.rs: next's object-style-aware text_wrap() plus the branch's boundary parsing for the item's own wrap; pdf_page_in and wrap_path_of both kept
Conflict: crates/idml/src/tests.rs: both sides' tests
The line's em box top is measured at the unshifted baseline; the shift
moves only the drawn glyphs. CJK character alignment still counts. The
Ascent, Cap Height and x Height offsets keep counting a raised baseline.
Conflict: Cargo.lock: next's (the PR's dependency is already on next)
Conflict: ROADMAP.md: next's (the PR's earlier head is already merged)
Conflict: docs/gaps.md: next's (the PR's earlier head is already merged)
Conflict: docs/typography-parity.md: next's, test count 170 -> 172 as in d0120bf6
Conflict: crates/compose/src/optical.rs: next's plus d0120bf6's kernable() dash and fullwidth exclusion and its unit test
Conflict: crates/compose/src/tests.rs: next's plus d0120bf6's optical_kerning_leaves_dash_pairs_alone
Two dashes end to end have no ink gap to measure, so -- and the double
em dash opened up by the loosen cap; Chinese sets —— as one continuous
rule. Fullwidth Latin (U+FF21–FF5A) was kernable through its Latin
script and moved CJK text off its em grid. Both are now excluded from
kernable(), with a unit test and a compose test that failed before.
Optical kerning was treated exactly like Metrics: the font's kern pairs
and nothing else, so text set with Optical in InDesign came out wider,
rewrapped and went overset in tightly fitted frames.
Now the font's pairs are turned off for Optical text and each pair is
spaced from its outlines (crates/compose/src/optical.rs): every glyph's
ink is sampled into 28 bands across the em, the pair's mean and smallest
ink gap over the bands both glyphs ink are compared with the font's
stem-to-stem gap (the pair nn, or HH then oo), and the pair moves 60% of
the way towards it, keeping 30% of that gap and moving at most -0.12 or
+0.06 em. Latin, Greek and Cyrillic letters, digits and Western
punctuation take part; spaces, marks, missing glyphs, right-to-left
runs, upright and tate-chu-yoko glyphs and pairs across a fallback font
keep their font spacing. Profiles and references are cached, bounded.
Tests: AV, To and r. close up and nn stays; HH closes up under Optical
but not Metrics (fails on main); every change on a pangram stays within
the bounds and spaces keep their width; profile, gap, adjustment and
character-class unit tests.
Docs: typography parity Kerning row and remaining effort, gap 4,
ROADMAP milestone 6 and progress log.
Fixes#181.
Conflict: crates/compose/src/tests.rs: next's baseline-fit and keep tests, then the branch's OS/2 offset doc and code-page constant
Conflict: crates/fonts/src/tests_vertical.rs: testing imports from both (with_base, with_code_pages)
A font's em box is its BASE em box; else, for a CJK font (its font
menu group isn't Western: meta or name languages, CJK code pages, else
kana, Hangul or ideograph coverage), the OS/2 typo ascender when the
typo ascender and descender span exactly one em; else 0.5 em + cap
height / 2; else 0.88 em. Latin fonts with a one-em typo box, such as
DejaVu Serif, centre on the cap height.
Conflicts:
- Cargo.lock: designcraft-compose depends on both unicode-script (#427)
and unicode-segmentation (next).
- ROADMAP.md: the optical kerning progress-log line goes above next's two
2026-10-11 lines. Status line keeps next's Last reviewed date and names
both changes.
- docs/gaps.md, docs/typography-parity.md: same status-line merge.
Conflicts:
- engine cmd/file.rs, cmd/interchange.rs: kept next's file.open and
file.openIdml docs (content detection, INDD conversion, package parts)
and added "GREP styles whose pattern can't compile" to their warnings.
open_idml keeps next's conversion and import-report warnings and appends
grep_style_warnings after the font warnings.
- engine cmd/style.rs: style.paragraph.create/edit docs combine next's
startAt and kinsoku notes with the grepStyles rule. create_para and
edit_para keep next's set_json_over, continue_numbering and
explicit_default_kinsoku, then call check_grep_styles. The branch's
grep_style_error, check_grep_styles and grep_style_warnings follow
explicit_default_kinsoku. Kept next's delete_tests and
swatch_options_tests; grep_style_tests goes at the end of the file.
- ui-egui dialogs.rs: kept next's style_section_edit tests and added the
branch's bad-GREP-pattern test.
confirm_paragraph_style_options, grep_error and the dialog status line
merged without conflicts; next had not changed the OK arm they replace.
style.paragraph.create, style.paragraph.edit and type.para return the
compile error (bad GREP: ...) for a GREP style pattern that can't compile
and change nothing. Paragraph Style Options shows the error under the
pattern as it is typed, and on OK keeps the dialog open on the GREP Style
section with the command's error.
Opening an IDML or native document keeps such a style and reports a
warning naming the style (or paragraph) and the pattern; composition
keeps skipping it. Adds a lookbehind GREP style test.
Optical kerning was treated exactly like Metrics: the font's kern pairs
and nothing else, so text set with Optical in InDesign came out wider,
rewrapped and went overset in tightly fitted frames.
Now the font's pairs are turned off for Optical text and each pair is
spaced from its outlines (crates/compose/src/optical.rs): every glyph's
ink is sampled into 28 bands across the em, the pair's mean and smallest
ink gap over the bands both glyphs ink are compared with the font's
stem-to-stem gap (the pair nn, or HH then oo), and the pair moves 60% of
the way towards it, keeping 30% of that gap and moving at most -0.12 or
+0.06 em. Latin, Greek and Cyrillic letters, digits and Western
punctuation take part; spaces, marks, missing glyphs, right-to-left
runs, upright and tate-chu-yoko glyphs and pairs across a fallback font
keep their font spacing. Profiles and references are cached, bounded.
Tests: AV, To and r. close up and nn stays; HH closes up under Optical
but not Metrics (fails on main); every change on a pangram stays within
the bounds and spaces keep their width; profile, gap, adjustment and
character-class unit tests.
Docs: typography parity Kerning row and remaining effort, gap 4,
ROADMAP milestone 6 and progress log.
Fixes#181.
A font's em box is its BASE em box; else it tops at the OS/2 typo
ascender when the typo ascender and descender span exactly one em; else
it is centred on half the cap height (OS/2 sCapHeight, else the H
glyph's top), 0.5 em + cap / 2 above the baseline; else 0.88 em. The Em
Box Height first baseline, vertical text and declared_em_box all use
this one em box; declared_em_box is None only for the 0.88 em fallback.
Conflicts:
- crates/fonts/src/fontdb.rs, vertical.rs: next's em cache
((top, bottom), declared) with FontFace::declared_em_box and the ICF
cache/icf_box kept; the branch's base_em_box accessor dropped (same
value). em_box's fallback chain takes the branch's let em form.
- crates/compose/src/lib.rs: next's cjk_icf_box and the branch's
alignment_em_box, which reads declared_em_box.
- crates/compose/src/tests.rs: both tests. next applies character
alignment only under the Japanese composers (5dc8383a), so the branch's
smaller_cjk_run_aligns_to_the_line_em_box_centre runs under japanese();
next's cjk_em_center_alignment_moves_small_characters measures centres
on the alignment em box (declared, else 0.88/-0.12 em).
Conflicts:
- crates/pdf/src/text.rs run_text: the branch's two generated_text
branches first, then #392's kashida branch (no text), then the cmap
fallback for other inserted glyphs.
- crates/compose/src/lib.rs: next's kashida PlacedGlyph (with dx) gets
generated_text: None.
- crates/compose/src/shape.rs hyphen_after: next's rendered_char/moji
resets and generated_text = None.
- crates/compose/src/ruby.rs: next's ruby module (#363); the branch's
edit to the old ruby set() has nothing to apply to.
- crates/pdf/src/tests.rs: tests from both.
Also, so the merge compiles: next's ruby (ruby.rs annotate) and kenten
(kenten.rs shape_mark) PlacedGlyph literals take generated_text: None;
they map back through the font like other inserted glyphs. Kanji page
numbers, composite-font note numbers and fallback-face label segments
go through shape_segment's replacement and list labels through
prepend_label, so they carry their generated text.
Generated glyphs (page numbers, section markers, list labels) carry the
text they were shaped from, so a figure the font substitutes under small
caps or other OpenType features extracts as the digit instead of U+FFFD.
Character alignment (em box top, centre, bottom) measured each glyph's em
box from its font's ascender/descender proportions, which puts a Latin
font's em box centre too low, so a smaller CJK run in a Latin line sat
below the line's em box centre. Alignment now uses the font's BASE em box,
else 0.88 em above and 0.12 em below the baseline.