Conversation
The layout engine tested fit against the bare bottom margin, while footers are stamped on afterwards into the ~14 pt strip above it, so a page's last system could print through its footer. FooterBand now holds the footer's size and baseline for both sides. When the document prints any footer, VerticalLayoutEngine raises its floor by the footer's ascent + descent plus a 6 pt gap on every page. A document with no (or only empty) footers reserves nothing and lays out exactly as before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every single-valued string field in TuneMetadata (C:, A:, B:, D:, F:, G:, N:, S:, R:, Z:) was overwritten by each later occurrence, so a tune with three N: lines kept only the last. ABC v2.2 §3 treats a repeated string field as additional information, so each is now an ordered array filled in source order, as H: and O: already were. The title block prints each C: right-aligned on its own row (§6.1.3), with O: appended to the last composer and repeated R: joined with "; " on the first row. TuneMetadata's initializer now defaults every field to [], so callers need pass only the fields they have. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Fix #192 — layout keeps music out of the footer band
A chord symbol spelled "Bb" or "F#m" printed its raw text, so the
accidental came out as a letter. Unicode ♭/♯ came out too, but small,
drawn from the text face.
The parser now records which characters of a chord it read as
accidentals, in ChordSymbol.segments: in the root, the bass note, the
type ("C7b9") and an alternate chord ("G(Bb)"). Only the parser can tell
the b of "Bb" from the b of a lowercase root, so the renderer doesn't
guess. The annotation band draws those segments as Bravura's
chord-symbol accidentals (csymAccidental*, U+ED60–ED64) at the text's
size and on its baseline. Lines with no accidental, and text that isn't
a chord, are drawn exactly as before.
"C7(b9)" was read as C7 with an alternate chord "b9" (lowercase B). The
parser now tries the parentheses as part of the type first and falls
back to the alternate-chord reading only when that fails, so the type
is "7(b9)". "G(Em)" is unaffected.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Fix #188 — repeated string fields keep every value
Fix #184 — chord symbols draw ♭ and ♯ signs
Columns were sized for the music and for w: syllables, but never for the text above and below the staff, so a chord symbol wider than its note's column printed over the next one. AnnotationSpacing runs on every bar MeasureSizer produces, from both the single-voice and the shared-staff paths. It measures each note's widest line above the staff (chord symbol with its accidental signs, chord-line text, ^ annotations) and below it (_ annotations), and opens the columns until each clears the next note's text by a staff space. The room is shared evenly over the column boundaries in between. Text on a bar's last note must end within the bar, since the next bar isn't known. > text clears the next note, and < text clears the previous note or the bar line. A bar with no text comes back unchanged. SizedMeasure records the music-only offsets and width alongside the widened ones. When a system is stretched, the Justifier now stretches the music and holds each gap (and each bar) to at least its widened width (floorScale), so the room a chord needed is not scaled up again along with everything else. Systems with no widened bar take the old code path unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Fix #185 — note spacing makes room for chord symbols and annotations
The semantic pass had no case for InformationField.words, so W: lines were parsed and then dropped without a diagnostic, and Tune had nowhere to hold them. - Model: Tune.words, one TextString per W: line, header lines then body lines, in source order. An empty W: is kept as an empty line. - Parser: a W: line keeps the writer's spacing. Only trailing space and the one space conventionally written after the colon are dropped. Every other text field is still trimmed. - Layout: TuneBlock carries the lines (empty when %%writefields W false). VerticalLayoutEngine places them a line at a time below the last system, left-aligned at the margin, before the tune gap, breaking to a new page when a line doesn't fit (a blank line at the top of a new page is skipped). Their height counts in the tune's height for the whole-tune fit test. The rows ride on the page's title rows. - SVG: a <text> whose content has leading, trailing or repeated spaces gets xml:space="preserve", so --font-face output keeps the alignment. Text without such runs is written exactly as before. Words are set in Libertinus Serif 12 until %%wordsfont is honoured (#186). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Fix #187 — W: words reach the model and print below the tune
OpenTypeFont could only outline CFF-flavoured faces and rejected
collections outright, which ruled out most installed fonts (Times.ttc,
Courier New.ttf, DejaVu, Liberation).
- TrueTypeGlyphs reads glyf via loca, both short and long formats.
- Simple glyphs: run-length flags, short and long deltas, implied
on-curve midpoints, contours that start off-curve or have no on-curve
point. Contours start where fontTools and FreeType start them.
- Composite glyphs: byte and word offsets, the three transform forms,
and SCALED_COMPONENT_OFFSET. Nesting is limited to 8 levels, so a
cycle throws compositeTooDeep instead of recursing forever.
Point-matched components are skipped.
- Hinting instructions are ignored.
- GlyphPath gains .quadCurve, emitted as SVG Q, plus an affine transform
for composite components.
- Collections: parse(_:faceIndex:), parse(_:postScriptName:), and
postScriptNames(in:) from name ID 6. Table offsets are file-relative in
both single fonts and collections.
- OS/2.fsType is exposed as EmbeddingPermissions. allowsOutlineEmbedding
refuses restricted, bitmap-only and no-subsetting faces, ready for #190.
The bundled CFF faces take the same path as before.
Fixtures are CeolKit's own fonts, generated by a committed fontTools
script, with fontTools' reading of the same bytes as the reference.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Fix #189 — OpenTypeFont reads TrueType outlines and font collections
The renderer could draw text only in the faces it bundles, and OutlineFontSet.resolve answered nil for any other name. - FontRequest (public): family, weight and style, or parsed from a PostScript name (Times-BoldItalic, Helvetica-Oblique). The base 14 and the generic families (serif, sans-serif, monospace) list stand-ins: Times/Times New Roman/Liberation Serif/Nimbus/DejaVu and so on. - FontLibrary (public, immutable, Sendable): faces the host supplies as font Data, CFF or TrueType, single faces or collections, parsed once. Set on SVGRenderConfig.fontLibrary. Per-config rather than process-wide, so a config always gives the same output. - SVGRenderConfig.systemFonts (default false): installed fonts on Apple platforms. CoreText only locates the file and face, which our own parser (#189) then reads, so every face is outlined and fsType-checked by the same code. With a mandatory attribute set, CoreText's everything-is-Helvetica fallback cannot answer a missing family. - FontProvider resolves a request through registered, then system, then bundled faces. For each family (asked-for first, then stand-ins), an exact face from any source beats a near one. Faces whose fsType forbids embedding are skipped when outlining. Results are cached per provider. The emitter's own Bravura and Libertinus Serif requests go straight to the bundle, so existing output is unchanged. - FontResolution (public) records the face used, its origin and any refusals. diagnostics(at:) produces .fontSubstituted (info for a stand-in, warning otherwise) and .fontNotEmbeddable, for #186 to report against the directive that named the font. - FaceKey becomes an open-ended key. Bundled faces keep their ids; other faces get "f-<PostScript name>". - TextOutliner.outline(_:font:fontSize:fill:library:systemFonts:) draws a named face, and OutlinedText.resolution says which face was used. - OpenTypeFont reads family (name 16/1), usWeightClass and italic, with head.macStyle as the fallback for faces without OS/2 (macOS Courier.ttc). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Fix #190 — font lookup: registered, system and bundled faces
Every font directive was reported as unsupported and dropped, so all text was Libertinus Serif at sizes the renderer fixed. Parser - TextFontRole (titlefont … setfont-4) and FontSpec(name?, size?) in the model, carried as CeolKitDirective.font. File header → file-global; tune header and body → tune-global. - `<font name> [<size>]`, a quoted name for families with spaces, and `*` to keep the face in force (abcm2ps). Anything else is reported as .invalidFontDirective and dropped. Renderer - Each tune's text styles are resolved from CeolKit's defaults, then SVGRenderConfig.textFonts (new: a host house style), then file directives, then tune directives, each half of a spec overriding only what it states. Faces resolve through the #190 FontProvider. - With no directive a role has no face and is drawn by the existing code, so documents without font directives are byte-identical (the snapshots confirm it). - Staff text (gchord, annotation, vocal, an inline tempo) scales with %%ceolkit:scale; page text (title, subtitle, composer, info, header tempo, words) is in absolute points. - Layout follows the text: title rows grow past their default size; chord/annotation and lyric bands take their line height and ascent/descent from the style; words lines scale; chord symbols, annotations and syllables are measured in their own face for spacing. - Libertinus Serif Bold and Bold Italic (v7.040, the same release as the bundled Regular/Italic, OFL) are bundled, so bold requests render bold without system fonts. fontFace mode embeds them only when used. - Substitutions are reported once per directive, at the directive: info while system lookup is off, warning when it was searched and nothing answered. The §14.4 Canzonetta sets 32 pt titles and 14 pt lyrics and relies on abcm2ps's %%scale 0.7, which CeolKit doesn't implement, so it now runs to two pages. Its structural checks are now per page, and its snapshot was re-recorded after a visual check. Not done: $1–$4 in-string switches (setfont-n is parsed only); partsfont and textfont have no printed text to style yet. CONFORMANCE.md documents all of this. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Fix #186 — honour the §11.4.2 font directives
- CeolKitFonts.availableFaces(config:): registered, system (when enabled) and bundled faces, in search order; system faces described through CoreText, with the base-14 stand-in families probed by name since macOS hides some from enumeration. - RenderedDocument.fonts: per tune, what each kind of text asked for and got. - %%ceolkit:fontlist [resolved|available]: reports as info diagnostics at the directive, for the scope it is written in. - ckprobe: --fonts, --system-fonts, --font-file, and a per-tune fonts: section. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Fontconfig: libfontconfig.so.1 loaded with dlopen at first use (no build-time dependency; absent library = no system fonts, as before). FcFontList only, never FcFontMatch, so an uninstalled family finds nothing. - SystemFonts: family and PostScript-name lookups and the listing go through it on Linux; faces in formats OpenTypeFont cannot read (Type 1, bitmap) are passed over. - OpenTypeFont.summary: names, weight, slant, format and fsType without parsing outlines, sharing readStyle with parse. - CI Linux job installs fontconfig, Liberation, EB Garamond, URW base 35, WenQuanYi and the CeolKitTest.ttc fixture, and sets CEOLKIT_FONT_FIXTURES=1 for the gated FontconfigTests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Fix #191 — probe available and resolved fonts
Fix #206 — system font lookup on Linux through fontconfig
A |: or [|: that closes a system now draws as a plain bar line ending at the staff end, and a :: as its end-repeat half; the next system opens with the start-repeat (a restated :: keeps only its start-repeat half). Drawn whole, the sign's thick line and dots landed past the end of the staff, and a restated :: drew end-repeat dots before the first note. This matches abcm2ps and engraving convention. The kinds are rewritten where measures are resolved, so the emitted SVG and the resolved layout agree. Mid-line repeats are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Fix #197 — split a repeat sign that falls on a system break
%%scale S and %%pagescale F (= %%scale 0.75 × F) are parsed in the file header, tune header and tune body, with abcm2ps's generation scope: a file value governs the tunes after it, a tune's own governs that tune, and the last one written wins. A body directive scales the whole tune and says so with a scaleAppliesToWholeTune note. With no directive a document renders at abcm2ps's default %%scale 0.75 (4.5 pt staff space). The scale applies to the music and to every piece of text the tune sets — title block and W: words included — but not to the page, its margins or the footer. Default font sizes are now abcm2ps's, in its nominal units, scaled with everything else. SVGRenderConfig.scale is the host default (abcm2ps's -s); scaledStaffSize is the drawn staff space. %%ceolkit:scale F now means %%pagescale F and emits a deprecatedDirective warning. ckprobe --scale/--sweep set %%scale. Canzonetta (§14.4, %%scale 0.7) now prints on one page with a 22.4 pt title, as abcm2ps does; the tunebook fixture paginates to 15 pages, the same as abcm2ps 8.14. Snapshots re-recorded after a visual comparison. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Fix #203 — %%scale and %%pagescale, as abcm2ps implements them
The last line of a tune was drawn at natural spacing, which packs eighths almost head to head next to the justified lines above it. abcm2ps 8.14.0 does two things instead, and CeolKit now does both: - %%stretchlast F (0…1, default 0.25): the last system is stretched to the line when its unstretched staff reaches 1 − F of it. The spec's logical form is accepted as 1/0. - A last system that is not stretched is spaced like the line before it, never below natural and never past the line. %%stretchstaff false stretches no system at all; ckprobe --natural now forces it, so it reports natural widths for every system. %%ceolkit:justifylast is deprecated: true/false parse as %%stretchlast 1/0 with a deprecatedDirective warning. CeolKitDirective.justifyLast(Bool) is replaced by .stretchLast(Double) and .stretchStaff(Bool), and SVGRenderConfig.justifyLastSystem by stretchLast/stretchStaff — release notes need an Upgrading entry, since the last line of most tunes changes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Fix #198 — %%stretchlast and %%stretchstaff, as abcm2ps implements them
abcm2ps rewrites `|:` to `[|:` before drawing it (draw.c, bar_cnv), so a start repeat is thick, thin, dots: the mirror image of `:|`. CeolKit drew it thin, thick, dots. `|:` now draws exactly as `[|:`. abcm2ps draws `::` as its `%%dblrepbar`, which defaults to `:][:` (format.c): dots, two thick bars, dots. CeolKit drew dots, thin, thick, dots. The two thick bars stand `barlineSeparation` apart edge to edge (SMuFL has no thick–thick separation), and the closing padding is sized from that offset. The repeat dots were set `repeatBarlineDotSeparation` from the *centre* of the bar they abut. Against a thin bar that hardly shows; against the thick bars of `::` the dots ran into them. They are now measured from the bar's edge, and `leftMargin` measures the same way — for `::` from the thick bar on its anchor, rather than from a thin–thick span it no longer has. The Canzonetta snapshot (§14.4 has both signs) is re-recorded. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Fix #208 — draw |: and :: as abcm2ps does
`$1`…`$4` now set the rest of a string, to the next switch, in the face and size `%%setfont-1`…`4` name; `$0` returns to the string's own font and `$$` prints `$`. Any other `$` (`$5` included) prints as written. - `FontSwitch` (CeolKitModel) splits a string into runs; the text itself keeps its switches, and `plainText` takes them out. - `TextStyle` carries the `%%setfont-n` faces, so `width(of:)` and `builder.text(_:style:)` follow switches wherever text is measured or drawn: titles, composer, info fields, `Q:` text, annotations, chord-line text, chord symbols, `w:` and `W:`. A string with no switch draws as before, byte for byte. - A run set in a switched face is measured in it, so a title stays centred and the note spacing for annotations (#185) and lyrics widens to suit. - Chord symbols are read with their switches removed (`"G$1m"` is G minor, not chord-line text), and the switches go back into the text segments. - As in abcm2ps, a switch in a `w:` line holds from syllable to syllable; the aligner writes each syllable with the switch in force at its start. - A switch in `Q:` text is closed before the metronome mark. - An unset `%%setfont-n` keeps the string's face at abcm2ps's documented default of 12, with an `unsetFontSwitch` warning. - Footers are unchanged: abcm2ps 8.14 does not switch font there either. `$T` gives the title without its switches. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Fix #204 — $1…$4 in-string font switches (%%setfont-n)
A bar written in its own right — after a bar of its own, as in `:| ::` or `| |`, or at the head of the tune — is now drawn whole wherever it falls, as abcm2ps draws it, rather than dropped or drawn on top of the bar before it. - The sizer records whether a measure owns its opening bar (it is not the previous measure's closing bar restated) and the lead that bar needs: ~2.4 staff spaces clear of the bar it follows mid-line, and at the head of a system its leftmost ink where a `|:`'s thick line starts. - VerticalLayoutEngine draws an owned bar whole at that offset; a bar split across a system break (#197) is handled as before. - Bars VoiceAligner pads with are never owned. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Fix #212 — draw back-to-back bars as written
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Major release: font directives and font lookup (registered, system, bundled faces),
%%scale/%%pagescaleand%%stretchlast/%%stretchstaffas abcm2ps implements them, repeat-sign drawing, repeated header fields,W:words, and chord-symbol accidentals.Source-breaking:
TuneMetadatastring fields become arrays;CeolKitDirective.justifyLastandSVGRenderConfig.justifyLastSystemare replaced bystretchLast/stretchStaff. Engraved output changes for every document (default%%scale 0.75).Fix #184
Fix #185
Fix #186
Fix #187
Fix #188
Fix #189
Fix #190
Fix #191
Fix #192
Fix #197
Fix #198
Fix #203
Fix #204
Fix #206
Fix #208
Fix #212
🤖 Generated with Claude Code