Draw an OpenType SVG font in colour (#76) - #300
Merged
Merged
Conversation
jakejackson1
added this pull request to stack #301
September 22, 2026 00:18
jakejackson1
force-pushed
the
feature/76-svg
branch
from
September 22, 2026 00:24
1a9c3b5 to
8540c28
Compare
jakejackson1
removed this pull request from stack #301
September 22, 2026 00:27
jakejackson1
added this pull request to stack #302
September 22, 2026 00:27
jakejackson1
force-pushed
the
feature/76-svg
branch
from
September 22, 2026 00:31
8540c28 to
5cbbc1d
Compare
jakejackson1
force-pushed
the
feature/76-svg
branch
from
September 22, 2026 00:40
5cbbc1d to
ab2b75f
Compare
jakejackson1
force-pushed
the
feature/76-svg
branch
from
September 22, 2026 01:57
ab2b75f to
3ecd4f2
Compare
jakejackson1
force-pushed
the
feature/76-svg
branch
6 times, most recently
from
September 22, 2026 02:45
77a06bb to
d4b2d38
Compare
jakejackson1
force-pushed
the
feature/76-svg
branch
2 times, most recently
from
September 22, 2026 03:29
6c41a97 to
8205e99
Compare
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
jakejackson1
force-pushed
the
feature/76-svg
branch
from
September 22, 2026 03:49
8205e99 to
124d929
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Phase 5, the last phase of the colour emoji plan for #76. Stacked on #299; when it merges, GitHub will retarget this one down the stack. Fonts carrying an OpenType SVG table now draw in colour.
What changes
SvgSourcereads the SVG table.glyphNelement, falls to the next source (its outline). So does every glyph when PHP lacks the DOM extension, or zlib for a gzipped document; each case is logged once.SvgRendereris a new, small renderer for what the OpenType SVG spec requires of a glyph.Image\Svgis untouched: it has no clipPath, turns group opacity into per-element opacity, and registers gradients and states in the page's lists, with transparent gradients drawn through patterns sized to the page.g,use(with x/y), and nestedsvgdrawn asg.rgb()and named colours.currentColoris the colour of the text: the fill sets no colour, as a COLR foreground layer does.var(--colorN, fallback)reads CPAL throughColorFontFile::colour(), or uses the fallback where the font has no such entry.linearGradientandradialGradient, in user space or the bounding box.gradientTransform,spreadMethod, a focus point, and stops inherited throughhref.clip-path: one shape clips withW n(orW* nfor evenoddclip-rule). Several shapes clip through a soft mask of them all, since a clip path's shapes add up while a PDF path's winding may cancel. BothclipPathUnitsare supported.mask: a soft mask of the mask content's luminosity, clipped to the mask's region, in eithermaskUnitsandmaskContentUnits. PDF takes the luminosity of the colours as they are, where SVG 1.1 converts them to linear RGB first; the two agree for white, black and anything between drawn at an opacity.opacitypaints the element as a group; a shape with only a fill takes it into the fill instead.fill-opacityandstroke-opacityare supported.image: a PNG or JPEGdata:URL, placed withpreserveAspectRatio.viewBox(withwidth,heightandpreserveAspectRatio), presentation attributes, and thestyleattribute.Not drawn, each with a warning:
textand the other elements a font must not use;<style>element's rules;filter,patternandmarker;A gradient stroke is drawn in its middle stop's colour.
Mpdf\Image\Svg\Pathparses path data into moves, lines and cubics. It lives besideImage\Svg, asTag\*does besideTag, because nothing in it is specific to glyphs andImage\Svgcan use it later; it writes its numbers throughUtils\NumericString::decimal(), so it depends on nothing inFonts. It handles:Shared with COLRv1:
FillsWithGradients, the gradient fill, is moved out ofColrV1Source. It carries Draw a COLR version 1 font in colour (#76) #299's spec fixes, so SVG gradients get them too: a radial gradient is cut where its radius reaches 0, and colours are interpolated premultiplied by alpha. CPAL asks the latter of SVG glyphs as well as COLR ("For both the COLR and SVG tables, interpolation must be done with alpha pre-multiplied").Geometryholds the matrices and boxes. Its number formatting goes through the newUtils\NumericString::decimal(), whichImage\Svg\Pathuses too.GlyphOutline::number()does the same and moves to it in a follow-up.ColorLine::middle()is the middle-stop fallback, for the COLR sweep and the SVG gradient stroke.ColorFontFilegainsbbox()andwarn()/log(), and each warning is now logged once, COLRv1's included.GlyphResources::alpha()setsCAas well asca, so a semi-transparent stroke is drawn directly rather than through a group. COLR never strokes, so its glyphs are unchanged. The COLRv0 and COLRv1 snapshots are refreshed for the extra key; their pages are the same.SVGsits afterCOLRv0inColorFormats::SOURCES.CACHE_FORMATgoes from 8 to 9, since an SVG-only font cached before this was cached as not drawable.New fixture
TestEmoji-SVG.ttf, built bybuild.py. Each emoji's document shows another feature:use;<style>;hrefgradient with a bounding-box clip.The England flag has no document, so it is drawn from its outline. The other fixture fonts rebuild byte-identical.
Checked against a real font
1, U+20E3 or the tag characters. It is meant to sit behind a text font, so this is not an SVG issue.gradientTransform.Faster path parsing
preg_match_all, instead of one anchoredpreg_matchper number. Numbers are read in the same loop, with the command's case worked out once.01100comes out as one number token. A flag takes that token's first character and leaves the rest as the next token.SvgRendererkeeps each path's parse, by its data, for the life of the document. A shape drawn many times throughuse, or several paths with the same data, are parsed once. Twemoji has nouse, so it gains nothing there.NumericString::decimal()writes a whole number as it is, without rounding it.Two snapshots
color-emoji-svg, the test font. EachTestEmoji-SVGglyph is built to show another part of SVG, not to look like its emoji, so several look unfinished on purpose: a notch in the heart, a clipped half thumb, the England flag as a black square. A table now names each one, says what it tests and why it looks as it does, before the runs of text.color-emoji-svg-noto, a real font (new). A subset of Adobe's OpenType-SVG Noto Color Emoji,NotoColorEmoji-SVG-Subset.ttf(200 KB), so a regression in real artwork is obvious at a glance.subset_noto_svg.pybeside it rebuilds it byte for byte from the v2.100 release. It keeps only the glyphs the snapshot's emoji need, and converts the fallback outlines from CFF to TrueType, since mPDF reads only TrueType outlines. The SVG documents are unchanged.NotoColorEmoji-SVG-OFL.txt; it declares no Reserved Font Names, so the subset keeps its name. Like every fixture it's undertests/, which.gitattributesleaves out of the Composer archive.viewBox, opacity; Twemoji: paths, circles, ellipses, transforms), so the test font stays for everything else.Kept as it is
An
.otfSVG emoji font, with CFF outlines, still can't be loaded: mPDF reads only TrueType outlines. Converting CFF outlines on load would be a PR of its own.CACHE_FORMATis still raised by hand when a format is added, as theSOURCESdocblock says. Folding the format list into the cache check would be the general fix, but it reaches beyond this PR.Tests
Image\Svg\PathTest: each path command, compact numbers, commas and stray characters, errors, arcs (including degenerate ones and flags run into what follows), bounds, shapes.SvgSourceTest:useloop;usetwice beside a path with the same data, parsed once;NumericStringTest: whole numbers, rounding, trailing zeros,-0, and no decimal places.ColorFontTest: the SVG font's image, shading and mask as written to the PDF, and the England flag's outline.ColorFormatsTest: SVG is detected and drawn.color-emoji-svg, now labelled, andcolor-emoji-svg-noto.🤖 Generated with Claude Code