Today on a video call @m4rc1e and @chrissimpkins mentioned that VFs seem to use floating point values on interpolations and they had not seen any significant difference in rendering when using smaller UPM values. This diverges from our current understanding on the issue, which suggested using upm=2000 for better interpolation on VFs.
Both @tphinney and myself were a bit shocked to hear that. We need to double-check that info and if that's really the case, we need to update our upm check to remove the suggestion.
I'd like to make sure @davelab6 is also aware of this ongoing discussion, as he was involved in coming up with our current suggestion of upm=2000 for VFs.
My understanding in this area was based on older rasterizer behaviors from long ago. It is entirely _possible_ that some (or all!) modern rasterizers have gotten smarter in this area. I gather @m4rc1e and @chrissimpkins were specifically looking at FreeType? I am also very curious about various Microsoft rendering modes, as well as Apple and Adobe rendering.
Yes - I found my understanding of this was based on the freetype implementation at that time, which was fixed about a year ago - freetype was doing int rounding during the interpolation process - https://savannah.nongnu.org/bugs/?54371
@dberlow also wrote up his thoughts on this topic which led to me defining that FB check here,
https://github.com/TypeNetwork/Roboto-Flex/blob/master/docs/Sketch%20Docs/UPM%20resolution.pdf
And then Chris did an analysis of filesize and visible changes by scaling the UPM, and found that while 256 UPM makes some filesize savings and is very visible, above 1000 on a low contrast sans design did not make a visible improvement.
So, a UPM of 1000 is generally OK for VFs unless they have very thin stroke designs, such as a Weight axis with a range down to 1 (named Hairline)
@felipesanches do you need any more dogma info, or is this sufficient for you to propose a new check heuristic?
Nobody I know said, or wrote, that upm=2000 better interpolation on VFs.
My opinion is that 2000 upm is better for definition of some fonts, variable or not.
As mentioned above, we saw no perceptible improvements in static or animated interpolated renders in a wght+opsz L/G/C .ttf VF design space with UPM scaling above 1000 UPM at build time using a script developed by Cosimo (and posted below).
There was a ~ linear file size impact over the 250 UPM to 4000 UPM range:

Here is the UPM scaling script if anyone would like to replicate the analysis in different designs/char sets or review the study approach in more detail:
# Copyright 2020 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from copy import deepcopy
import logging
from ufoLib2 import Font
from ufo2ft.filters.transformations import TransformationsFilter
__all__ = ["scale_ufo"]
# fontinfo.plist attributes to include in scaling operation
INCLUDE_INFO_ATTRIBUTES = {
"unitsPerEm",
"descender",
"xHeight",
"capHeight",
"ascender",
"openTypeHheaAscender",
"openTypeHheaDescender",
"openTypeHheaLineGap",
"openTypeHheaCaretOffset",
"openTypeOS2TypoAscender",
"openTypeOS2TypoDescender",
"openTypeOS2TypoLineGap",
"openTypeOS2WinAscent",
"openTypeOS2WinDescent",
"openTypeOS2SubscriptXSize",
"openTypeOS2SubscriptYSize",
"openTypeOS2SubscriptXOffset",
"openTypeOS2SubscriptYOffset",
"openTypeOS2SuperscriptXSize",
"openTypeOS2SuperscriptYSize",
"openTypeOS2SuperscriptXOffset",
"openTypeOS2SuperscriptYOffset",
"openTypeOS2StrikeoutSize",
"openTypeOS2StrikeoutPosition",
"openTypeVheaVertTypoAscender",
"openTypeVheaVertTypoDescender",
"openTypeVheaVertTypoLineGap",
"openTypeVheaCaretOffset",
"postscriptUnderlineThickness",
"postscriptUnderlinePosition",
"postscriptBlueValues",
"postscriptOtherBlues",
"postscriptFamilyBlues",
"postscriptFamilyOtherBlues",
"postscriptStemSnapH",
"postscriptStemSnapV",
"postscriptBlueShift",
"postscriptDefaultWidthX",
"postscriptNominalWidthX",
}
def _scale_info(font, scale_factor):
def scale(v):
v *= scale_factor
if attr.startswith("openType"):
# ufoLib validators strictly require these to be formatted as integers
return round(v)
return v
for attr in INCLUDE_INFO_ATTRIBUTES:
value = getattr(font.info, attr)
if isinstance(value, list):
setattr(font.info, attr, [scale(v) for v in value])
elif value is not None:
setattr(font.info, attr, scale(value))
def _scale_kerning(font, scale_factor):
for pair, value in font.kerning.items():
font.kerning[pair] = value * scale_factor
def _scale_glyphs(font, scale_factor):
# ufo2ft transformations filter uses percentages for scale.
# NOTE: Make sure to use ufo2ft 2.14 as it fixes a bug with transformations of
# composite glyphs: https://github.com/googlefonts/ufo2ft/issues/378
scale = TransformationsFilter(ScaleX=scale_factor * 100, ScaleY=scale_factor * 100)
for layer in font.layers:
scale(font, glyphSet=layer)
for glyph in layer:
glyph.width *= scale_factor
glyph.height *= scale_factor
def scale_ufo(font, scale_factor, inplace=True):
if not inplace:
font = deepcopy(font)
_scale_info(font, scale_factor)
_scale_kerning(font, scale_factor)
_scale_glyphs(font, scale_factor)
return font
def main(args=None):
import argparse
parser = argparse.ArgumentParser("scale_ufo")
parser.add_argument(
"--output", metavar="OUTPUT_UFO", help="If omitted, save UFO in place"
)
parser.add_argument(
"input_ufo", metavar="INPUT_UFO", help="Path to input UFO to be scaled"
)
parser.add_argument(
"upem", metavar="UPEM", type=int, help="Units Per EM of the scaled UFO"
)
options = parser.parse_args(args)
logging.basicConfig(level="INFO")
font = Font.open(options.input_ufo, lazy=False)
scale_factor = options.upem / font.info.unitsPerEm
if scale_factor.is_integer():
scale_factor = int(scale_factor)
logging.info("scale factor: %s", scale_factor)
scale_ufo(font, scale_factor)
if options.output:
font.save(options.output, overwrite=True)
else:
font.save()
if __name__ == "__main__":
main()
thanks for all the feedback!
I will drop the "upm=2000 for VFs" recommendation from our fontbakery check, then.
thanks for all the feedback!
I will drop the"upm=2000 for VFs"recommendation from our fontbakery check, then.
That's not what I've requested above :)
@felipesanches do you need any more dogma info, or is this sufficient for you to propose a new check heuristic?
I though I did not need further info and that the "new check heuristic" would simply be that a finer grained upm grid is not actually needed in order to get good quality interpolations (meaning finer grained grids are only expected if the font design has details in its outlines that are only possible to express in such finer grids)
Other than that, lower upm values tend to be better due to file size optmization.
Did I miss something?
So, a UPM of 1000 is generally OK for VFs unless they have very thin stroke designs, such as a Weight axis with a range down to 1 (named Hairline)
It would be good to test this assumption as well. If we find it makes no difference, we should drop the check, otherwise we should only implement it for styles with a weight class <= n
The check was updated so that it does not include the upm=2000 dogma anymore. Please open new issues if there are further tweaks needed (such as the hairline special case @m4rc1e mentioned).
Most helpful comment
Nobody I know said, or wrote, that upm=2000 better interpolation on VFs.
My opinion is that 2000 upm is better for definition of some fonts, variable or not.