LYH Studio episode 6 (8:28) gives the "1969" headline the "New Gradient
Swatch" (magenta → cyan) from the Swatches panel. InDesign runs a gradient
on text across its text frame's box. DesignCraft resolved a character
fill to one solid colour, so a gradient swatch drew no text at all.
- render: a character fill or stroke that names a gradient swatch is
painted with that gradient over the frame's box in text space (type on
a path: the path's box).
- pdf: text runs take a gradient paint: the shading is laid out in the
frame's text space and mapped into each run's origin, so skew and
horizontal scale don't bend it; outlined runs and type on a path too.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A gradient swatch on a stroke drew nothing: strokes were painted with a
single resolved colour. LYH Studio episode 6 gives the loop on the right
page (9:13) and, at first by accident, the circle (1:19, "right now it
is on the stroke") a gradient stroke.
- doc: Stroke::gradient_angle / gradient_vector (as Fill's) and
Stroke::as_fill(), the stroke's paint as a fill.
- render and PDF: a gradient stroke is painted like a gradient fill,
across the path's box (dash gaps and arrowheads keep their paints).
- engine: object.gradient {target: stroke} edits the stroke's
gradient (kind, stops, angle, the Gradient Swatch tool's from/to);
scaling moves the stroke's vector with the shape.
- IDML: GradientStrokeStart/Length/Angle round-trip.
- Gradient panel: acts on the stroke when the toolbox's Stroke box is
in front, as InDesign's follows the Fill/Stroke proxy.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Fields, buttons and video or sound are collected with the page content's
inclusion rule: the item's layer is visible and printable, and neither the
item nor any group around it is hidden or non-printing.
InDesign draws a radial gradient in the object's bounding box: on a
non-square frame it's an ellipse reaching the frame's sides, and a
Gradient Swatch tool drag keeps those proportions. LYH Studio episode 6
(3:16) gives the left page's background a radial gradient: magenta in
the middle, cyan out to every edge. DesignCraft drew a circle reaching
only the longer side, so a portrait page stayed purple at its sides.
- doc: `Fill::radial_ellipse(bounds)`: centre and radii (default: the
box's centre and half its width and height; with a tool vector, its
start, and its length measured with the box made square).
- render: the radial paint is that circle, stretched by the paint
transform; PDF: the shading's transform does the same.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Cycle checks walk based_on with a seen-set over all parents, so a cycle
longer than the drawing depth cap is still found. Snapping lists parent
items through shown_parent_pages instead of recursing, and offers base
items on a parent spread. A base parent's text on a parent spread is
composed for that spread, so page-number markers show its prefix.
Cmd+Shift-click hits what is drawn: it honours Show Parent Items, hidden
and locked layers, the facing-page filter and layer order. IDML import
warns when a parent spread's pages name different bases or their own
spread.
Pages draw the base parent's items, then the parent's, then their own,
on the canvas, in PDF and image export, and on the parent spread itself.
Chains are followed base first with a depth bound. Overrides, parent hit
testing and the flattener's transparency check include base items.
layout.parents.options {parent, basedOn} sets or clears the base and
refuses cycles; document.inspect reports basedOn per parent. Opening a
.designcraft or IDML file clears a cyclic or dangling base with a warning.
IDML already carried the base as the parent pages' AppliedMaster; a test
covers the round trip.
- designcraft-color: ICC profiles (built-in RGB working spaces, user .icc files) go through
craft-cms. The default CMYK working space is craft-cms's Coated FOGRA39; CMYK swatches show
relative colorimetric with the settings' black point compensation, as placed images do.
Generic CMYK and Device CMYK stay selectable; names of the profiles FOGRA39 stands in for
resolve to it; ColorSettings::with_known_profiles replaces unavailable saved names with the
defaults (used when the UI restores saved settings). Lab, gamut error and proofing of ICC
CMYK spaces use craft-cms transforms. The Generic CMYK output profile is encoded by craft-cms.
- designcraft-images: Profile, Transform, TransformOptions, Intent and Builtin are craft-cms's;
its black point compensation replaces the module's sRGB table. Untrusted profiles are still
ignored with a log line.
- designcraft-pdf: the PDF/X output intent is the working CMYK space's profile: the FOGRA39
built-in with identifier FOGRA39, user profiles as loaded, Generic CMYK otherwise.
- moxcms is no longer a direct dependency.
Conflict: crates/render/src/text.rs: the branch's paint_glyphs with next's fill and stroke overprint (overprint_fill / overprint_stroke, an overprinting() Multiply layer around each paint); next's cell_stroke(kind, weight, length); path text strokes don't overprint, as before
Conflict: crates/pdf/src/text.rs: the branch's text_run / paint_outlines / glyph_outlines split with next's skew tan, vertical mark offsets and split_clusters glyph order (#421)
Conflict: crates/engine/src/cmd/paths.rs: next's shatai transform + the branch's stroke_behind_fill key
Conflict: crates/idml/src/tests.rs: tests both
Conflict: docs/typography-parity.md, ROADMAP.md: headers combined, logs both
Placed rasters are converted to display sRGB once, at decode time, in one
module (designcraft-images colour):
- embedded ICC profiles in JPEG, PNG, TIFF, WebP and PSD (RGB and grey) are
applied; sRGB-like profiles leave the pixels exactly as decoded
- CMYK JPEGs (incl. YCCK, Adobe-inverted values), CMYK TIFFs and CMYK PSD
composites go through their embedded CMYK profile, else the working CMYK
space, with the colour settings' intent and black-point compensation
- oversized (> 16 MiB), unreadable or mismatched profiles are ignored with a
log line
The render image cache is rebuilt when the colour settings change. PDF/A
exports of CMYK TIFFs get the same managed RGB as the screen.
A character attribute strokeBehindFill paints a text stroke under the fill
at double width, so its full weight lies outside the letters and they keep
their shape. Screen and PNG rendering, PDF export (stroke as outlines
first, then the filled text, so the text is not duplicated; clipped to the
outside of the glyphs when there is no fill), Create Outlines (outside-
aligned stroke), the Properties panel and Character Style Options.
IDML StrokeAlignment on CharacterStyleRange, CharacterStyle and
ParagraphStyle imports and exports as the attribute (OutsideAlignment =
on, CenterAlignment = off).
The DestIn mask now covers the whole render target (and the PDF's
exported area), with the gradient kept in item space through the paint
transform, since past the gradient's ends the end opacity carries on: a
child's long drop shadow or thick stroke is faded, not cut off at a box.
feather_bounds counts only shown children and active states, walking
nested groups without recursion.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A gradient feather on a group (Gradient Feather tool over grouped text, as
in LYH Studio's poster tutorial) did nothing on screen: groups never went
through the effects path. In PDF the group's mask was the group's own path
bounds, and a group has no path, so the mask covered a box of zero size and
hid the group.
- Item::feather_bounds: the box the feather spans, the objects' bounds for
a group.
- Render: a feathered group draws its objects in one layer, then fades it
with the same mask as other objects (fx::gradient_feather_mask).
- PDF: the group's mask spans its objects.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Conflict: ROADMAP.md: both progress lines
Conflict: docs/gaps.md: next's header, clipping paths added to its change list
Conflict: docs/target-app-parity.md: next's header, clipping paths added to its change list
Conflict: crates/doc/src/item.rs: both Graphic methods (next's shown_bounds, the branch's clip_region)
Conflict: crates/idml/src/names.rs: both imports (ClippingKind, ContourType)
Conflict: crates/idml/src/export.rs: both pushes, TextWrapPreference first, then the clipping settings
Conflict: crates/idml/src/import.rs: both lets (next's wrap, the branch's clip)
Conflict: crates/engine/src/cmd/file.rs: next's size and transform, plus clip: None
Conflict: crates/engine/src/cmd/datamerge/fill.rs: next's Graphic with wrap, plus clip: None
Conflict: crates/engine/src/cmd/mathexpr.rs: next's Graphic with wrap, plus clip: None
Conflict: crates/pdf/src/tests.rs: both tests
A placed graphic keeps its clipping path (Detect Edges, Alpha Channel,
Photoshop Path or User-Modified Path) in graphic space, with the
options IDML carries. IDML import reads ClippingPathSettings and its
saved ClippingPathGeometry (anchors capped, non-finite subpaths
dropped); export writes them back. The canvas and PDF export clip the
image to the path inside the frame clip; an inverted path shows the
image outside it. Without saved geometry nothing changes.
object.clippingPathOptions reports a graphic's clipping path, inverts
it or removes it (Object > Clipping Path > None).
Fixes#450.
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
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
8806a223 (next, for #392) drew a slanted glyph with run while a
right-to-left line span was open, so its tagged span would not nest in
the line span. pdf-rtl-text (8f30ba1c) removes the line span and its
line_span flag, so the glyph always goes through tagged_run. The
regression test keeps its export of tagged and untagged PDFs with shatai
Hebrew and checks what the branch's tests check: reading-order text,
no marked content opened inside an ActualText span, and no ActualText
wider than a word.
poppler positions an ActualText with the transform in force where its
span ends; a line span closes outside the runs' transforms, so pdftotext
placed the line text off the page and dropped it. An ActualText per run
fares no better: poppler reverses its characters in a right-to-left
line. Without them, poppler orders the glyphs of right-to-left lines
itself and extracts whole lines in reading order, and the content stream
keeps listing them in reading order. With no line span left, glyphs in
it no longer need to avoid krilla's cluster spans, so that workaround
goes too. The tests that read the line span's ActualText now check that
ActualText never spans more than a cluster.
krilla wraps a mark cluster that maps to several glyphs, and a glyph drawn
with other text than its font already maps it to, in an ActualText span.
Inside a line's ActualText span that nested the spans, and poppler drops
the text before a nested span. Inside a line span each glyph now gets the
text its font maps it to (the exporter keeps a copy of krilla's mappings),
a cluster's text goes to its first glyph, and a run splits into separate
draw calls where two glyphs in a row would share an empty range. A run
drawn through tagged_run inside a line span takes the span as its tag.
Test: mark_clusters_in_right_to_left_lines_open_no_span_of_their_own
(5 nested spans before).
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.
next's shatai path draws a slanted glyph on its own through tagged_run,
which opens a Span; inside #392's right-to-left line span that nested
marked content and krilla panicked ("can't start marked content twice")
in tagged exports. While a line span is open, the glyph is drawn with
run like the line's other runs. Test:
shatai_glyphs_in_right_to_left_lines_stay_in_the_line_span (panicked
before).
Lines with right-to-left text are wrapped in a marked span whose
ActualText is the line's text in reading order, so viewers that order
text by position (Preview, PDFKit) no longer return it reversed.
A base letter and its marks (Hebrew points) map glyph by glyph when
each glyph stands for one of the cluster's characters, and are drawn in
text order, so ToUnicode-only extraction reads each letter once with
its points after it. Marks stay in their run, placed by glyph offsets
(compensated for skew), instead of starting runs of their own.
Kashidas inserted by justification carry no text.
Fixes#392.
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.
- table.setCell starts each chosen edge from the stroke rendering draws there (the winning
copy of a shared edge, the row/column pattern, or the table border) and writes the result
to both copies; table.getCellStroke reports drawn edges, counting each shared edge once.
compose::table::cell_side_strokes shares the resolution with the renderer.
- Commands accept dash patterns of 1-64 lengths, each 0 or 0.1-10000 pt. Screen and PDF
draw a dash or dot pattern that would repeat more than 20000 times along a path as solid.
- Stripes, wavy, hash and gap colour extend by half the weight at table corners.
- Cell Options: an edge change reseeds every stroke field and tolerates a non-object seed.
- Edge proxy: a triple-click decides from the choice before the first click.
Conflicts: compose lib.rs imports (both; Composer unused), idml tests.rs (both test modules), ui-egui
panels/mod.rs (next's ruler origin with the branch's graphic wrap for Direct Selection).
A right-to-left bound spread lists its right page first, parent spreads
included. The parent page shown behind a document page was picked by list
index (first = left), so every page of an imported right-to-left document
showed the other side's parent items. Pick it by position instead: the
leftmost parent page for a left page, the rightmost for a right page.
New parent spreads in a right-to-left document list their pages right
first, and changing the binding reorders existing parent spreads, so IDML
export writes them in binding order. Spread bleed on the canvas and in
spread PDF export takes the outer edges by position as well.
A placed graphic carries its own text wrap (Graphic.wrap). IDML import
reads TextWrapPreference on the Image/PDF element and export writes it
back there. Layout treats it as an obstacle around the part of the
graphic its frame shows, plus the offsets, and also collects wraps of
items inside groups. object.textWrap acts on the graphic when the
selection is the frame's content, and the Text Wrap panel shows its wrap.
Conflicts: ui-egui dialogs.rs (next's numberingSection arm and the branch's cellOptions arm; the branch's
copies of the dialog test helpers already on next dropped, its cell options test kept); idml tests.rs and
the i18n tables appended at the end on both sides (both kept; the branch's Gap Tint: and Tint: rows that
next already had dropped).