Skip to content

Releases: sbeitzel/CeolKit

2.1

Choose a tag to compare

@sbeitzel sbeitzel released this 04 Oct 22:29
a9bae96

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= on K: and V: (#224) shifts Note.pitch in the semantic pass, scoped as
    abcm2ps scopes it: a header K: covers every voice, a body K: or a V: its own voice,
    and each lasts until another octave= replaces it. Transposition.statesOctave lets
    octave=0 reset it.
  • A K: with a clef and no key (#223). K:bass, K:clef=bass and [K:clef=bass] are
    read as options only (§3.1.14, §4.6) instead of being rejected. KeySignature records
    what was stated (statesKey, statesClef); what was left unsaid carries on from what is
    in force, so K:G after K:bass stays in the bass clef. An unreadable K: still warns,
    and now changes nothing.
  • Clef changes are drawn (#223). A clef change part way through a voice is a new
    Event.clefChange where 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.key is set only by a K: 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 .auto voice, 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

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

2.0

Choose a tag to compare

@sbeitzel sbeitzel released this 04 Oct 01:08
dc1e37d

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,
    %%wordsfont and %%setfont-1…4 take 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…$4 switch to the %%setfont-n face, $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, a FontLibrary), 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). OpenTypeFont reads glyf outlines (simple and
    composite) and .ttc collections, and checks OS/2.fsType before embedding a face.
  • Probing fonts (#191). CeolKitFonts.availableFaces(config:) lists the faces text could
    be set in, and RenderedDocument.fonts reports what each kind of text asked for and got.
    %%ceolkit:fontlist [resolved|available] reports both as diagnostics. ckprobe gains
    --fonts, --system-fonts and --font-file.

Layout, as abcm2ps does it

  • %%scale and %%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:scale is deprecated.
  • %%stretchlast and %%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:justifylast is 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
    new ChordSymbol.segments. "C7(b9)" is now read as type 7(b9).
  • W: words (#187) are kept in Tune.words and 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: or Z: lines keeps every value, in source order. Each C: prints on a
    row of its own.

What's Changed

Full Changelog: v1.9.1...v2.0.0

Upgrading

Source-breaking changes

  • Ten TuneMetadata properties change from optional to array: composer, area, book,
    discography, fileURL, group, notes, source, rhythm and transcription. Where
    you read metadata.composer?.value, read metadata.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), and CeolKitDirective also gains .font and .fontList.
  • SVGRenderConfig.justifyLastSystem is gone. It is replaced by stretchLast (0…1;
    1 stretches every last line, 0 none) and stretchStaff.
  • Exhaustive switches with no default need new arms for DiagnosticCode,
    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

Choose a tag to compare

@sbeitzel sbeitzel released this 26 Sep 15:51
c3e0a37

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 / stemDownNW anchor: 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 / stemDownSW anchor 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

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

1.9

Choose a tag to compare

@sbeitzel sbeitzel released this 24 Sep 15:00
01351da

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 Annotation at the new AnnotationPosition.chordLine. Nothing
    that plays or transposes chords sees it, and ChordSymbol.root stays 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 unrecognisedChordSymbol warning points at the string and suggests "^…".

CONFORMANCE.md has the rules.

What's Changed

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 from Note.chordSymbol?.raw, read the
    .chordLine entries of annotations instead. Parses that contain such text now produce a
    warning.
  • ChordSymbol.quality no longer includes an alternate chord: "G(Em)" has quality "".
    raw still 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

1.8

Choose a tag to compare

@sbeitzel sbeitzel released this 23 Sep 20:11
c8828de

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

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

1.7

Choose a tag to compare

@sbeitzel sbeitzel released this 22 Sep 22:35
f165774

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

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

Choose a tag to compare

@sbeitzel sbeitzel released this 22 Sep 03:33
c8e1a1b

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-convert without an explicit -w rasterised 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

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

1.6

Choose a tag to compare

@sbeitzel sbeitzel released this 18 Sep 19:26
e5d9931

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

  • Size is Hashable rather than only Equatable.
  • ckprobe reports a placements: block — which page each tune landed on and what that
    page prints as — in both its text and --json output.

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, placements in the ckprobe
    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.footer no 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 let renderDocument(_:) / render(_:) do it.
  • A tune header's %%ceolkit:scale, %%ceolkit:gracenotespacing, %%pipeformat,
    %%ceolkit:justifylast, %%straightflags and %%graceslurs stop at the end of that
    tune. A document that relied on one running on needs it written in the file header.
  • %%landscape outside the file header now needs a %%newpage at the same point, and
    turns only the pages from there on.
  • A %%score / %%staves in 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

1.5

Choose a tag to compare

@sbeitzel sbeitzel released this 15 Sep 18:45
2669952

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 distance

The 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 — TextOutliner in the module table.
  • EXTENSIONS.md — a table of contents.

What's Changed

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 an unknownDirective diagnostic.

1.4

1.4

Choose a tag to compare

@sbeitzel sbeitzel released this 04 Sep 16:03
971a809

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

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 N in its preamble now actually numbers from
    N. If you worked around the old behaviour by offsetting page numbers downstream,
    remove the workaround.
  • %%footer templates using a $-token CeolKit does not substitute now produce a
    warning. The engraved output is unchanged — the token is still drawn literally.