Repository navigation
Releases: sbeitzel/CeolKit
Release list
2.1
Clefs, octaves and stems, as abcm2ps draws them.
A minor release. Notes are placed for the clef in force, clef changes part way through a
voice are drawn, octave= takes effect, and stems and beams follow abcm2ps's rules. The
model gains Event.clefChange. See Upgrading.
Clefs and octaves
- Notes follow the clef (#222). Every notehead, stem, ledger line, accidental, tie and
slur used to be placed at its treble-clef position whatever clef the staff carried. Staff
position is now counted from the clef's bottom line (ClefSpec.staffPosition(of:)). An
octave clef (treble-8) moves nothing, as in abcm2ps. octave=onK:andV:(#224) shiftsNote.pitchin the semantic pass, scoped as
abcm2ps scopes it: a headerK:covers every voice, a bodyK:or aV:its own voice,
and each lasts until anotheroctave=replaces it.Transposition.statesOctavelets
octave=0reset it.- A
K:with a clef and no key (#223).K:bass,K:clef=bassand[K:clef=bass]are
read as options only (§3.1.14, §4.6) instead of being rejected.KeySignaturerecords
what was stated (statesKey,statesClef); what was left unsaid carries on from what is
in force, soK:GafterK:bassstays in the bass clef. An unreadableK:still warns,
and now changes nothing. - Clef changes are drawn (#223). A clef change part way through a voice is a new
Event.clefChangewhere the field was written. A change against a bar line is a small
clef before that bar (a courtesy clef at the end of a line, with the new clef full size
at the head of the next); a change mid-bar is a small clef between the notes; a clef
change alone draws no key signature.Measure.keyis set only by aK:that names a key.
Stems and beams
- Only notes shorter than a quarter are beamed (#217), whatever the meter (§4.7). In
6/8, 2/2 and 3/2, quarter notes were beamed to their neighbours. - One stem direction per beam group (#218). Each group's direction is decided before
any of it is drawn: down where the highest note is farther above the middle line than the
lowest is below, up in the opposite case, and a group as far above as below follows the
part's previous stem (up if none). Every stem in the group is placed for that direction. - A middle-line note follows the stem before it (#221) in an
.autovoice, across bar
lines and rests, and stems up where there is none.stem=, pipe format and a shared
staff's opposition still decide outright.
What's Changed
- Fix #217 — don't beam notes too long to carry a flag by @sbeitzel in #219
- Fix #218 — one stem direction per beam group, every stem placed for it by @sbeitzel in #220
- Fix #222, #224 — place notes for the clef in force; apply octave= by @sbeitzel in #225
- Fix #221 — stem a middle-line note the way the stem before it went by @sbeitzel in #226
- Fix #223 — accept a K: that names a clef but no key, and draw clef changes by @sbeitzel in #227
- Release 2.1.0 by @sbeitzel in #228
Full Changelog: v2.0.0...v2.1.0
Upgrading
Event has a new case, clefChange(ClefSpec). An exhaustive switch over Event needs a
branch for it; everything else is source-compatible with 2.0.0. The new KeySignature and
Transposition initializer parameters have defaults.
Engraved output changes for tunes in a clef other than treble, tunes using octave=, beamed
quarter notes in compound meter, and some stem directions on beam groups and middle-line
notes. Each now matches abcm2ps 8.14 -g.
2.0
Fonts, scale and the last line, as abcm2ps does them.
A major release. Text can now be set in any font the host or the system supplies, the
§11.4.2 font directives take effect, and %%scale, %%stretchlast and %%stretchstaff
behave as they do in abcm2ps. Repeated header fields keep every value, and W: words print.
Because the default scale is now abcm2ps's 0.75, every document engraves differently, and
some public API changed shape. See Upgrading.
Fonts
- Font directives (#186).
%%titlefont,%%subtitlefont,%%composerfont,
%%tempofont,%%gchordfont,%%annotationfont,%%infofont,%%vocalfont,
%%wordsfontand%%setfont-1…4take effect. They are parsed into
CeolKitDirective.font(TextFontRole, FontSpec), and layout grows to fit the text they
set. Libertinus Serif Bold and Bold Italic are now bundled. - In-string font switches (#204).
$1…$4switch to the%%setfont-nface,$0
switches back, and$$prints$. Measurement and drawing both follow the switches. - Font lookup (#190, #206). A text font is looked up in faces the host registers
(SVGRenderConfig.fontLibrary, aFontLibrary), then in installed fonts
(SVGRenderConfig.systemFonts: CoreText on Apple platforms, fontconfig on Linux), then
in the bundled faces. The base-14 and generic families fall back to stand-ins.
Substitutions and faces that forbid embedding are reported as diagnostics at the
directive that named the font. - TrueType and collections (#189).
OpenTypeFontreadsglyfoutlines (simple and
composite) and.ttccollections, and checksOS/2.fsTypebefore embedding a face. - Probing fonts (#191).
CeolKitFonts.availableFaces(config:)lists the faces text could
be set in, andRenderedDocument.fontsreports what each kind of text asked for and got.
%%ceolkit:fontlist [resolved|available]reports both as diagnostics.ckprobegains
--fonts,--system-fontsand--font-file.
Layout, as abcm2ps does it
%%scaleand%%pagescale(#203) are parsed in the file header, tune header and tune
body, with abcm2ps's scoping rules. With no directive a document renders at abcm2ps's
default%%scale 0.75. Music and tune text scale; the page, its margins and the footer
do not.%%ceolkit:scaleis deprecated.%%stretchlastand%%stretchstaff(#198). The last system is stretched to the line
once it fills 1 − F of it (default F is 0.25). A last system that is not stretched is
spaced like the line above it, so its eighths no longer sit almost head to head.
%%ceolkit:justifylastis deprecated.- Repeat signs (#197, #208, #212). A repeat sign that falls on a system break is split
across the break.|:draws as thick–thin–dots and::as:][:. A bar written
straight after another (:| ::,| |), or at the head of a tune, is drawn as written. - Footers (#192). A page's last system no longer prints over its
%%footer. - Text spacing (#185). Note spacing opens to make room for chord symbols and
annotations, so they no longer print over each other.
Text
- Chord symbols draw ♭ and ♯ (#184) using Bravura's chord-symbol accidentals, from the
newChordSymbol.segments."C7(b9)"is now read as type7(b9). W:words (#187) are kept inTune.wordsand printed below the tune, with the
writer's spacing intact.- Repeated header fields (#188). A tune with several
C:,A:,B:,D:,F:,G:,
N:,S:,R:orZ:lines keeps every value, in source order. EachC:prints on a
row of its own.
What's Changed
- Fix #192 — layout keeps music out of the footer band by @sbeitzel in #193
- Fix #188 — repeated string fields keep every value by @sbeitzel in #194
- Fix #184 — chord symbols draw ♭ and ♯ signs by @sbeitzel in #195
- Fix #185 — note spacing makes room for chord symbols and annotations by @sbeitzel in #196
- Fix #187 — W: words reach the model and print below the tune by @sbeitzel in #199
- Fix #189 — OpenTypeFont reads TrueType outlines and font collections by @sbeitzel in #200
- Fix #190 — font lookup: registered, system and bundled faces by @sbeitzel in #201
- Fix #186 — honour the §11.4.2 font directives by @sbeitzel in #202
- Fix #191 — probe available and resolved fonts by @sbeitzel in #205
- Fix #206 — system font lookup on Linux through fontconfig by @sbeitzel in #207
- Fix #197 — split a repeat sign that falls on a system break by @sbeitzel in #209
- Fix #203 — %%scale and %%pagescale, as abcm2ps implements them by @sbeitzel in #210
- Fix #198 — %%stretchlast and %%stretchstaff, as abcm2ps implements them by @sbeitzel in #211
- Fix #208 — draw |: and :: as abcm2ps does by @sbeitzel in #213
- Fix #204 — $1…$4 in-string font switches (%%setfont-n) by @sbeitzel in #214
- Fix #212 — draw back-to-back bars as written by @sbeitzel in #215
- Release 2.0.0 by @sbeitzel in #216
Full Changelog: v1.9.1...v2.0.0
Upgrading
Source-breaking changes
- Ten
TuneMetadataproperties change from optional to array:composer,area,book,
discography,fileURL,group,notes,source,rhythmandtranscription. Where
you readmetadata.composer?.value, readmetadata.composer.first?.value, or use every
element. The initializer defaults each field to[]. CeolKitDirective.justifyLast(Bool)is gone. It is replaced by.stretchLast(Double)and
.stretchStaff(Bool), andCeolKitDirectivealso gains.fontand.fontList.SVGRenderConfig.justifyLastSystemis gone. It is replaced bystretchLast(0…1;
1stretches every last line,0none) andstretchStaff.- Exhaustive
switches with nodefaultneed new arms forDiagnosticCode,
CeolKitFonts.Face(libertinusSerifBold,libertinusSerifBoldItalic) and
SMuFLGlyph(csymAccidental*).
Additive and source-compatible: ChordSymbol.init gains a defaulted segments:,
RenderedDocument.init a defaulted fonts:, and SVGRenderConfig gains scale,
fontLibrary, systemFonts and textFonts.
Deprecated directives. %%ceolkit:scale F is read as %%pagescale F, and
%%ceolkit:justifylast true|false as %%stretchlast 1|0. Both still work, with a
deprecatedDirective warning.
Engraved output changes for every document. The default %%scale 0.75 (a 4.5 pt staff
space) and abcm2ps's default font sizes change the size of everything, so pagination
changes. The last line of most tunes is now stretched or spaced like the line above it.
Repeat signs, chord symbols and spacing around text change as described above. Any
snapshot test that pins pages needs re-recording. To get back 1.9's 6 pt staff space, set
SVGRenderConfig.scale = 1.0 or write %%scale 1 in the file header. Text sizes still
follow abcm2ps's defaults, not 1.9's.
1.9.1
Stems join their noteheads cleanly.
A rendering fix. 1.9.0 drew each stem with its centreline on the notehead's edge, hung
flags from the stem's centreline, and stopped beams at the outer stems' centrelines, so
every join showed a step. No API changes.
Stems, flags and beams use the SMuFL anchors (#181)
Bravura says where a stem meets its notehead and where a flag meets its stem, in the
anchors of bravura_metadata.json. CeolKit parsed them but never read them. Now:
- A stem's outer corner sits on the notehead's
stemUpSE/stemDownNWanchor: its
centreline is half a stem inside the notehead's edge, and it starts 0.168 sp off the
notehead's centre. Stem tips do not move. - A flag is placed by its
stemUpNW/stemDownSWanchor against the stem's left edge at
its tip. - A beam, beam stub or grace beam runs from the outer edge of its first stem to the outer
edge of its last. - Grace notes follow the same anchors at grace scale, and grace stems are drawn at
grace-scaled thickness so they meet the grace flag's stem cap without a notch. The
acciaccatura slash is unchanged, and so is the width of a grace group.
Where a font lacks an anchor, the renderer falls back to the old bounding-box geometry.
What's Changed
- Fix #181 — stems, flags and beams attach at the SMuFL anchors by @sbeitzel in #182
- Release 1.9.1 by @sbeitzel in #183
Full Changelog: v1.9.0...v1.9.1
Upgrading
Source-compatible with 1.9.0.
Engraved output changes for every tune with stems: the stem and beam lines and the flag
positions move by fractions of a staff space. Note spacing is unchanged except after a
single flagged grace note, where the gap allowed for the flag's overhang is recomputed from
the flag's new position.
1.9
Quoted text is no longer lost.
Two fixes to quoted text that 1.8.0 parsed but lost before the renderer saw it. Both show
up in the same line of svpb-music's Bengullion.abc, [2 "repeat of part 2"{g}A…. One
adds a public enum case, which is why this is 1.9.0 and not 1.8.1.
Text before a grace group (#176)
"^text"{g}A and "G"{g}A attached the annotation or chord symbol to the first grace note,
and nothing draws text on grace notes, so it disappeared. §4.20 puts grace notes first, but
pipe music often writes the text ahead of the braces, and abcm2ps attaches it to the main
note. CeolKit now does the same: quoted text written before or inside a grace group goes to
the note after the }. A decoration inside the braces, as in {!accent!g}, still belongs to
its grace note.
Unprefixed text that is not a chord (#177)
Quoted text with no placement prefix is read as a chord symbol (§4.18). Text that did not
start with a note letter, like "repeat of part 2", was dropped with no diagnostic. Text that
did, like "Fine", became an F chord of quality "ine", which a transposer would turn into
"Gine".
- A chord symbol must now spell a chord from start to finish: a root and accidental
(♯/♭count), a type made of chord-quality words, signs and numbers (m,maj7,
dim,+,sus4,add9,7b5,ø, …), an optional/bass, and an optional alternate
chord like"G(Em)". - Anything else becomes an
Annotationat the newAnnotationPosition.chordLine. Nothing
that plays or transposes chords sees it, andChordSymbol.rootstays non-optional. - It is drawn as abcm2ps draws it, on the chord-symbol line. A note with both a chord symbol
and such text stacks them in the order written. - A new
unrecognisedChordSymbolwarning points at the string and suggests"^…".
CONFORMANCE.md has the rules.
What's Changed
- Fix #176 — quoted text before a grace group goes to the main note by @sbeitzel in #178
- Fix #177 — unprefixed text that is not a chord symbol is printed by @sbeitzel in #179
- Release 1.9.0 by @sbeitzel in #180
Full Changelog: v1.8.0...v1.9.0
Upgrading
Source-compatible with 1.8.0, with one exception: AnnotationPosition gains a chordLine
case, so an exhaustive switch over it with no default needs one more arm.
DiagnosticCode also gains unrecognisedChordSymbol.
Behaviour changes:
- Quoted text that is not a chord (
"Fine","D.C. al Fine","N.C.") is no longer a
ChordSymbol. If you read these strings fromNote.chordSymbol?.raw, read the
.chordLineentries ofannotationsinstead. Parses that contain such text now produce a
warning. ChordSymbol.qualityno longer includes an alternate chord:"G(Em)"has quality"".
rawstill holds the full string.
Engraved output changes for tunes that write quoted text before a grace group, or
unprefixed text that does not start with a note letter, because that text is now drawn. It
also changes for a note that carries both a chord symbol and such text, because the two
now stack. Other tunes are byte-identical to 1.8.0.
1.8
Chord symbols and annotations are drawn.
Two fixes, both to things the parser already understood but the renderer did not draw
correctly. Each adds a small amount of public API, which is why this is 1.8.0 and not 1.7.1.
Chord symbols and annotations (#171)
Chord symbols ("Am7", ABC v2.2 §4.18) and annotations ("^text", §4.19) were parsed onto
Note and Chord, and given room above the staff, but nothing drew them. They are now set
in the text face at twice the staff space, as outlines or <text> according to the
document's TextRendering.
| Written | Drawn |
|---|---|
"Am7" |
above the staff, on one line nearest it across the system |
"^text" |
above the staff, stacked over the chord symbol |
"_text" |
below the staff, above the lyrics |
"<text", ">text" |
beside the notehead, clear of its accidental or dot |
"@x,y text" |
offset from the notehead, in staff spaces |
The vertical layout reserves the lines each staff actually stacks. The first ^ annotation on
the first note of a variant ending, as in [2 "^repeat of part 2"A…, is set inside the
bracket just right of its number, which is where abcm2ps puts it.
Not done yet: the measure sizer does not widen a column for its text, annotations on rests and
bar lines attach to the next note, and chord symbols are printed as written (# and b are not
turned into ♯ and ♭). CONFORMANCE.md has the details.
ResolvedSystem.annotationFloors (new type AnnotationFloors) records where the layout
engine put the bands.
A variant ending that opens mid-measure (#172)
§4.10 ties only the end of an ending to a bar line, so a bare [2 may stand part way
through a bar, as a pickup that changes on the second time through usually is. CeolKit drew
the bracket from the bar line anyway. It now starts just ahead of the note that opens the
ending.
The model records the position as Measure.endingStartIndex, the index into events. It is
nil for an ending that opens at a bar line (|1, :|2, [1 written against a bar), and
those render exactly as before.
What's Changed
- Fix #171 — draw text annotations and chord symbols by @sbeitzel in #173
- Fix #172 — mid-measure variant ending bracketed from its note by @sbeitzel in #174
- Release 1.8.0 by @sbeitzel in #175
Full Changelog: v1.7.0...v1.8.0
Upgrading
Source-compatible with 1.7.0. The additions (Measure.endingStartIndex,
ResolvedSystem.annotationFloors, AnnotationFloors) are all behind defaulted initializer
parameters, and a Measure encoded by 1.7.0 decodes with endingStartIndex == nil.
Engraved output changes for any tune that has chord symbols or annotations, because they are
now drawn and the staff reserves room for them. It also changes for any tune with an ending
opened mid-measure. A snapshot test that pins such pages needs re-recording. Other tunes
are byte-identical to 1.7.0.
1.7
A footer can print the caller's own text.
One feature. A tool that binds several separately rendered ABC files into one document
often needs some of its own text in the footer, such as the name of the section each page
belongs to. ${name} marks (#137) let a consumer substitute text after rendering, but
only if it can draw that text. Under the default TextRendering.outlines, every glyph is
already a path, so a consumer without fonts cannot. CeolKit now draws it instead.
%%ceolkit:label (#168)
%%footer "${pagenumber}\t${label}\t$D"
%%ceolkit:label "Reels"
X:1
T:The Silver Spear
...
X:2
T:The Kesh
%%ceolkit:label "Jigs"
...
%%ceolkit:label "<text>" supplies the value of a ${label} footer mark. The quotes are
optional and are stripped the same way %%footer's are. It parses to the new
CeolKitDirective.label(String).
It is scoped like %%footer, not like %%ceolkit:pagenumber:
- In the file header it covers the whole document.
- In a tune header it covers that tune alone, and the last one in the header wins.
- A page prints the label of the tune whose music opens that page.
%%ceolkit:label ""in a tune header clears the file header's label for that tune.
The value is drawn exactly as written. $P, $T, $D and ${…} inside it are not
expanded.
Without the directive, ${label} draws "", which is what it drew before, so existing
footers do not change. See EXTENSIONS.md for the full description.
What's Changed
- Fix #168 — %%ceolkit:label for a ${label} footer mark by @sbeitzel in #169
- Release 1.7.0 by @sbeitzel in #170
Full Changelog: v1.6.1...v1.7.0
Upgrading
Source-compatible with 1.6.1, with one exception: CeolKitDirective gains a
label(String) case, so an exhaustive switch over it that has no default needs one
more arm.
Engraved output is byte-identical to 1.6.1 for any document that does not use
%%ceolkit:label.
1.6.1
A page says how big it is in the unit it was engraved in.
One fix. A unitless SVG length is a user unit, and a user unit is a CSS pixel — 1/96
inch, not 1/72 — so the root element's width="792" never said 11 inches, it said 8.25.
Every page CeolKit emitted declared itself at 75% of the size it was engraved for, and
anything that trusted that declaration got the smaller page.
The fix (#165)
The root now writes 792pt. The viewBox is unchanged, so the document is exactly
self-consistent: one user unit is one point, which is what every coordinate in the body
already assumed. Nothing about the drawing moves, and nothing about the geometry the
library computes changes — only what the page tells a reader about its own size.
What that reader was getting wrong:
- A PDF whose media box is taken from the document opened at 8.25 × 6.375 in rather than
11 × 8.5. rsvg-convertwithout an explicit-wrasterised at three quarters of the intended
scale.- A browser embedding the page at its natural size laid it out 25% small.
CeolKitSVGGeometry needed the matching change. It read the root through the generic
attribute reader, which drops any value Double(_:) cannot parse, so 792pt would have
made pageSize(in:) throw missingPageDimensions on every page. It now reads the
root's two lengths directly and resolves an absolute unit suffix — pt, px, in, pc,
cm, mm, or none — to points. A length it cannot resolve, such as 100%, is still
missingPageDimensions. The generic attribute reader is untouched, because body values
genuinely are unitless user units.
PageGeometry.width and .height are therefore now in points in fact, not only in the
documentation.
What's Changed
- Fix #165 — the root declares its size in points by @sbeitzel in #166
- Release 1.6.1 by @sbeitzel in #167
Full Changelog: v1.6.0...v1.6.1
Upgrading
Source-compatible with 1.6.0. No API changed.
The emitted SVG differs from 1.6.0 in exactly two characters per page — pt on the root's
width and height — so a snapshot test that pins page markup needs re-recording.
If you worked around the old declaration downstream, remove the workaround: a rasteriser
told to render at 4× no longer needs the 96/72 correction, and a PDF pipeline that read
the document's own size no longer needs to override it.
1.6
A directive belongs to the tune it was written in.
Six fixes, five of them the same bug wearing different clothes. A stylesheet directive
written in a tune header governs that tune (ABC v2.2 §4.23) — CeolKit had been letting
some of them run on to the end of the document, dropping others on the floor, and folding
%%footer into a single document-wide value that the last tune won. All of them are
scoped properly now. The sixth fix hands back the layout the renderer used to throw away,
so a consumer can finally say which page a tune landed on.
Highlights
Which page did this tune land on? (#152). A multi-tune document shares a page between
tunes whenever they fit, so the page a tune starts on is decided by the layout walk and is
written nowhere in the emitted SVG. render(_:) returned [String] and discarded the
layout that knew. renderDocument(_:) returns it instead:
let doc = try SVGRenderer(config: config).renderDocument(score)
doc.pages // exactly what render(_:) returns
doc.layout // the positioned layout, footers included
for p in doc.placements {
let title = score.tunes[p.tuneIndex].titles.first?.value ?? ""
print("\(title) … page \(p.printedPageNumber)")
}printedPageNumber is what the page prints, after %%newpage N and
%%ceolkit:pagenumber have renumbered it — not pageIndex + 1. topY is where the tune's
title block sits, so a tune packed in below another is distinguishable from one that opens
a page. Enough to print a table of contents, or answer "turn to page N". render(_:) is
unchanged and still there.
A tune header's layout directives no longer leak forward (#153). %%ceolkit:scale,
%%ceolkit:gracenotespacing, %%pipeformat and %%ceolkit:justifylast lived in vars
outside the renderer's tune loop and were never reset, so a scale written in tune 3's
header quietly re-engraved tunes 4 through 20 as well. Each tune now starts from what the
file preamble said, and applies its own header on top. scale, stemDirection,
straightFlags and graceSlurs travel with the TuneBlock for the same reason: one page
can hold systems from several tunes, and they can disagree.
%%footer is scoped, not document-wide (#155). The semantic pass carried one
var currentFooter across the whole file, last-wins, and stamped the survivor onto
Score. A tunebook where each tune names its own footer printed the last tune's footer
on every page. Tune.footer now holds what governs that tune, Score.footer holds what
the file header said, and a renderer resolves tune.footer ?? score.footer. A page belongs
to the tune whose music opens it — the new ResolvedPage.openingTuneIndex — because that
is the tune a reader turning to the page is reading. $T and %%dateformat follow the
same tune, so a footer reading "Tune title — date" is the right title with the right
format. %%footer "" in a tune header is a statement, not an absence: it suppresses a
file-header footer for that tune rather than inheriting it.
%%straightflags and %%graceslurs honour scope (#156). applyingScoreDirectives
read the first tune's directive list and ignored scope entirely, so these two were
broken in both directions at once: tune 1's header applied to the whole document, and every
later tune's was dropped. Only fileGlobal directives set the document value now; a tune
header's reaches that tune's blocks.
A file-level %%score / %%staves governs every tune it covers (#157). The same
asymmetry inverted: a plan written in the file preamble was promoted to the first tune
only, so tunes 2 onwards engraved with no plan at all. It now governs every tune it covers.
A plan is voice-specific in a way the other directives are not, so a tune that does not have
the voices the plan names is laid out as though the plan had not been written — reported as
staffPlanNotApplicableToTune — rather than half-applying it and dropping voices from a
tune that engraves perfectly well without it.
A page size is a property of a page (#158). %%landscape was folded into the preamble
and resolved last-wins for the document, which meant a %%landscape 0 written between
two tunes retroactively unrotated pages that had already been engraved landscape — and a
tunebook that opens landscape and turns portrait halfway through could not be expressed at
all. A page cannot change size part-way down, so a change of orientation is paired with the
%%newpage written at the same point:
%%landscape 1
X:1
T:First
…
%%newpage
%%landscape 0
X:2
T:Second
Page 0 comes out 792 × 612, page 1 612 × 792. The width follows, so systems on a landscape
page are broken and justified to the landscape line. A %%landscape with no %%newpage at
its own point is dropped with a landscapeWithoutPageBreak warning rather than reaching
forward to a break the author did not write. One in the file header is not a change at all —
it is the orientation the document opens in.
New in the model
| Type / member | Purpose |
|---|---|
Tune.footer |
the %%footer template governing this tune, or nil for Score.footer |
PageBreak.landscape |
the orientation the new page takes, from a %%landscape at this break |
StaffPlan.namedVoices |
every voice a plan names, in written order — the plan read as a question about a tune |
DiagnosticCode.landscapeWithoutPageBreak |
a %%landscape with no page boundary to start at |
DiagnosticCode.staffPlanNotApplicableToTune |
a file-level plan naming a voice some tune it governs does not have |
New in the renderer
| Type / member | Purpose |
|---|---|
SVGRenderer.renderDocument(_:) |
pages, plus the layout and where every tune landed |
SVGRenderer.renderDocument(_:diagnostics:) |
the same, collecting render diagnostics |
RenderedDocument |
pages, layout, placements |
TunePlacement |
tuneIndex, pageIndex, printedPageNumber, topY |
VerticalLayoutEngine.layoutDocument(_:firstPageNumber:) |
the layout together with the placements it worked out |
ResolvedPage.pageSize |
the size this page is packed to and written out at |
ResolvedPage.openingTuneIndex |
the tune whose music opens the page, and so owns its footer |
TuneBlock.stemDirection / .straightFlags / .graceSlurs |
per-tune values, nil to leave the document's standing |
ResolvedSystem.tuneStemDirection / .straightFlags / .graceSlurs |
the same, carried through to emission |
Smaller changes
SizeisHashablerather than onlyEquatable.ckprobereports aplacements:block — which page each tune landed on and what that
page prints as — in both its text and--jsonoutput.
Documentation
- CONFORMANCE.md — a new §9.1 on
%%landscape: where a change of orientation takes
effect, why the file header is read separately, and what is dropped. - README.md —
renderDocument(_:)in the module table,placementsin theckprobe
sample output.
What's Changed
- Fix #152 — report which page each tune landed on by @sbeitzel in #154
- Fix #153 — layout directives in a tune header no longer leak into following tunes by @sbeitzel in #159
- Fix #155 — %%footer and %%dateformat are scoped, not document-wide by @sbeitzel in #160
- Fix #156 — %%straightflags and %%graceslurs honour directive scope by @sbeitzel in #161
- Fix #157 — a file-level %%score / %%staves plan governs every tune it covers by @sbeitzel in #162
- Fix #158 — a page size is a property of a page, and %%landscape turns one at a %%newpage by @sbeitzel in #163
- Release 1.6.0 by @sbeitzel in #164
Full Changelog: v1.5.0...v1.6.0
Upgrading
Source-compatible with 1.5.0. Three signatures gain defaulted parameters and existing
calls compile unchanged: PageBreak.init, ResolvedPage.init and TuneBlock.init.
The behaviour changes are the point of the release, so if you worked around any of them
downstream, remove the workaround:
Score.footerno longer means "the document's footer, last occurrence wins". It is the
file header's footer — the one every tune starts from. Resolve a page's footer as
tune.footer ?? score.footer, or letrenderDocument(_:)/render(_:)do it.- A tune header's
%%ceolkit:scale,%%ceolkit:gracenotespacing,%%pipeformat,
%%ceolkit:justifylast,%%straightflagsand%%graceslursstop at the end of that
tune. A document that relied on one running on needs it written in the file header. %%landscapeoutside the file header now needs a%%newpageat the same point, and
turns only the pages from there on.- A
%%score/%%stavesin the file preamble now applies to every tune, not just the
first.
Engraved output for a single-tune document is byte-identical to 1.5.0.
1.5
Text you can write, and text you can take away.
Three fixes from the same downstream consumer that 1.4.0 served. A title can now hold
a percent sign, an I: directive in a header does what the standard says it does, and
text can be outlined without engraving a tune to get it.
Highlights
Outline text without a tune (#146). CeolKit could only turn text into glyph
outlines as a side effect of engraving a tune, so a page with text and no music — a
divider page rasterised by rsvg-convert alongside tune pages, say — meant engraving a
title-only tune and regex-matching its output apart. TextOutliner now does it
directly:
let title = try TextOutliner.outline("Competition Sets", face: .libertinusSerifItalic,
fontSize: 24)
title.svg // <g fill="black"><path …/>…</g>
title.advanceWidth // measure to centre or fit it
title.ascent // typographic ascender at this size
title.descent // typographic descender, as a positive distanceThe fragment is self-contained — pen origin at (0, 0) on the baseline, no ids, no
<use>, no <defs> — so it can be dropped into any SVG as many times as needed and
positioned with a <g transform>. TextOutliner.width(of:face:fontSize:) measures
without decoding outlines. Any bundled face works, including Bravura. Layout is shared
with the engraver rather than reimplemented, so a string measured here and the same
string engraved in a tune are the same width; engraved documents are byte-identical to
1.4.0.
\% is a literal percent sign (#145). ABC v2.2 §2.2.5 makes % start a comment
and \% a percent sign, but every information field cut its payload at the first %,
escaped or not: T:100\% Pipes came out as 100\, and no text field could hold a
percent sign at all. A backslash now escapes the character after it, so \% is not a
comment and \\% — an escaped backslash, then a bare % — is. Text fields decode \%
and \\ (§8.2); every other backslash sequence reaches the value verbatim, as before.
I: payloads keep their escapes, as a %% directive's do, so I:dateformat \%Y now
works where it was previously truncated.
I: stylesheet directives apply in file and tune headers (#148). §3.1.19 makes
I:name payload interchangeable with %%name payload, but only the tune body honoured
it. In the file header or a tune header — where I: fields normally go — the directive
was dropped with no diagnostic. Both headers now read it with the same scope and the
same unknownDirective diagnostic as the %% spelling, and in a tune header the two
spellings resolve by source order. I: fields that are parser instructions
(abc-version, linebreak, …) are unaffected, and a bare I: is ignored.
New in the renderer
| Type / member | Purpose |
|---|---|
TextOutliner.outline(_:face:fontSize:fill:) |
a string as a self-contained SVG outline fragment |
TextOutliner.width(of:face:fontSize:) |
the advance width, without decoding outlines |
OutlinedText |
the fragment, plus advanceWidth, ascent and descent |
CeolKitFontsError.unreadable(Face) |
a bundled face that is present but cannot be parsed |
Smaller changes
- A line that is
%%alone, or followed only by whitespace, is a comment. It used to
be read as a directive with an empty name and reported as "Unsupported stylesheet
directive '%%'". - Faces are parsed independently, so a request for one face is not failed by a problem
with another.
Documentation
- README.md —
TextOutlinerin the module table. - EXTENSIONS.md — a table of contents.
What's Changed
- Fix #145 — % is a literal percent sign in text fields by @sbeitzel in #147
- Fix #146 — public API to outline text outside a tune by @sbeitzel in #149
- Fix #148 — I: stylesheet directives apply in file and tune headers by @sbeitzel in #150
- Release 1.5.0 by @sbeitzel in #151
Full Changelog: v1.4.0...v1.5.0
Upgrading
Source-compatible with 1.4.0, with one exception: CeolKitFontsError gains an
unreadable(Face) case, so an exhaustive switch over it without a default needs
one more arm.
Three behaviour changes worth knowing about:
- Text fields containing
\%now keep the percent sign, and\\decodes to one
backslash. If you worked around #145 by spelling out "percent" or stripping a trailing
backslash, remove the workaround. I:stylesheet directives in a file or tune header now take effect. A file that
carried one — harmlessly, until now — will render differently.- A bare
%%line no longer produces anunknownDirectivediagnostic.
1.4
Pages you can number, and hand on.
Four fixes from a downstream consumer that assembles binders out of separately
rendered tunes. Page breaks are now something the source can ask for, page numbers
are something the source can set and a consumer can override, and a footer
placeholder that never worked is gone along with the silence that hid it.
Highlights
%%newpage starts a page, and can renumber it (#140). The directive is listed in
ABC v2.2 §11.4.7 and implemented by abcm2ps and abc2svg, so it lands under its
established unprefixed name — a file that page-breaks in abcm2ps does not need editing
to page-break here. Where it is written decides what it moves: the file header moves
the first tune, a gap between tunes moves the tune below it, a tune header moves that
tune, and inside the body it moves the stave it stands above. A break part-way through
a stave snaps back to the start of it — a page break is a system break — with a
pageBreakSnappedToStave warning; one past the last tune is dropped with
pageBreakAfterLastTune. %%newpage N renumbers the page it opens, and the count
carries on from there.
%%ceolkit:pagenumber reaches the page it numbers (#138). It was parsed,
validated, and then dropped: $P came from pageIndex + 1 and nothing else, so every
document numbered from 1 — including the "start numbering from page 3" example in
EXTENSIONS.md, which printed "Page 1". The renderer now reads the last .pageNumber
directive out of the first tune and numbers from it. It composes with %%newpage N
rather than competing: this directive names the document's opening page, and each
%%newpage N renames the one it opens. The base reaches the footer, $P,
${pagenumber} and the ceolkit-meta scroll-sync comment alike, so they cannot
disagree.
A footer span can be marked for a downstream consumer (#137). Outlining is a
one-way door: under the default .outlines a footer reading "Page 1" leaves the
renderer as six <use> elements and no text anywhere in the document, so a tool
assembling pages from several files has nothing left to rewrite. ${name} in a
%%footer template now marks a span as the consumer's to fill in. CeolKit still draws
its own value there, but wraps the span in a group carrying the name and the position,
size, family and anchor it drew at:
<g id="ceolkit-tag-pagenumber" class="ceolkit-tag" data-ceolkit-tag="pagenumber"
data-x="306" data-y="780" data-font-size="12"
data-font-family="Libertinus Serif" data-text-anchor="middle">…</g>A consumer empties the group and stamps its own text without owning the face. The
wrapper is emitted identically in every text-rendering mode, and a consumer that
ignores it gets exactly the output it always got. ${pagenumber}, ${pagecount},
${title} and ${date} have CeolKit values; any other name draws nothing and leaves a
positioned, empty group — a binder name is not CeolKit's to invent.
Unrecognised footer placeholders are diagnosed, and $Q is gone (#141).
EXTENSIONS.md documented a $Q "non-resetting page counter" that no commit on any
branch ever implemented; %%footer "$Q" engraved the literal characters, and under
.outlines engraved them as artwork, so the only way to find the gap was to find $Q
printed on a finished page. Removed rather than implemented. The class is covered too:
the parser now warns on every $-token a template uses that CeolKit does not
substitute, so an unimplemented abcm2ps placeholder ($F, $V, $P0) and a typo
($p) are both caught where they are written. A $ not followed by a letter is
ordinary text, and ${name} is a substitutable span — neither is a token, neither is
diagnosed.
New in the model
| Type / member | Purpose |
|---|---|
PageBreak |
a %%newpage, as a stave index plus an optional restart number |
Tune.pageBreaks: [PageBreak] |
the breaks the tune asks for, in source order |
Diagnostic.Code.unknownFooterPlaceholder |
a $-token CeolKit does not substitute |
Diagnostic.Code.pageBreakSnappedToStave |
a break mid-stave, moved to the stave's start |
Diagnostic.Code.pageBreakAfterLastTune |
a break with nothing left to move |
PageBreak is positional and deliberately not also a flattened CeolKitDirective:
a page break has no meaning apart from where it falls, which is how %%score is
modelled too.
New in the renderer
| Type / member | Purpose |
|---|---|
ForcedPageBreak |
a break the layout engine honours, by group index |
ResolvedPage.pageNumber |
the number the layout stamped on the page |
VerticalLayoutEngine.layout(_:firstPageNumber:) |
firstPageNumber defaults to 1 |
The layout engine now stamps each page as it decides where the page falls, because
only the walk that breaks the pages can honour a directive that breaks and renames at
once. The footer, $P, ${pagenumber} and the scroll-sync comment all read that
stamp.
ckprobe reports a tune's page breaks alongside its staff plans.
Documentation
- EXTENSIONS.md — the
${…}substitution mark; the list of the four$
placeholders CeolKit supports, and a plain statement that the set is narrower than
abcm2ps's;%%ceolkit:pagenumber's "Interaction with%%newpage" section is true
for the first time. - CONFORMANCE.md — a new §11.4.7 section on how CeolKit reads
%%newpage.
What's Changed
- Fix #137, #138 — footer substitution marks, and a page number that arrives by @sbeitzel in #142
- Fix #140 — %%newpage starts a page, and can renumber it by @sbeitzel in #143
- Release 1.4.0 by @sbeitzel in #144
Full Changelog: v1.3.0...v1.4.0
Upgrading
Source-compatible with 1.3.0. The only changed signature is
VerticalLayoutEngine.layout(_:), which gains a defaulted firstPageNumber:
parameter; existing calls compile unchanged.
Two behaviour changes worth knowing about:
- A document with
%%ceolkit:pagenumber Nin its preamble now actually numbers from
N. If you worked around the old behaviour by offsetting page numbers downstream,
remove the workaround. %%footertemplates using a$-token CeolKit does not substitute now produce a
warning. The engraved output is unchanged — the token is still drawn literally.