Skip to content

Lots of improvements (OTF fixes, more GPOS, synthetic bold/italics) - #1

Open
jeske wants to merge 10 commits into
StbSharp:masterfrom
jeske:master
Open

Lots of improvements (OTF fixes, more GPOS, synthetic bold/italics)#1
jeske wants to merge 10 commits into
StbSharp:masterfrom
jeske:master

Conversation

@jeske

@jeske jeske commented Jul 28, 2026

Copy link
Copy Markdown

This is not intended to be accepted as is, but to start a conversation, because SafeStbTrueTypeSharp was last touched 6 years ago.

I've been making fixes and improvements to my fork, as a fully memory safe font implementation for my projects.

I'm soon going to diverge further, so before that happens, I thought I would have a conversation to see what (if any) changes you'd like to upstream.

For example, my first two commits would be just easy nice fixes to cherry pick:

  • 0e0ac91 - fix OTF/CFF parsing bugs
  • d59b43f - fixes for multi-component glyphs, and more OTF tests

After this I started working on some light extensions of the GPOS kerning support with a HarfBuzz test oracle, y-grid-fit support, and a first cut at a Synthetic bold+italic outline processor. (there are some thin feature deficiencies compared to my Skia Oracle, but it's not terrible, and i'll get them). I also have a Type-1 to OTF converter in another repo I've been considering adding to this.

If anyone is maintaining upstream, let me know your thoughts.. otherwise, this can sit as a pointer to others looking for added features in a memory safe font engine...

Thanks for SafeStbTrueTypeSharp!

jeske added 10 commits July 1, 2026 17:11
…standard2.0/net9.0 lib; killed 482 phantom CS0518s in IDE tooling) - lint now covers src + tests only
Move the CFF/OTF console repro harness (TestOtfLoading) into
tests/TestOtfLoading/ to make room for the upcoming GposKerning.Tests
xunit project (HarfBuzz numeric kerning oracle). ProjectReference path
updated; UNLICENSED-FONTS.md scope stays with the harness fonts.
xunit + HarfBuzzSharp (TEST-ONLY), hb_font scale pinned to unitsPerEm for
exact integer font-unit comparison against stbtt_GetGlyphKernAdvance.
Feature set pinned explicitly (+kern; liga/clig/calt/curs/dist off) to
prevent GSUB substitution contaminating the xAdvance delta.

Expected-RED baseline (5776 pairs/font, 0 substitutions):
  consola 0/0 PASS; Roboto 28/412 mismatched (all quote-glyph pairs);
  times 92/92 and arial 88/88 lost entirely (extension-lookup signature).
Roboto-Regular.ttf committed (Apache-2.0, see FONTS.md); Arial/Times/
Consolas loaded from C:\\Windows\\Fonts, skip-guarded.
…class-0 fix

stbtt_GetGlyphKernAdvance now matches HarfBuzz exactly (oracle: 5776
pairs x 4 fonts, 0 mismatches, exact font-unit integer equality).

- Script/feature resolution: latn (fallback DFLT) default LangSys ->
  'kern' feature -> lookup indices, resolved once per font and cached;
  applied in LookupList index order, accumulating across lookups.
  Previously ALL lookups in the font were scanned (undefined order,
  non-kern features included).
- Extension lookups (type 9): dispatch inner type-2 subtables via
  32-bit extensionOffset. Was silently skipped - Arial/Times lost
  their ENTIRE kern sets (88/92 pairs).
- Generalized ValueRecords: size = 2*popcount(valueFormat), xAdvance
  at offset 2*popcount(vf & 0x0003); device-table bits count toward
  size only. Was hard-rejecting anything but vf1=4/vf2=0.
- ClassDef fix (Common.cs): unlisted glyphs are class 0 per spec, not
  -1. Restores Roboto quote-pair kerning (28 pairs) via correct
  fmt1->fmt2 subtable fall-through with HarfBuzz 'applied' semantics.
- Legacy 'kern' table fallback now also covers fonts whose GPOS has
  no latn/DFLT kern feature (font-level decision, never per-pair).
- LookupFlags deliberately ignored: only IgnoreMarks (0x0008) seen in
  target fonts, a no-op for base-glyph pairs.

Tests: KernOracleTests (HarfBuzz numeric oracle, now gating) plus
synthetic ValueRecordTests covering 8 valueFormat combos x fmt1/fmt2
blobs - 21 tests green.
For SilkyNvg plans/crisp_text_yonly_gridfit_hinting.md (FreeType 'slight'
analog). New public delegate stbtt_YPixelGridFitRemap: a monotonic remap
over y-up pixel space, applied to FLATTENED outline points right before
edge building (piecewise-linear maps apply exactly; no control-point
straddle distortion). x is never touched, so x-subpixel phase shifts
compose freely.

- Bmp.stbtt__rasterize / stbtt_Rasterize: optional y_remap parameter
- FontInfo.stbtt_GetGlyphBitmapBoxSubpixelYRemap: box corners through
  the same remap so mask and origin agree
- FontInfo.stbtt_MakeGlyphBitmapSubpixelYRemap: remapped raster entry
- FontInfo.stbtt_GetGlyphHorizontalLineEdges: flat horizontal segment
  detector (bar edges + ink side from winding; TT CW / CFF CCW) for
  stem-bar snapping
…n, oblique shear, horizontal scaling

SilkyNvg plans/crisp_text_synthetic_styles.md Phase A2. New FontInfoSynth.cs:

- SynthOutline.SynthesizeOutline: applies the synth transform ONCE to the
  stbtt_vertex[] outline in font units (y-up), upstream of rasterization.
  1. Embolden (Skia's fake-bold technique): flatten contours (~1/256 em),
     build +/-halfWidth offset rings (miter joins, limit 4, bevel fallback),
     append as line contours; NON-ZERO winding computes the union at raster
     time. Winding normalization: dominant fill sign w = sign(sum of contour
     areas); each annulus emits larger ring wound w, smaller wound -w, so
     stroke bands add flesh-signed ink and net zero inside - inner rims never
     erase, holes shrink correctly, collapsed counters saturate (never cancel).
  2. Shear/scale: x' = (x + y*skewX) * scaleX on all coordinates including
     control points (linear map - quadratics stay quadratics).
- stbtt_GetGlyphShapeSynth / stbtt_GetGlyphBitmapBoxSubpixelSynth /
  stbtt_MakeGlyphBitmapSubpixelSynth (+ y_remap variants): box computed from
  the transformed outline's control polygon so mask and box always agree.
- FontInfo made partial (FontInfo.cs one-line change).

netstandard2.0-compatible (no MathF/Math.Clamp/index-from-end).
…ding erase)

David's diagnosis (2026-07-29): the A2 construction emitted a +/-d loop PAIR
per contour. The -d loop of a flesh contour is region-theoretically redundant
(it only cancels one layer over the eroded interior) and is exactly the loop
that SELF-CROSSES wherever the local stroke width t < 2d (script tails, thin
connectors) - its locally-inverted lobes broke the cancellation and left
0-winding (uncovered) patches INSIDE thin features: the 'lightened tail'
artifact, identical on both SilkyNvg raster tiers because the geometry is
shared.

Fix: offset EVERY contour by -fillSign*d (away from ink), PRESERVING source
traversal orientation, and append just that one loop. The algebra collapses
flesh (grow outward) and holes (shrink inward) to one uniform formula.
Winding field: deep interior 2, new stroke band 1, hole centers 0. Inward
loops never exist; outward self-crossings at concave corners are
double-POSITIVE lobes - winding-safe under both nonzero and |signed|
evaluation. Collapse-inverted hole loops keep their inverted sense, so
collapsed counters SATURATE solid (correct bold degeneracy). Also halves the
appended geometry per styled glyph.

The loop-PAIR construction is retained behind EmboldenLoopPairLegacyMode:
without the fill layer it is the stroke BAND builder (band +/-1, interior 0 =
hollow outline ring) that PDF Tr render modes 1/2 need - the B1 seed
(SilkyNvg plans/crisp_text_synthetic_styles.md Part B).

Measured (SilkyNvg FontShapeTortureTest synthbold vs Skia): Miama 0.71->0.54%,
Across Before 0.66->0.56%, sans fonts unchanged (no t<2d features);
SyntheticGlyphPerfectionTest registry cells 4-10x better, now at unstyled
baseline - 100% of the thin-features-turning-midgray artifacts eliminated.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant