diff --git a/README.md b/README.md index 6d30e679..44d182eb 100644 --- a/README.md +++ b/README.md @@ -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: @@ -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 diff --git a/src/nanoemoji/bitmap_tables.py b/src/nanoemoji/bitmap_tables.py index 23d75a11..cf1e77ac 100644 --- a/src/nanoemoji/bitmap_tables.py +++ b/src/nanoemoji/bitmap_tables.py @@ -39,7 +39,6 @@ Sequence, Tuple, ) -import statistics import sys @@ -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) diff --git a/src/nanoemoji/colr_to_svg.py b/src/nanoemoji/colr_to_svg.py index a2025004..f2ad12fd 100644 --- a/src/nanoemoji/colr_to_svg.py +++ b/src/nanoemoji/colr_to_svg.py @@ -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>' @@ -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, @@ -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, ) @@ -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) diff --git a/src/nanoemoji/copy.py b/src/nanoemoji/copy.py new file mode 100644 index 00000000..f30cb113 --- /dev/null +++ b/src/nanoemoji/copy.py @@ -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) diff --git a/src/nanoemoji/generate_svgs_from_colr.py b/src/nanoemoji/generate_svgs_from_colr.py index 72e425c7..ce6178f2 100644 --- a/src/nanoemoji/generate_svgs_from_colr.py +++ b/src/nanoemoji/generate_svgs_from_colr.py @@ -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): diff --git a/src/nanoemoji/glue_together.py b/src/nanoemoji/glue_together.py index 1f71f9b0..8c214c84 100644 --- a/src/nanoemoji/glue_together.py +++ b/src/nanoemoji/glue_together.py @@ -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 @@ -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 @@ -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}'") diff --git a/src/nanoemoji/keep_glyph_names.py b/src/nanoemoji/keep_glyph_names.py new file mode 100644 index 00000000..322105a1 --- /dev/null +++ b/src/nanoemoji/keep_glyph_names.py @@ -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) diff --git a/src/nanoemoji/maximum_color.py b/src/nanoemoji/maximum_color.py index 2342c6cb..8f1cc89a 100644 --- a/src/nanoemoji/maximum_color.py +++ b/src/nanoemoji/maximum_color.py @@ -29,6 +29,7 @@ from absl import flags from absl import logging from fontTools import ttLib +from fontTools.ttLib.ttFont import newTable from nanoemoji import config from nanoemoji.colr_to_svg import colr_glyphs from nanoemoji.extract_svgs import svg_glyphs @@ -42,18 +43,9 @@ ) from nanoemoji.util import only from pathlib import Path -from typing import List, Tuple +from typing import List, NamedTuple, Tuple -_SVG2COLR_GLYPHMAP = "svg2colr.glyphmap" -_SVG2COLR_CONFIG = "svg2colr.toml" - -_COLR2SVG_GLYPHMAP = "colr2svg.glyphmap" -_COLR2SVG_CONFIG = "colr2svg.toml" - -_CBDT_GLYPHMAP = "cbdt.glyphmap" -_CBDT_CONFIG = "cbdt.toml" - FLAGS = flags.FLAGS @@ -62,6 +54,41 @@ True, "If true feel free to obliterate any existing glyf/cff content, e.g. fallback glyphs", ) +flags.DEFINE_bool( + "bitmaps", + False, + "If true, generate a bitmap table (specificaly CBDT)", +) + + +# attribute names need to match inputs to write_font rule +class WriteFontInputs(NamedTuple): + glyphmap_file: Path + config_file: Path + + @property + def table_tag(self) -> str: + return f"{Path(self.glyphmap_file).stem:4}" + + @property + def color_format(self) -> str: + identifier = self.table_tag.strip().lower() + + if identifier == "svg": + # for good woff2 performance, at cost of inflated size + return "picosvg" + elif identifier == "colr": + # optimize for woff2 performance + return "glyf_colr_1" + elif identifier == "cbdt": + return "cbdt" + else: + raise ValueError(f"What is {identifier}?!") + + @classmethod + def for_tag(cls, table_tag: str) -> "WriteFontInputs": + basename = table_tag.strip() + return cls(Path(basename + ".glyphmap"), Path(basename + ".toml")) def _vector_color_table(font: ttLib.TTFont) -> str: @@ -102,7 +129,7 @@ def bitmap_dest(input_svg: Path) -> Path: return bitmap_dir() / input_svg.with_suffix(".png").name -def _write_preamble(nw: NinjaWriter, input_font: Path): +def _write_preamble(nw: NinjaWriter): module_rule( nw, "extract_svgs_from_otsvg", @@ -129,48 +156,14 @@ def _write_preamble(nw: NinjaWriter, input_font: Path): module_rule( nw, "write_config_for_mergeable", - "--color_format glyf_colr_1 $in $out", - rule_name="write_glyf_colr_1_config", - ) - nw.newline() - - module_rule( - nw, - "write_config_for_mergeable", - "--color_format picosvg $in $out", - rule_name="write_picosvg_config", - ) - nw.newline() - - module_rule( - nw, - "write_config_for_mergeable", - "--color_format cbdt $in $out", - rule_name="write_cbdt_config", - ) - nw.newline() - - module_rule( - nw, - "write_font", - f"--glyphmap_file {_SVG2COLR_GLYPHMAP} --config_file {_SVG2COLR_CONFIG} --output_file $out", - rule_name="write_colr_font_from_svg_dump", - ) - nw.newline() - - module_rule( - nw, - "write_font", - f"--glyphmap_file {_COLR2SVG_GLYPHMAP} --config_file {_COLR2SVG_CONFIG} --output_file $out", - rule_name="write_svg_font_from_generated_svgs", + "--color_format $color_format $in $out", ) nw.newline() module_rule( nw, "write_font", - f"--glyphmap_file {_CBDT_GLYPHMAP} --config_file {_CBDT_CONFIG} --output_file $out", - rule_name="write_cbdt_font", + f"--glyphmap_file $glyphmap_file --config_file $config_file --output_file $out", ) nw.newline() @@ -191,118 +184,137 @@ def _write_preamble(nw: NinjaWriter, input_font: Path): module_rule( nw, "glue_together", - f"--color_table COLR --target_font {input_font} --donor_font $in --output_file $out", - rule_name="copy_colr_from_svg2colr", + f"--color_table $color_table --target_font $target_font --donor_font $in --output_file $out", ) nw.newline() module_rule( nw, - "glue_together", - f"--color_table SVG --target_font {input_font} --donor_font $in --output_file $out", - rule_name="copy_svg_from_colr2svg", + "keep_glyph_names", + f"$in $out", ) nw.newline() - # Glue CBDT onto the font that has both vector tables module_rule( nw, - "glue_together", - f"--color_table CBDT --target_font {input_font.stem}.both_vector_tables.ttf --donor_font $in --output_file $out", - rule_name="copy_cbdt", + "strip_glyph_names", + f"$in $out", ) nw.newline() + module_rule( + nw, + "copy", + f"$in $out", + ) + nw.newline() -def _generate_svg_from_colr( - nw: NinjaWriter, input_font: Path, font: ttLib.TTFont -) -> Tuple[Path, List[Path]]: - # generate svgs - svg_files = [ - rel_build(svg_generate_dir() / f"{gid:05d}.svg") for gid in colr_glyphs(font) - ] - nw.build(svg_files, "generate_svgs_from_colr", input_font) + +def _write_font(nw: NinjaWriter, output_file: Path, inputs: WriteFontInputs): + nw.build( + output_file, "write_font", implicit=list(inputs), variables=inputs._asdict() + ) nw.newline() - # picosvg them + +def _write_config_for_mergeable( + nw: NinjaWriter, config_file: Path, input_font: Path, color_format: str +): + nw.build( + config_file, + "write_config_for_mergeable", + input_font, + variables={"color_format": color_format}, + ) + nw.newline() + + +def _picosvgs(nw: NinjaWriter, svg_files: List[Path]) -> List[Path]: picosvgs = [rel_build(picosvg_dest(s)) for s in svg_files] for svg_file, picosvg in zip(svg_files, picosvgs): nw.build(picosvg, "picosvg", svg_file) nw.newline() + return picosvgs + + +def _generate_additional_color_table( + nw: NinjaWriter, + input_font: Path, + glyphmap_inputs: List[Path], + table_tag: str, + glue_target: Path, +) -> Path: + write_font_inputs = WriteFontInputs.for_tag(table_tag) + identifier = write_font_inputs.color_format + del table_tag # make a glyphmap nw.build( - _COLR2SVG_GLYPHMAP, "write_glyphmap_for_glyph_svgs", picosvgs + [input_font] + write_font_inputs.glyphmap_file, + "write_glyphmap_for_glyph_svgs", + glyphmap_inputs, ) nw.newline() - # make a config - nw.build(_COLR2SVG_CONFIG, "write_picosvg_config", input_font) - nw.newline() + # picosvg because we want good woff2 outcomes + _write_config_for_mergeable( + nw, write_font_inputs.config_file, input_font, write_font_inputs.color_format + ) # generate a new font with SVG glyphs that use the same names as the original - nw.build( - "svg_from_colr.ttf", - "write_svg_font_from_generated_svgs", - [_COLR2SVG_GLYPHMAP, _COLR2SVG_CONFIG], - ) - nw.newline() + font_with_new_table = Path("MergeSource." + identifier + ".ttf") + _write_font(nw, font_with_new_table, write_font_inputs) - # stick our shiny new COLR table onto the input font - output_file = Path(input_font.stem + ".both_vector_tables.ttf") + # stick our shiny new table onto the input font + output_file = Path(input_font.stem + f".added_{identifier}.ttf") nw.build( output_file, - "copy_svg_from_colr2svg", - "svg_from_colr.ttf", + "glue_together", + font_with_new_table, + implicit=list({input_font, glue_target}), + variables={ + "color_table": write_font_inputs.table_tag.strip(), + "target_font": glue_target, + }, ) nw.newline() - return output_file, picosvgs + return output_file -def _generate_colr_from_svg( +def _generate_svg_from_colr( nw: NinjaWriter, input_font: Path, font: ttLib.TTFont ) -> Tuple[Path, List[Path]]: - # extract the svgs - svg_extracts = [ - rel_build(svg_extract_dir() / f"{gid:05d}.svg") for gid, _ in svg_glyphs(font) + # generate svgs + svg_files = [ + rel_build(svg_generate_dir() / f"{gid:05d}.svg") for gid in colr_glyphs(font) ] - nw.build(svg_extracts, "extract_svgs_from_otsvg", input_font) - nw.newline() - - # picosvg them - picosvgs = [rel_build(picosvg_dest(s)) for s in svg_extracts] - for svg_extract, picosvg in zip(svg_extracts, picosvgs): - nw.build(picosvg, "picosvg", svg_extract) + nw.build(svg_files, "generate_svgs_from_colr", input_font) nw.newline() - # make a glyphmap - nw.build( - _SVG2COLR_GLYPHMAP, "write_glyphmap_for_glyph_svgs", picosvgs + [input_font] + # create and merge an SVG table + picosvgs = _picosvgs(nw, svg_files) + output_file = _generate_additional_color_table( + nw, input_font, picosvgs + [input_font], "SVG ", input_font ) - nw.newline() + return output_file, picosvgs - # make a config - nw.build(_SVG2COLR_CONFIG, "write_glyf_colr_1_config", input_font) - nw.newline() - # generate a new font with COLR glyphs that use the same names as the original - nw.build( - "colr_from_svg.ttf", - "write_colr_font_from_svg_dump", - [_SVG2COLR_GLYPHMAP, _SVG2COLR_CONFIG], - ) +def _generate_colr_from_svg( + nw: NinjaWriter, input_font: Path, font: ttLib.TTFont +) -> Tuple[Path, List[Path]]: + # extract the svgs + svg_files = [ + rel_build(svg_extract_dir() / f"{gid:05d}.svg") for gid, _ in svg_glyphs(font) + ] + nw.build(svg_files, "extract_svgs_from_otsvg", input_font) nw.newline() - # stick our shiny new COLR table onto the input font - output_file = Path(input_font.stem + ".both_vector_tables.ttf") - nw.build( - output_file, - "copy_colr_from_svg2colr", - "colr_from_svg.ttf", + # create and merge a COLR table + picosvgs = _picosvgs(nw, svg_files) + output_file = _generate_additional_color_table( + nw, input_font, picosvgs + [input_font], "COLR", input_font ) - nw.newline() - return output_file, picosvgs @@ -319,34 +331,32 @@ def _generate_cbdt( nw.build(bitmap, "write_bitmap", picosvg) nw.newline() - # make a glyphmap - nw.build( - _CBDT_GLYPHMAP, - "write_glyphmap_for_glyph_svgs", - picosvg_files + bitmap_files + [input_font], + # create and merge a COLR table + output_file = _generate_additional_color_table( + nw, input_font, picosvg_files + bitmap_files + [input_font], "CBDT", color_font ) - nw.newline() + return output_file - # make a config - nw.build(_CBDT_CONFIG, "write_cbdt_config", input_font) - nw.newline() - # generate a new font with CBDT glyphs that use the same names as the original - # hidden in the rule definition is that the both vector table font is an input - # so list it as implicit +def _keep_glyph_names(nw: NinjaWriter, input_file: Path) -> ttLib.TTFont: + # The whole concept is we keep glyph name stable until the end so + # make sure we start with stable names. Doesn't matter what they are, + # just that they don't change. + output_file = Path(input_file.stem + ".keep_glyph_names.ttf") nw.build( - "cbdt.ttf", - "write_cbdt_font", - [_CBDT_GLYPHMAP, _CBDT_CONFIG], - implicit=[input_font.stem + ".both_vector_tables.ttf"], + output_file, + "keep_glyph_names", + input_file, ) nw.newline() + return output_file + - # stick our shiny new CBDT table onto the input font and declare victory +def _strip_glyph_names(nw: NinjaWriter, input_file: Path, output_file: Path): nw.build( - input_font.name, - "copy_cbdt", - "cbdt.ttf", + output_file, + "strip_glyph_names", + input_file, ) nw.newline() @@ -358,33 +368,40 @@ def _run(argv): if not FLAGS.destroy_non_color_glyphs: raise NotImplementedError("Retention of non-color glyphs not implemented yet") - input_font = Path(argv[1]) - assert input_font.is_file() - font = ttLib.TTFont(input_font) + input_file = Path(argv[1]).resolve() # we need a non-relative path + assert input_file.is_file() + font = ttLib.TTFont(input_file) + final_output = Path(config.load().output_file) + assert ( + input_file.resolve() != (build_dir() / final_output).resolve() + ), "In == Out is bad" build_file = build_dir() / "build.ninja" build_dir().mkdir(parents=True, exist_ok=True) - # TODO flag control color_table = _vector_color_table(font) if gen_ninja(): logging.info(f"Generating {build_file.relative_to(build_dir())}") - input_font = input_font.resolve() # we need a non-relative path with open(build_file, "w") as f: nw = NinjaWriter(f) - _write_preamble(nw, input_font) + _write_preamble(nw) + wip_file = _keep_glyph_names(nw, input_file) + + # generate the missing vector table if color_table == "COLR": - color_font, picosvg_files = _generate_svg_from_colr( - nw, input_font, font - ) + wip_file, picosvg_files = _generate_svg_from_colr(nw, wip_file, font) else: - color_font, picosvg_files = _generate_colr_from_svg( - nw, input_font, font - ) + wip_file, picosvg_files = _generate_colr_from_svg(nw, wip_file, font) + + if FLAGS.bitmaps: + wip_file = _generate_cbdt(nw, input_file, font, wip_file, picosvg_files) - _generate_cbdt(nw, input_font, font, color_font, picosvg_files) + if config.load().keep_glyph_names: + nw.build(final_output, "copy", wip_file) + else: + _strip_glyph_names(nw, wip_file, final_output) maybe_run_ninja(build_file) diff --git a/src/nanoemoji/ninja.py b/src/nanoemoji/ninja.py index 43b24e58..2bd11d01 100644 --- a/src/nanoemoji/ninja.py +++ b/src/nanoemoji/ninja.py @@ -35,28 +35,36 @@ flags.DEFINE_bool("exec_ninja", True, "Whether to run ninja.") +def _str_path(arg): + if isinstance(arg, Path): + return str(arg) + if isinstance(arg, MutableSequence): + return _str_paths(arg) + return arg + + +def _str_paths(args): + # Path in particular kerplodes nw.build + if isinstance(args, dict): + return {_str_path(k): _str_path(v) for k, v in args.items()} + elif isinstance(args, list) or isinstance(args, tuple): + return [_str_path(v) for v in args] + else: + raise ValueError(f"What to do with {type(args)}") + + class NinjaWriter: def __init__(self, output_f): self._nw = ninja_syntax.Writer(output_f) self.comment("Generated by nanoemoji") self.newline() - def _str_paths(self, args): - # Path in particular kerplodes nw.build - args = list(args) - for i, arg in enumerate(args): - if isinstance(arg, Path): - args[i] = str(arg) - if isinstance(arg, MutableSequence): - args[i] = self._str_paths(arg) - return args - def rule(self, *args, **kwargs): self._nw.rule(*args, **kwargs) def build(self, *args, **kwargs): self._nw.build( - *self._str_paths(args), **{k: self._str_paths(v) for k, v in kwargs.items()} + *_str_paths(args), **{k: _str_paths(v) for k, v in kwargs.items()} ) def newline(self): diff --git a/src/nanoemoji/strip_glyph_names.py b/src/nanoemoji/strip_glyph_names.py new file mode 100644 index 00000000..0a9a2e27 --- /dev/null +++ b/src/nanoemoji/strip_glyph_names.py @@ -0,0 +1,57 @@ +# 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 drop 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 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) + + post = font["post"] + post.formatType = 3.0 + for attr in ("extraNames", "mapping"): + if hasattr(post, attr): + delattr(post, attr) + post.glyphOrder = None + + font.save(output_file) + + +if __name__ == "__main__": + app.run(main) diff --git a/tests/bitmap_tables_test.py b/tests/bitmap_tables_test.py index acfc6977..732e658f 100644 --- a/tests/bitmap_tables_test.py +++ b/tests/bitmap_tables_test.py @@ -13,6 +13,7 @@ # limitations under the License. from io import BytesIO +from nanoemoji.color_glyph import ColorGlyph from nanoemoji import config from nanoemoji.bitmap_tables import _cbdt_bitmap_data, BitmapMetrics from nanoemoji.png import PNG diff --git a/tests/maximum_color_test.py b/tests/maximum_color_test.py index 018e1628..51e42560 100644 --- a/tests/maximum_color_test.py +++ b/tests/maximum_color_test.py @@ -14,10 +14,14 @@ # Integration tests for nanoemoji.maximum_color +import copy from fontTools import ttLib +from nanoemoji.keep_glyph_names import keep_glyph_names +from pathlib import Path import pytest import sys from test_helper import cleanup_temp_dirs, locate_test_file, run, run_nanoemoji +from typing import Tuple @pytest.fixture(scope="module", autouse=True) @@ -30,14 +34,7 @@ def _cleanup_temporary_dirs(): cleanup_temp_dirs() -@pytest.mark.parametrize( - "color_format, expected_new_tables", - [ - ("picosvg", {"COLR", "CPAL", "CBDT", "CBLC"}), - ("glyf_colr_1", {"SVG ", "CBDT", "CBLC"}), - ], -) -def test_build_maximum_font(color_format, expected_new_tables): +def _build_initial_font(color_format: str) -> Path: tmp_dir = run_nanoemoji( ( "--color_format", @@ -49,21 +46,76 @@ def test_build_maximum_font(color_format, expected_new_tables): initial_font_file = tmp_dir / "Font.ttf" assert initial_font_file.is_file() + return initial_font_file + + +def _maximize_color(initial_font_file: Path, additional_flags: Tuple[str, ...]) -> Path: # Moar color + out_dir = initial_font_file.parent / "maximum_color" run( ( sys.executable, "-m", "nanoemoji.maximum_color", "--build_dir", - tmp_dir / "maximum_color", - initial_font_file, + out_dir, ) + + additional_flags + + (initial_font_file,) ) - maxmium_font_file = tmp_dir / "maximum_color" / "Font.ttf" + maxmium_font_file = out_dir / "Font.ttf" assert maxmium_font_file.is_file() + return maxmium_font_file + + +@pytest.mark.parametrize( + "color_format, expected_new_tables", + [ + ("picosvg", {"COLR", "CPAL"}), + ("glyf_colr_1", {"SVG "}), + ], +) +@pytest.mark.parametrize("bitmaps", [True, False]) +def test_build_maximum_font(color_format, expected_new_tables, bitmaps): + initial_font_file = _build_initial_font(color_format) + + bitmap_flag = "--nobitmaps" + if bitmaps: + bitmap_flag = "--bitmaps" + expected_new_tables = copy.copy(expected_new_tables) + expected_new_tables.update({"CBDT", "CBLC"}) + + maxmium_font_file = _maximize_color(initial_font_file, (bitmap_flag,)) + initial_font = ttLib.TTFont(initial_font_file) maximum_font = ttLib.TTFont(maxmium_font_file) - assert set(maximum_font.keys()) == set(initial_font.keys()) | expected_new_tables + assert set(maximum_font.keys()) - set(initial_font.keys()) == expected_new_tables + + +@pytest.mark.parametrize("keep_names", [True, False]) +def test_keep_glyph_names(keep_names): + initial_font_file = _build_initial_font("glyf_colr_1") + + # set identifiable glyph names + font = ttLib.TTFont(initial_font_file) + keep_glyph_names(font) + font.setGlyphOrder(["duck_" + gn for gn in font.getGlyphOrder()]) + font.save(initial_font_file) + + keep_glyph_names_flag = "--keep_glyph_names" + if not keep_names: + keep_glyph_names_flag = "--nokeep_glyph_names" + + maxmium_font_file = _maximize_color(initial_font_file, (keep_glyph_names_flag,)) + maximum_font = ttLib.TTFont(maxmium_font_file) + + if keep_names: + assert all( + gn.startswith("duck_") for gn in maximum_font.getGlyphOrder() + ), maximum_font.getGlyphOrder() + else: + assert all( + not gn.startswith("duck_") for gn in maximum_font.getGlyphOrder() + ), maximum_font.getGlyphOrder()