What the original Slice does
A numbered map of the original program's behaviour
A behavioural map of Slice 0.7.1 by Source
Foundry, read out of its source rather than its documentation. Every entry is a numbered,
testable claim; the test corpus in tests/suite/ references these IDs, and every claim
carries at least one case.
Read this before reading the corpus. It is the answer to “what is the thing we are trying to be compatible with”, and several entries record behaviour that turns out to be wrong — noted as such, with the evidence, rather than quietly reproduced.
How to read an entry
Each claim has an ID, a statement, where it lives in the original, and a class:
| class | meaning |
|---|---|
spec |
the OpenType specification requires this |
fonttools |
behaviour inherited from fontTools, which the original delegates to |
slice-ui |
the application’s own contract, documented or evident from its interface |
judgement |
a defensible design choice with no external authority behind it |
suspect |
behaviour that looks like a defect; carries the evidence and a proposed verdict |
A suspect entry is not a licence to differ. It is a flag for adjudication: the test
corpus encodes what the behaviour should be, the original is scored against that, and
the disagreement is resolved on evidence.
A. Loading a font
A1 · slice-ui · A file is accepted as a font if fontTools.ttLib.TTFont can open
it. Anything else raises, and the error is shown in a dialog with the exception text as
detail.
(__main__.py:707 load_font)
A2 · slice-ui · A font is treated as variable if and only if it has an fvar
table. A font without one is rejected with: “The font is missing the OpenType fvar table
and is not recognized as a variable font.”
(models.py:454 FontModel.is_variable_font)
A3 · fonttools · TrueType, CFF, WOFF and WOFF2 inputs are all readable, because
TTFont reads all four. The open dialog filters on .ttf .otf .woff .woff2.
(dialogs.py:44)
A4 · slice-ui · On a successful load the status bar reads
{family} {version} loaded ({n} axes), where family is nameID 1 and version is nameID 5
truncated at the first ;.
(__main__.py:746, models.py:160)
A5 · slice-ui · Loading a font resets all six bit-flag checkboxes to unchecked,
regardless of what the font contains.
(__main__.py:730-735) — also D5, and see suspect there.
B. Axis Editor — parsing and validation
B1 · slice-ui · An empty cell means “leave this axis alone”: the axis keeps its
original range and stays in the output.
(models.py:288)
B2 · slice-ui · A cell containing : is a restricted range. Anything else
non-empty is a pin.
(models.py:292)
B3 · slice-ui · A pin is parsed with Python’s float().
(models.py:299)
B4 · slice-ui · A pin that float() rejects raises
'{value}' is not a valid {tag} axis value. Please enter a single numeric value and try
again.
(models.py:302)
B5 · slice-ui · A range is parsed with the regular expression
(?P<start>-?\d+(\.\d+)?)\s*:\s*(?P<end>-?\d+(\.\d+)?)\s*(\[\s*(?P<default>-?\d+(\.?\d+)?)\s*\])?
(models.py:200)
B6 · slice-ui · A reversed range is sorted, so 800:400 and 400:800 mean the
same thing.
(models.py:344)
B7 · slice-ui · A restricted range must contain the axis’s original default value,
or it is rejected with The {tag} range {min}:{max} does not include the default axis
value ({default}). This is currently a requirement. This is the Level 3 sub-spacing rule:
the compiler cannot move a default axis location.
(models.py:352, and the project README’s footnote 1)
B8 · slice-ui · A trailing [default] group parses and is then discarded. The
code comments name it as groundwork for Level 4 sub-spacing.
(models.py:322-324)
suspect. Accepting 200:700[400] and silently producing 200:700 gives the user a
font that is not what they asked for, with no warning. Proposed verdict: an unsupported
request must be refused, not silently reinterpreted. Evidence: the OpenType specification
has no notion of “ignore part of the user’s request”; and the value is parsed, so the
program demonstrably understands what was asked and declines to say it cannot do it.
B9 · slice-ui · A range whose bounds fall outside the axis’s own extent is
not checked by the editor. It reaches fontTools, which clamps it into range.
(no check in models.py; fontTools AxisTriple.limitRangeAndPopulateDefaults)
B10 · fonttools · A pin outside the axis extent is likewise not checked, and is
clamped by fontTools.
suspect. Measured against the original on a 300/300/1000 wght axis:
| typed | result |
|---|---|
5000 |
accepted, usWeightClass 1000 |
-9999 |
accepted, usWeightClass 300 |
Proposed verdict: warn or refuse. Weak suspect — clamping is what fontTools does
deliberately, and a case can be made either way. Flagged for adjudication rather than
assumed.
B11 · The pin parser is float(), which accepts nan, inf, -inf, 1e3 and
leading/trailing whitespace.
suspect. Measured against the original, on a 300/300/1000 wght axis:
| typed | result |
|---|---|
inf |
accepted, silently clamped to 1000 |
-inf |
accepted, silently clamped to 300 |
nan |
refused, but by fontTools deep inside normalization, with the message Ooops... v=nan, triple=(300.0, 300.0, 1000.0) |
1e3 |
accepted as 1000, which is correct |
An earlier draft of this entry claimed a NaN would propagate into the outlines. That is
wrong and the measurement above corrects it: fontTools catches NaN before it reaches
any delta, and the output coordinates are always finite. The real defects are smaller but
real: inf is not a weight, and silently reading it as “the heaviest available” invents
an intent the user never expressed; and Ooops... is not an error message a user can act
on. Proposed verdict: reject non-finite input in the editor, with a message naming the
axis. Evidence: OpenType axis coordinates are Fixed (16.16), a format with no
representation for infinity or NaN, so no such value can ever be written to a font — the
input is meaningless rather than merely out of range.
B12 · The range parser uses re.search, not re.fullmatch.
suspect. xyz200:700zzz matches, because search finds 200:700 anywhere in the
string. So does 400:700 garbage. Proposed verdict: the whole cell must parse. Evidence:
every other malformed entry is refused (B4); accepting a value embedded in noise is
inconsistent with that and hides typos.
B13 · slice-ui · If every cell is empty, the Slice button refuses the job: “You
requested the same design space that is supported in the font path that you are
processing. Please define at least one axis location or restricted axis range.”
(models.py:363, __main__.py:579)
C. Axis Editor — display
C1 · slice-ui · One row per fvar axis, in fvar order, labelled with the axis
tag.
(models.py:247-277)
C2 · slice-ui · The read-only column shows {min} : {max} [{default}] using the
fvar values.
(models.py:262)
C3 · slice-ui · The row tooltip is a human-readable axis name: the registered name
for ital opsz slnt wdth wght, the Google Fonts registry name for
CASL CRSV XPRN GRAD MONO SOFT WONK, otherwise the axis’s own name table entry.
(models.py:380-408)
C4 · slice-ui · The axis-name lookup consults the font’s name table only as a
fallback, so a font that names wght something unusual still displays “Weight”.
(models.py:404)
D. Name Editor
D1 · slice-ui · Nine rows, for nameIDs 1, 2, 3, 4, 6, 16, 17, 21, 22, labelled
01 Family, 02 Subfamily, 03 Unique, 04 Full, 06 Postscript, 16 Typo Family,
17 Typo Subfamily, 21 WWS Family, 22 WWS Subfamily.
(models.py:63-88)
D2 · slice-ui · Only the Windows / Unicode BMP / English (US) record — platform 3,
encoding 1, language 1033 — is read and written.
(models.py:93-95, instanceworker.py:93)
D3 · slice-ui · nameIDs 1, 2, 3, 4 and 6 are always written to the output, even
when the editor cell is empty.
(instanceworker.py:97-101)
D4 · slice-ui · nameIDs 16, 17, 21 and 22 are written when the cell is non-empty
and removed from the font when it is empty.
(instanceworker.py:108-136)
D5 · The editor loads nameIDs 1, 2, 3, 4 and 6 from the font. It never loads 16, 17,
21 or 22.
(models.py:90-142 — no branch for those IDs)
suspect, and consequential. Combined with D4, every font Slice touches loses its
Typographic and WWS family names. For a family with more than four styles those records
are how the OS groups it; stripping them silently breaks style linking. Proposed verdict:
load all nine, so an untouched editor preserves what the font had. Evidence: the
OpenType name specification describes nameIDs 16/17 as required “if the font has more
than four styles that are part of a family”; deleting them from such a font makes it
non-conformant. This is the clearest defect in the program.
D6 · slice-ui · nameID 5 is read for the status bar’s version display but is not
editable and is not rewritten.
(models.py:38-43)
E. Bit Flag Editor
E1 · slice-ui · Four OS/2.fsSelection checkboxes: bit 0 ITALIC, bit 5 BOLD,
bit 6 REGULAR, bit 8 WWS.
(__main__.py:380-384)
E2 · slice-ui · Two head.macStyle checkboxes: bit 0 BOLD, bit 1 ITALIC.
(__main__.py:387-388)
E3 · slice-ui · Each checkbox sets its bit when ticked and clears it when unticked.
Bits not exposed by the editor keep whatever value the font had.
(models.py:415-435)
E4 · Loading a font clears all six checkboxes (A5), and slicing then writes that
state.
suspect. Any font whose OS/2.fsSelection has bit 6 REGULAR set — which is most
Regular-weight fonts — has it cleared unless the user notices and re-ticks it. The
resulting font declares itself neither Regular nor Bold nor Italic. Proposed verdict:
read the bits from the font, so an untouched editor is a no-op. Evidence: the OpenType
OS/2 specification states that bit 6 “should be set if the font is a regular style” and
that bits 0, 5 and 6 are mutually informative; silently clearing them changes how the
font is classified. This is a data-loss defect of the same shape as D5.
E5 · The editor permits contradictory combinations — REGULAR together with BOLD, or
fsSelection BOLD without macStyle BOLD — with no warning.
suspect, mild. The OpenType OS/2 specification says bit 6 “is mutually exclusive
with bits 0 and 5”, and that head.macStyle bits 0 and 1 “must be consistent with”
fsSelection bits 5 and 0. Proposed verdict: still allow it, since the user may be
correcting a font deliberately, but say so. A warning, not an error.
F. The Slice action
F1 · slice-ui · With no font loaded, the status bar reads “Requires a font path”
and nothing happens.
(__main__.py:561)
F2 · slice-ui · Axis entries are validated before the save dialog opens; a parse
error is shown and the job abandoned.
(__main__.py:573-577)
F3 · slice-ui · The job is refused if it does not narrow the design space (B13).
(__main__.py:579)
F4 · slice-ui · If the font-path text field no longer matches the loaded font, the
job is refused: “The file path in the font path field does not match the loaded font
path. Please load your font again.”
(__main__.py:589)
Not portable: it exists only because that field is editable.
F5 · slice-ui · Cancelling the save dialog sets the status to “Canceled” and does
nothing else.
(__main__.py:604)
F6 · slice-ui · The work runs on a QThreadPool with a modal indeterminate
progress dialog; failure shows an error dialog with the exception text and sets the
status to “Failed”.
(__main__.py:610-650)
G. What is written
The original delegates the whole of instancing to one call:
instantiateVariableFont(self.ttfont, axis_instance_data, inplace=True, optimize=True)
(instanceworker.py:83)
so its output behaviour is fontTools 4.x’s, at that call’s defaults. The claims below are the ones a test can observe.
G1 · fonttools · Pinning every axis yields a static font: no fvar, no gvar, no
HVAR/VVAR/MVAR. STAT survives with all of its design axis records, which is
why name IDs that only STAT refers to legitimately remain in a font with no axes left
(see G5 and G10).
G2 · fonttools · Pinning some axes and restricting others yields a variable font
whose fvar holds only the surviving axes, with the narrowed extents.
G3 · fonttools · avar segment maps are renormalized onto the new extents.
G4 · fonttools · Named instances that fall outside the new design space are dropped
from fvar.
G5 · fonttools · STAT is kept; its axis values outside the new limits are
dropped, and its design axis records are kept in full.
G6 · fonttools · GSUB/GPOS feature variations are resolved: a condition set that
holds at the new default has its substitutions folded into the feature list, one that
cannot hold is dropped, one that still depends on a surviving axis is renumbered and
renormalized.
G7 · fonttools · OS/2.usWeightClass, OS/2.usWidthClass and post.italicAngle
are set from the pinned location (varLib.set_default_weight_width_slant).
G8 · fonttools · maxp’s point, contour and composite maxima, the head bounding
box and flags bit 1, and hhea’s extremes are recalculated from the final outlines on
save (maxp.recalc and hhea.recalc, both gated on recalcBBoxes).
An earlier draft said “maxp … recalculated” without qualification, which overstates
it. maxp’s instruction fields are not touched. Measured: planting
maxSizeOfInstructions = 1 into a font whose longest glyph program is 5 bytes, then
instancing and saving, leaves it at 1. Those fields are inherited from the input, so a
font that arrives with them wrong stays wrong.
G9 · fonttools · OS/2.xAvgCharWidth is recalculated.
G10 · fonttools · Name records that existed only to name axes and instances the
output no longer has are pruned.
G11 · fonttools · DSIG is deleted, because it signs bytes that no longer exist.
G12 · fonttools · At the default overlap mode (KEEP_AND_SET_FLAGS), a static
instance gets OVERLAP_SIMPLE set on the first point of every simple glyph with
contours, and OVERLAP_COMPOUND on the first component of every composite — not on
every component, and not at all on a glyph with zero contours such as an empty
.notdef.
G13 · fonttools · Outlines at the requested location match what a renderer produces
for the variable font at that location.
G14 · The original never removes overlaps. instantiateVariableFont accepts
overlap=OverlapMode.REMOVE, and the original does not pass it.
Not a defect — the missing feature this project exists to add. Recorded here so the
corpus can mark cases that only the new program is expected to pass.
H. Containers
H1 · The output container is the input’s container, whatever the user names the
file. TTFont.save never looks at the filename: it passes self.flavor to
SFNTWriter (ttFont.py:411), and self.flavor was set from the reader when the font
was opened (ttFont.py:318).
suspect. Measured, saving each input under each name:
| opened | saved as | container actually written |
|---|---|---|
.ttf |
.woff |
sfnt |
.ttf |
.woff2 |
sfnt |
.woff |
.ttf |
WOFF |
.woff2 |
.ttf |
WOFF2 |
An earlier draft of this entry asserted the opposite and cited dialogs.py:44, which is
the open dialog’s filter and says nothing about saving. The correction matters: a user
who opens a TTF and types Bold.woff gets an sfnt with a .woff name, which a web
server will send with the wrong MIME type and a build tool will misidentify. Proposed
verdict: the extension chooses the container, which is what the save dialog’s own filter
list implies it will do. Evidence: WOFF and WOFF2 are distinct formats with their own
signatures (wOFF, wOF2), and the file extension is the only thing the user was given
to express which one they wanted.
A consequence: H2’s zopfli setting only ever takes effect when the input was already a WOFF, since that is the only way a WOFF gets written.
H2 · slice-ui · WOFF output is compressed with zopfli, because the worker sets
sfnt.USE_ZOPFLI = True before saving.
(instanceworker.py:69)
Observable only as file size, and not a correctness property; a WOFF written with any
deflate encoder is equally valid.
I. The shell
Recorded for completeness. None of it is observable in an output font, and none of it is portable to a web page unchanged, so the corpus does not test it.
I1 File menu: Open Font (Ctrl+O), Quit. I2 References menu: fvar, head, name, OS/2 specification links. I3 Help menu: About, Check for Updates, Release Notes, Documentation, View License, View Source, Issue Tracker, Report a Bug. I4 Drag and drop onto the font-path field. I5 Status bar carrying the version number. I6 The window omits its own title and logo when the screen is under 1000 px tall.
Summary of suspected defects
Eight, in descending order of how much damage they do:
| id | defect | proposed verdict |
|---|---|---|
| D5 + D4 | Typographic and WWS family names are stripped from every font | load all nine records |
| E4 + A5 | OS/2.fsSelection and head.macStyle bits are cleared on load and written cleared |
read the bits from the font |
| B11 | a pin of nan or inf is accepted |
reject non-finite values |
| B12 | re.search accepts a range embedded in surrounding garbage |
require the whole cell to parse |
| B8 | [default] is parsed and silently discarded |
refuse the request |
| B10 | an out-of-range pin is silently clamped | warn; weak, flagged for adjudication |
| H1 | the output container is the input’s, whatever the file is named | the extension chooses |
| E5 | contradictory bit combinations pass unremarked | warn |
Each is encoded in the corpus as the corrected behaviour, so the original is expected to fail those cases. Section 4 of the plan adjudicates each one on evidence before any of it is treated as settled.