Paint compiler for COLRv1 fonts
Project description
Paint compiler for COLRv1 fonts
% paintcompiler --output "Example-Color.ttf" Example-Mono.ttf
There's a huge amount of very clever and pretty things you can do with the COLRv1 font fonmat. However, most font editors only expose a very small subset of the capabilities of the format. This is largely because, due to the fact that COLRv1 is so rich and extensive, it's not easy to produce a user interface which exposes all the functionality in a flexible way.
paintcompiler
is a Python library and command line tool which makes it (slightly) easier to add COLRv1 COLR
and CPAL
tables to your fonts, using all the features that the format has to offer. That's the positive side. The negative side is that you need to describe your COLRv1 paints as Python code.
COLRv1 describes color glyphs as a kind of tree structure. In paintcompiler
these tree nodes are described using Python functions. For example, to say that the glyph A
is made up of a red square
glyph and a blue circle
glyph, you would say:
glyphs["A"] = PaintColrLayers([
PaintGlyph("square", PaintSolid("#FF0000FF")),
PaintGlyph("circle", PaintSolid("#0000FFFF"))
])
The full interface is described below. See the example in example/paints.py
for a paint file which exercises all paints.
Using paintcompiler
To use paintcompiler
, you need to write a paint definition file. This is a Python program which specifies which paints are applied to each glyph. paintcompiler
will call your Python program in an environment with certain variables predefined. Your job is to fill the glyphs
dictionary with the result of certain Paint...()
function calls. In the example above, we call PaintColorLayers
to define our top-level paint, and associate this with the A
glyph.
As well as the glyphs
dictionary and the Paint...()
(and ColorLine
/VarColorLine
) functions, paintcompiler
also provides the font
variable, which is a fontTools.ttLib.TTFont
object representing the font. This means you can query the font and define your paints programmatically. For example:
SOLIDRED = PaintSolid("#FF0000FF")
BLUEDOT = PaintGlyph("dot", PaintSolid("#0000FFFF"))
for glyphname in font.getGlyphOrder():
advancewidth = font["hmtx"][glyphname][0]
glyphs[glyphname] = PaintColrLayers(
# Paint the glyph in red
PaintGlyph(glyphname, SOLIDRED),
# Put a blue dot in the bottom right corner of the glyph
PaintTranslate(advancewidth - 100, -100, BLUEDOT)
)
Typically the paint definition file is called paints.py
. If not, you can use the -p
/--paints
flag on the command line to specify a different name.
Using paintdecompiler
If you have an existing COLRv1 font and you want to turn it back into a Python definition (paints file), you can use the paintdecompiler
utility included with this module to do this. It is not smart enough to e.g. extract shared colorlines or variations into a Python variable, but it should get you started.
$ paintdecompiler example/COLRv1-Test.ttf
Written on paints.py
$ head paints.py
glyphs["p1_PaintColrLayers"] = PaintColrLayers(
[
PaintGlyph("square", PaintSolid("#EA4335FF")),
PaintGlyph("circle", PaintSolid("#4285F4FF")),
],
)
glyphs["p2_PaintSolid"] = PaintGlyph("square", PaintSolid("#EA4335FF"))
...
Variation specifications
You can create variable paints by passing a variation specification to the paint function. This variation specification describes how the values to the paint functions vary at different positions in the designspace, and is made up of a dictionary mapping a tuple of axis/location pairs to a value. For example, to create a glyph which rotates -180 degrees when the ROTA
axis is at -1.0 and rotates 359 degrees when the ROTA
axis is at 2.0, do this:
PaintRotate(
{
(("ROTA", -1.0),): -180,
(("ROTA", 2.0),): 359,
},
PaintGlyph("square", PaintSolid("#FF0000FF"))
)
Notice that the method here is PaintRotate
. You can also call PaintVarRotate
, but the compiler knows that this has a variation specifier, and so should be upgraded to a variable rotation.
(A variation specification can also be written as a variable-FEA-like string "ROTA=-1.0:-180 ROTA=2.0:359"
, but that's deprecated and you'll get a warning.)
You can specify multiple axes in your variation specification by adding more axis/location pairs to the tuple:
PaintRotate(
{
(("ROTX", -1.0), ("ROTY", -1.0)): -180,
(("ROTX", -1.0), ("ROTY", 0.0)): -90,
(("ROTX", 2.0), ("ROTY", 0.0)): -90,
(("ROTX", 2.0), ("ROTY", -1.0)): -359,
},
PaintGlyph("square", PaintSolid("#FF0000FF"))
)
Adding synthetic axes
When adding variable paints, you might want to add additional variation axes to your font - in other words, the axes only control the variable paints, and so aren't present when the font is compiled as a non-color font. fontcompiler
provides the --add-axis
command line flag to add one or more axes to your font:
$ paintcompiler \
--add-axis "ALPH:0:0.5:1:Alpha value" \
--add-axis "STAX:0:0:1000:Start X coordinate" \
--add-axis "STAY:0:0:1000:Start Y coordinate" \
--output "Example-Color.ttf" \
Example-Mono.ttf
Now let's look at the functions that are available.
Paint Functions
PaintColrLayers(paints)
Stacks a number of paints together. paints
should be an array of paints returned from other paint functions.
PaintSolid(color_or_colors, alpha=1.0)
Creates a solid color paint, with the (potentially varying) given alpha value. A color string must be either an eight digit hex RGBA value, or the string foreground
to specify the current ink color in the user's application. color_or_colors
is either a single color string or, to give the user a choice of different color palettes, a Python list of color strings. The alpha
parameter can either be a float from 0.0 to 1.0 or a variation specification as described above.
PaintLinearGradient(
(start_x, start_y),
(end_x, end_y),
(rot_x, rot_y),
colorline
)
Creates a linear gradient which starts and ends and the given co-ordinates, and is rotated around the rot_x
,rot_y
coordinate. Any of the coordinates may either be floats or variation specifications.
The colors on the gradient are specified using a ColorLine
, as described below:
ColorLine(stops, extend="pad")
ColorLine(start_stop, end_stop, extend="pad")
A color line can be specified using a dictionary mapping positions along the gradient (from 0.0 to 1.0) to stops, where each stop is either a color_or_colors
or a tuple (color_or_colors, alpha)
; it may also be specified using two stops, in which case one is taken as the start and the other the end. Hence, the following calls are all equivalent:
ColorLine({
0.0: ("#FF0000FF", 1.0),
1.0: ("#00FF00FF", 1.0)
})
ColorLine({0.0: "#FF0000FF", 1.0: "#00FF00FF"})
ColorLine("#FF0000FF", "#00FF00FF")
The alpha value of a stop may vary, to make a variable color line:
ALPHA_AXIS = { (("ALPH", 0.0),): 0.0, (("ALPH", 1.0),): 1.0 }
PaintLinearGradient(
(0, 0), (1000, 1000), (0, 1000),
ColorLine(
{
0.0: "#FF0000FF",
0.5: ("#0000FFFF", ALPHA_AXIS),
1.0: "#00FF00FF"
}
)
)
and the position of a stop may also vary, in which case a list of tuples must be used as a parameter to ColorLine
instead of a dictionary:
STOP_AXIS = { (("STOP", 0.0),): 0.0, (("STOP", 1.0),): 0.8 }
ALPHA_AXIS = { (("ALPH", 0.0),): 0.0, (("ALPH", 1.0),): 1.0 }
PaintLinearGradient(
(0, 0), (1000, 1000), (0, 1000),
ColorLine([
(STOP_AXIS, "#FF0000FF"),
(1.0, ("#00FF00FF", ALPHA_AXIS))
])
)
The extend mode of a ColorLine
can be pad
, repeat
or reflect
.
PaintRadialGradient(
(pt0_x, pt0_y),
radius_0,
(pt1_x, pt1_y),
radius_1,
colorline
)
Creates a radial gradient made up of the cylinder defined by two circles, the first centered at pt0_x, pt0_y
with radius radius_0
and the second centered at pt1_x, pt1_y
with radius radius_1
. Any of the coordinates and either of the radii may either be floats or variation specifications.
PaintSweepGradient((pt_x, pt_y), start_angle, end_angle, colorline)
Creates a sweep gradient centered at pt_x, pt_y
made up of the arc between start_angle
and end_angle
. Any of the coordinates and either of the angles may either be floats or variation specifications.
PaintGlyph(glyph, paint)
Uses a (non-color) glyph's outline to mask the drawing of the given paint specification. For example, PaintGlyph("A", PaintSolid("#FF0000FF"))
will use the shape of the letter "A" to mask out a solid red paint; the effect of this is to draw the letter in red. But PaintGlyph("B", PaintGlyph("A", PaintSolid("#FF0000FF"))
uses the shape of the letter "B" as a mask to further mask out the painting; the effect of this is to draw the overlap between the A and the B outlines in red.
PaintColrGlyph(glyph, paint=None)
Re-uses a color glyph's definition, either to paint it, or as a mask to mask another paint.
PaintTransform([xx, xy, yx, yy, dx, dy], paint)
Transforms the given paint using an affine matrix. Any of the matrix elements may either be floats or variation specifications.
PaintTranslate(dx, dy, paint)
Translates the given paint in X and Y dimensions. Any of the translation coordinates may either be floats or variation specifications.
PaintScale(scale_x, scale_y, paint, center=(pt_x, pt_y))
PaintScale(scale_x, paint, center=(pt_x, pt_y))
PaintScale(scale_x, scale_y, paint)
PaintScale(scale_x, paint)
Scales the given paint in X and Y dimensions. Any of the scale factors or the center coordinates may either be floats or variation specifications. If scale_y
is not specified, a uniform scale is assumed. If the center is not specified, the scaling is performed around the origin.
PaintRotate(angle, paint, center=(pt_x, pt_y))
PaintRotate(angle, paint)
Rotates the paint the given angle. The angle and/or the center coordinates may either be floats or variation specifications. If the center is not specified, the rotation is performed around the origin.
PaintSkew(angle_x, angle_y, paint, center=(pt_x, pt_y))
PaintSkew(angle_x, angle_y, paint)
Skews the paint the given angles in X and Y dimensions. Any of the angles and/or the center coordinates may either be floats or variation specifications. If the center is not specified, the skewing is performed around the origin.
PaintComposite(mode, src_paint, dst_paint)
Composites the source paint onto the destination paint. mode
must be one of 'clear', 'src', 'dest', 'src_over', 'dest_over', 'src_in', 'dest_in', 'src_out', 'dest_out', 'src_atop', 'dest_atop', 'xor', 'plus', 'screen', 'overlay', 'darken', 'lighten', 'color_dodge', 'color_burn', 'hard_light', 'soft_light', 'difference', 'exclusion', 'multiply', 'hsl_hue', 'hsl_saturation', 'hsl_color', 'hsl_luminosity'
.
Specifying colors
There are multiple ways of specifying colors to be used to paint your glyphs,
depending on the complexity of your project. For simple projects, where there
are few colors and a single color palette, you may find it easiest to specify
colors directly using a hex string of the form #RRGGBBAA
, as we have done
so far in these examples:
PaintGlyph("square", PaintSolid("#FF0000FF"))
If you wish to use multiple palettes, you can write a list of strings instead of a single string:
PaintGlyph("square", PaintSolid([
"#FF0000FF", # Red in pallete 0
"#00FF00FF", # Green in pallete 1
]))
You may to a certain extent mix these two styles: if you have
PaintGlyph("square", PaintSolid("#FF0000FF"))
...
PaintGlyph("triangle", PaintSolid([
"#00FF00FF",
"#0000FFFF",
]))
then square
will be red in both palettes, but triangle will be blue in palette 0
but blue in pallete 1. However, this will give an error:
PaintGlyph("square", PaintSolid([
"#FF0000FF",
"#00FF00FF",
"#0000FFFF",
]))
...
PaintGlyph("triangle", PaintSolid([
"#00FF00FF",
"#0000FFFF",
# It's not clear what color should be used for palette 2 here.
]))
For more complex projects, it may be easier to declare your color palettes
in advance, and give numeric indices into the palette. You do this with the
SetColors
function. However, note that the palettes are specified as a
list of entries, just like we have been doing so far.
SetColors([
["#FF000000FF", "#00FF000FF"],
"#0000FFFF",
])
PaintGlyph("square", 0) # Entry 0 -> red in palette 0, green in palette 1
PaintGlyph("circle", 1) # Entry 1 -> blue in both palettes
OpenType allows for the designer to specify that certain palettes (not colors!)
are designed for light-mode or dark-mode interfaces - or even both. You can set
this information in the font with the SetDarkMode
and SetLightMode
functions.
These take a palette index and must be called after colors have been set up:
SetColors([
["#FF000000FF", "#00FF000FF"],
"#0000FFFF",
])
SetLightMode(0) # Red and blue
SetDarkMode(1) # Green and blue
Why not just use fontTools.colorLib.builder
?
paintcompiler
does use the fontTools color builder underneath to construct the COLR tables, but it adds a few helpful facilities on top:
-
paintcompiler
provides a command line interface to add COLR tables. This command line interface allows adding synthetic axes. -
Color palettes are built automatically, and with the flexibility described above.
-
In
paintcompiler
, paint operations are functions; this makes the syntax considerably less verbose and easier to follow. Compare
PaintColrLayers([
PaintGlyph("square", PaintSolid("#FF0000FF")),
PaintGlyph("circle", PaintSolid("#0000FFFF"))
])
versus
(
ot.PaintFormat.PaintColrLayers,
[
{
"Format": ot.PaintFormat.PaintGlyph,
"Paint": {
"Format": ot.PaintFormat.PaintSolid,
"PaletteIndex": 1,
"Alpha": 1.0,
},
"Glyph": "square",
},
{
"Format": ot.PaintFormat.PaintGlyph,
"Paint": {
"Format": ot.PaintFormat.PaintSolid,
"PaletteIndex": 2,
"Alpha": 1.0,
},
"Glyph": "circle",
},
]
)
-
colorLib.builder
does not provide any help when specifying variable paints; you have to keep track of theVarIndexBase
and populate the variation store yourself. Inpaintcompiler
, you can specify how a paint varies directly, and the variation table gets built for you. -
colorLib.builder
requires you to explicitly choose the appropriate paint representation.paintcompiler
uses the parameters to work out the correct paint format; a singlePaintScale
function allows access toPaintScale
,PaintVarScale
,PaintScaleAroundCenter
,PaintVarScaleAroundCenter
,PaintScaleUniform
,PaintVarScaleUniform
,PaintScaleUniformAroundCenter
andPaintVarScaleUniformAroundCenter
. This greatly reduces the number of functions that you need to worry about.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
File details
Details for the file paintcompiler-0.3.4.tar.gz
.
File metadata
- Download URL: paintcompiler-0.3.4.tar.gz
- Upload date:
- Size: 23.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/3.6.0 importlib_metadata/4.8.2 pkginfo/1.8.1 requests/2.25.1 requests-toolbelt/0.9.1 tqdm/4.62.3 CPython/3.9.18
File hashes
Algorithm | Hash digest | |
---|---|---|
SHA256 | 80c8a9b8a29207bc7744b75d36c75bff09a81795b261a08df1c45686953f682c |
|
MD5 | e6a1303ed8cbaf61be5877f837a01d62 |
|
BLAKE2b-256 | 805d2676c1b26e5a0aeed08fc1c80ff49d55bf9d4729c50cd4e1da539d1bc3eb |
File details
Details for the file paintcompiler-0.3.4-py3-none-any.whl
.
File metadata
- Download URL: paintcompiler-0.3.4-py3-none-any.whl
- Upload date:
- Size: 16.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/3.6.0 importlib_metadata/4.8.2 pkginfo/1.8.1 requests/2.25.1 requests-toolbelt/0.9.1 tqdm/4.62.3 CPython/3.9.18
File hashes
Algorithm | Hash digest | |
---|---|---|
SHA256 | df505bc9d19e3e08274832e36faef7042008e6bf431e77d130b65a03a410a7e7 |
|
MD5 | 9a508851a6c9133ebf71dc9bc79af59e |
|
BLAKE2b-256 | 6824c4821a30cbc354b6eb7d20012d2281b79704475bad1308405ea27907f1e6 |