Skip to content

Commit

Permalink
Add CBDT to maximum_color
Browse files Browse the repository at this point in the history
  • Loading branch information
rsheeter committed Apr 11, 2022
1 parent ce81cd9 commit 5533f7d
Show file tree
Hide file tree
Showing 12 changed files with 446 additions and 180 deletions.
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ can be generated:
```shell
# Adds COLR to a font with SVG and vice versa
maxmium_color my_colr_font.ttf

# Adds COLR to a font with SVG and vice versa, and generates a CBDT table
maxmium_color --bitmaps my_colr_font.ttf
```

The intended result is a font that will Just Work in any modern browser:
Expand All @@ -44,7 +47,7 @@ The intended result is a font that will Just Work in any modern browser:
| --- | --- | --- |
| COLR | Chrome 98+ | https://developer.chrome.com/blog/colrv1-fonts/ |
| SVG | Firefox, Safari | |
| CBDT | Chrome <98 | |
| CBDT | Chrome <98 | Only generated if you pass `--bitmaps` to `maximum_color`|

Note that at time of writing Chrome 98+ prefers CBDT to COLR. Also CBDT is
huge. So ... maybe take the resulting font and subset it per-browser if at
Expand Down
17 changes: 16 additions & 1 deletion src/nanoemoji/bitmap_tables.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@
Sequence,
Tuple,
)
import statistics
import sys


Expand Down Expand Up @@ -275,11 +274,27 @@ def _make_cbdt_strike(
return strike, data


def raise_if_too_big_for_cbdt(color_glyphs: Sequence[ColorGlyph]):
too_big = sorted(
(c for c in color_glyphs if max(c.bitmap.size) not in _UINT8_RANGE),
key=lambda c: c.bitmap_filename,
)
if not too_big:
return
raise ValueError(
"Bitmap is too big for CBDT, try lowering bitmap_resolution: "
+ ",".join(c.bitmap_filename for c in too_big)
)


def make_cbdt_table(
config: FontConfig,
ttfont: ttLib.TTFont,
color_glyphs: Sequence[ColorGlyph],
):
# CBDT is a wee bit limited in pixel size
raise_if_too_big_for_cbdt(color_glyphs)

# bitmap tables don't like it when we're out of order
color_glyphs = sorted(color_glyphs, key=lambda c: c.glyph_id)

Expand Down
20 changes: 17 additions & 3 deletions src/nanoemoji/colr_to_svg.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
from fontTools.ttLib.tables import otTables


_FOREGROUND_COLOR_INDEX = 0xFFFF
_GRADIENT_PAINT_FORMATS = (PaintLinearGradient.format, PaintRadialGradient.format)
_COLR_TO_SVG_TEMPLATE = r'<svg viewBox="TBD" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><defs/></svg>'

Expand Down Expand Up @@ -95,7 +96,12 @@ def _draw_svg_path(


def _color(ttfont: ttLib.TTFont, palette_index, alpha=1.0) -> colors.Color:
cpal_color = ttfont["CPAL"].palettes[0][palette_index]
palette = ttfont["CPAL"].palettes[0]
if palette_index == _FOREGROUND_COLOR_INDEX:
return colors.Color.fromstring("black") # as good a guess as any
if palette_index >= len(palette):
raise IndexError(f"{palette_index} illegal in palette of {len(palette)}")
cpal_color = palette[palette_index]
return colors.Color(
red=cpal_color.red,
green=cpal_color.green,
Expand Down Expand Up @@ -296,10 +302,13 @@ def glyph_region(ttfont: ttLib.TTFont, glyph_name: str) -> Rect:
"""The area occupied by the glyph, NOT factoring in that Y flips.
map_font_space_to_viewbox handles font +y goes up => svg +y goes down."""
width = ttfont["hmtx"][glyph_name][0]
if width == 0:
width = ttfont["glyf"][glyph_name].xMax
return Rect(
0,
-ttfont["OS/2"].sTypoAscender,
ttfont["hmtx"][glyph_name][0],
width,
ttfont["OS/2"].sTypoAscender - ttfont["OS/2"].sTypoDescender,
)

Expand All @@ -309,7 +318,12 @@ def _view_box_and_transform(
) -> Tuple[Rect, Affine2D]:

view_box = view_box_callback(glyph_name)
font_to_vbox = map_font_space_to_viewbox(view_box, glyph_region(ttfont, glyph_name))
assert view_box.w > 0, f"0-width viewBox for {glyph_name}?!"

region = glyph_region(ttfont, glyph_name)
assert region.w > 0, f"0-width region for {glyph_name}?!"

font_to_vbox = map_font_space_to_viewbox(view_box, region)

return (view_box, font_to_vbox)

Expand Down
32 changes: 32 additions & 0 deletions src/nanoemoji/copy.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Copyright 2022 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 absl import app
from pathlib import Path
import shutil


def main(argv):
assert len(argv) == 3, "Expected 2 args, input font and output"

input_file = Path(argv[1])
assert input_file.is_file(), f"No file {input_file}"
output_file = Path(argv[2])
assert input_file.resolve() != output_file.resolve()

shutil.copyfile(input_file, output_file)


if __name__ == "__main__":
app.run(main)
4 changes: 3 additions & 1 deletion src/nanoemoji/generate_svgs_from_colr.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,9 @@

def _view_box(font: ttLib.TTFont, glyph_name: str) -> Rect:
# we want a viewbox that results in no scaling when translating from font-space
return glyph_region(font, glyph_name)
region = glyph_region(font, glyph_name)
assert region.w > 0, f"0-width region for {glyph_name}"
return region


def main(argv):
Expand Down
16 changes: 10 additions & 6 deletions src/nanoemoji/glue_together.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,9 +118,12 @@ def _copy_cbdt(target: ttLib.TTFont, donor: ttLib.TTFont):
# other than glyph names so we can just construct a new
# order that matches that of target
donor_order = list(cbdt_glyph_info.keys())
only_in_donor = set(donor_order) - set(target.getGlyphOrder())
# confirm our core assumption that we successfully held glyph names stable
if not set(donor_order) <= set(target.getGlyphOrder()):
raise ValueError("Donor glyph names do not exist in target")
if only_in_donor:
raise ValueError(
f"Donor glyph names do not exist in target: {sorted(only_in_donor)}"
)
new_order = sorted(donor_order, key=target.getGlyphID)

# now we know the desired order, reshard into runs
Expand Down Expand Up @@ -154,7 +157,7 @@ def _copy_cbdt(target: ttLib.TTFont, donor: ttLib.TTFont):
strike.bitmapSizeTable.min_gid = min_gid
strike.bitmapSizeTable.max_gid = max_gid

max_width = max(cbdt_glyph_info[gn].data.metrics.width for gn in glyph_run)
max_width = max(cbdt_glyph_info[gn].data.metrics.Advance for gn in glyph_run)
strike.bitmapSizeTable.hori.widthMax = max_width
strike.bitmapSizeTable.vert.widthMax = max_width

Expand All @@ -179,11 +182,12 @@ def main(argv):
target = load_fully(Path(FLAGS.target_font))
donor = load_fully(Path(FLAGS.donor_font))

if FLAGS.color_table == "COLR":
donation = FLAGS.color_table.lower().strip()
if donation == "colr":
_copy_colr(target, donor)
elif FLAGS.color_table == "SVG":
elif donation == "svg":
_copy_svg(target, donor)
elif FLAGS.color_table == "CBDT":
elif donation == "cbdt":
_copy_cbdt(target, donor)
else:
raise ValueError(f"Unsupported color table '{FLAGS.color_table}'")
Expand Down
61 changes: 61 additions & 0 deletions src/nanoemoji/keep_glyph_names.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# Copyright 2022 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.

"""Update post to keep glyph names."""
from absl import app
from absl import flags
from absl import logging
from fontTools import ttLib
from pathlib import Path


FLAGS = flags.FLAGS


flags.DEFINE_string(
"log_level",
"INFO",
"The threshold for what messages will be logged. One of DEBUG, INFO, WARN, "
"ERROR, or FATAL.",
)


def keep_glyph_names(font: ttLib.TTFont):
# ref https://github.com/googlefonts/ufo2ft/blob/ad28eea062e0dd48678309bd9ef86dfcc85fa85a/Lib/ufo2ft/postProcessor.py#L281-L285
if "post" not in font:
raise ValueError(f"No post table")
post = font["post"]
post.formatType = 2.0
post.extraNames = []
post.mapping = {}


def main(argv):
logging.set_verbosity(FLAGS.log_level)

assert len(argv) == 3, "Expected 2 args, input font and output font"

input_file = Path(argv[1])
assert input_file.is_file(), f"No file {input_file}"
output_file = Path(argv[2])
assert input_file.resolve() != output_file.resolve()

font = ttLib.TTFont(input_file)
keep_glyph_names(font)

font.save(output_file)


if __name__ == "__main__":
app.run(main)
Loading

0 comments on commit 5533f7d

Please sign in to comment.