From 1e52b86db6f97057b83802dab8e29bdc5930f7d9 Mon Sep 17 00:00:00 2001 From: Marc Foley Date: Tue, 11 Jul 2023 14:52:20 +0100 Subject: [PATCH 1/2] add greendot algo --- Lib/axisregistry/__init__.py | 10 ++ Lib/axisregistry/greendot.py | 77 ++++++++++ ...FI,YTLC,YTUC,opsz,slnt,wdth,wght]_STAT.ttx | 138 +++++------------- tests/test_greendot.py | 27 ++++ 4 files changed, 147 insertions(+), 105 deletions(-) create mode 100644 Lib/axisregistry/greendot.py create mode 100644 tests/test_greendot.py diff --git a/Lib/axisregistry/__init__.py b/Lib/axisregistry/__init__.py index 30278fa91..4d824b2ae 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,20 @@ 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 From c4fe769abd25779dc771d770d49e22bd96a3d782 Mon Sep 17 00:00:00 2001 From: Marc Foley Date: Tue, 11 Jul 2023 15:05:05 +0100 Subject: [PATCH 2/2] black --- Lib/axisregistry/__init__.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/Lib/axisregistry/__init__.py b/Lib/axisregistry/__init__.py index 4d824b2ae..284cf6481 100644 --- a/Lib/axisregistry/__init__.py +++ b/Lib/axisregistry/__init__.py @@ -101,9 +101,15 @@ def fallbacks_in_fvar(self, ttFont): 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"]) + 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"]] + opsz_sizes += ( + [axes_in_font[axis]["min"]] + + opsz_sizes + + [axes_in_font[axis]["max"]] + ) else: opsz_sizes = [] for fallback in self[axis].fallback: