diff --git a/Lib/axisregistry/__init__.py b/Lib/axisregistry/__init__.py
index 30278fa91..284cf6481 100644
--- a/Lib/axisregistry/__init__.py
+++ b/Lib/axisregistry/__init__.py
@@ -13,6 +13,8 @@
import logging
from glob import glob
import os
+from axisregistry.greendot import green_dot
+
try:
from ._version import version as __version__ # type: ignore
@@ -98,12 +100,26 @@ def fallbacks_in_fvar(self, ttFont):
if axis not in self.keys():
log.warn(f"Axis {axis} not found in GF Axis Registry!")
continue
+ if axis == "opsz":
+ opsz_sizes = green_dot(
+ axes_in_font[axis]["min"], axes_in_font[axis]["max"]
+ )
+ # add min and max (Rosa request)
+ opsz_sizes += (
+ [axes_in_font[axis]["min"]]
+ + opsz_sizes
+ + [axes_in_font[axis]["max"]]
+ )
+ else:
+ opsz_sizes = []
for fallback in self[axis].fallback:
if (
fallback.value < axes_in_font[axis]["min"]
or fallback.value > axes_in_font[axis]["max"]
):
continue
+ if axis == "opsz" and fallback.value not in opsz_sizes:
+ continue
res[axis].append(fallback)
return res
diff --git a/Lib/axisregistry/greendot.py b/Lib/axisregistry/greendot.py
new file mode 100644
index 000000000..691cf6f0f
--- /dev/null
+++ b/Lib/axisregistry/greendot.py
@@ -0,0 +1,77 @@
+"""
+David Berlow's Green Dot algorithm:
+https://docs.google.com/document/d/15652Yabs0prnocpjG1TxG6zrfFSIYqOwlSIMkdbgqfg/edit?resourcekey=0-DXNZQLV2TbSqyn9HCLfhFA
+"""
+from collections import OrderedDict
+import sys
+
+
+size_ranges = OrderedDict(
+ {
+ (6, 12): [6, 7, 8, 9, 10, 11, 12],
+ (12, 18): [12, 14, 18],
+ (18, 32): [18, 24, 28, 32],
+ (32, 54): [32, 36, 48, 54],
+ (54, 144): [54, 60, 72, 96, 120, 144],
+ }
+)
+
+DOC_SIZES = [6, 7, 8, 9, 10, 11, 12, 14, 18, 24, 28, 36, 48, 60, 72, 96, 120, 144]
+
+SEGMENT_COUNT = 6
+
+
+def pin(n):
+ return min(DOC_SIZES, key=lambda x: abs(x - n))
+
+
+def mid(a, b):
+ return (a + b) / 2
+
+
+def green_dot(min_opsz, max_opsz):
+ pos_1 = get_first_pos(min_opsz, max_opsz)
+ pos_5 = get_fifth_pos(min_opsz, max_opsz)
+ pos_3 = get_third_pos(min_opsz, max_opsz)
+ pos_2 = mid(pos_1, pos_3)
+ pos_4 = mid(pos_3, pos_5)
+ if pos_5 - pos_1 < 3:
+ return [pos_1, pos_5]
+ return sorted(set(pin(o) for o in (pos_1, pos_2, pos_3, pos_4, pos_5)))
+
+
+def get_first_pos(min_opsz, max_opsz):
+ for smallest, largest in size_ranges:
+ if min_opsz <= smallest and max_opsz >= largest:
+ return mid(smallest, largest)
+ return min_opsz
+
+
+def get_fifth_pos(min_opsz, max_opsz):
+ best_range = None
+ for smallest, largest in size_ranges:
+ if min_opsz <= smallest and max_opsz >= largest:
+ best_range = (smallest, largest)
+ if not best_range:
+ return max_opsz
+
+ best_range = size_ranges[best_range]
+ mid = best_range[int(len(best_range) / 2)]
+ return int((max_opsz + mid) / 2)
+
+
+def get_third_pos(min_opsz, max_opsz):
+ segment_size = (max_opsz - min_opsz) / SEGMENT_COUNT
+ return min_opsz + 1.5 * segment_size
+
+
+def parse_range(string):
+ return tuple(map(int, string.split(",")))
+
+
+if __name__ == "__main__":
+ if len(sys.argv) != 2:
+ print("Usage: python -m gftools.greendot 0,10")
+ sys.exit()
+ opsz_range = parse_range(sys.argv[1])
+ print(green_dot(*opsz_range))
diff --git a/tests/data/RobotoFlex[GRAD,XOPQ,XTRA,YOPQ,YTAS,YTDE,YTFI,YTLC,YTUC,opsz,slnt,wdth,wght]_STAT.ttx b/tests/data/RobotoFlex[GRAD,XOPQ,XTRA,YOPQ,YTAS,YTDE,YTFI,YTLC,YTUC,opsz,slnt,wdth,wght]_STAT.ttx
index ccf8080f0..4d3a16a6e 100644
--- a/tests/data/RobotoFlex[GRAD,XOPQ,XTRA,YOPQ,YTAS,YTDE,YTFI,YTLC,YTUC,opsz,slnt,wdth,wght]_STAT.ttx
+++ b/tests/data/RobotoFlex[GRAD,XOPQ,XTRA,YOPQ,YTAS,YTDE,YTFI,YTLC,YTUC,opsz,slnt,wdth,wght]_STAT.ttx
@@ -19,56 +19,56 @@
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
@@ -186,168 +186,96 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
-
+
-
+
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
+
-
+
-
-
-
-
-
-
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
diff --git a/tests/test_greendot.py b/tests/test_greendot.py
new file mode 100644
index 000000000..551ec26e6
--- /dev/null
+++ b/tests/test_greendot.py
@@ -0,0 +1,27 @@
+import pytest
+from axisregistry.greendot import green_dot
+
+
+@pytest.mark.parametrize(
+ "range,expected",
+ [
+ # Bodoni-Moda
+ ((6, 96), [9, 18, 28, 48, 72]),
+ # Fraunces
+ ((9, 144), [14, 28, 48, 72, 120]),
+ # Imbue
+ ((10, 100), [14, 24, 36, 48, 72]),
+ # Literata
+ ((7, 72), [14, 18, 24, 36, 60]),
+ # Newsreader
+ ((6, 72), [9, 14, 24, 36, 60]),
+ # Piazolla
+ ((8, 30), [14, 18, 24]),
+ # Texturina
+ ((12, 72), [14, 18, 28, 48, 60]),
+ # NDA
+ ((17, 18), [17, 18]),
+ ],
+)
+def test_green_dot(range, expected):
+ assert green_dot(*range) == expected