diff --git a/README.md b/README.md index 9fd2961a..928c506b 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,27 @@ Requires Python 3.7 or greater. | [sbix](https://docs.microsoft.com/en-us/typography/opentype/spec/sbix) | Yes | Only for Mac Safari due to https://github.com/harfbuzz/harfbuzz/issues/2679#issuecomment-1021419864. Only square bitmaps. Uses [`resvg`](https://github.com/RazrFalcon/resvg).| | [CBDT](https://docs.microsoft.com/en-us/typography/opentype/spec/cbdt) | Yes | Only square bitmaps. Uses [`resvg`](https://github.com/RazrFalcon/resvg).| +### Adding color tables to existing fonts + +:warning: _under active development, doubtless full of bugs_ + +Given at least one vector color table (COLR or SVG) the other vector color table and bitmap table(s) +can be generated: + +```shell +# Adds COLR to a font with SVG and vice versa +maxmium_color my_colr_font.ttf +``` + +The intended result is a font that will Just Work in any modern browser: + +| Color table | Target browser | Notes | +| --- | --- | --- | +| COLR | Chrome 98+ | https://developer.chrome.com/blog/colrv1-fonts/ | +| SVG | Firefox, Safari | | +| CBDT | Chrome <98 | | + + ## Releasing See https://googlefonts.github.io/python#make-a-release. diff --git a/src/nanoemoji/bitmap_tables.py b/src/nanoemoji/bitmap_tables.py index f7f3bab0..56349c79 100644 --- a/src/nanoemoji/bitmap_tables.py +++ b/src/nanoemoji/bitmap_tables.py @@ -25,14 +25,18 @@ ) from fontTools.ttLib.tables.C_B_D_T_ import cbdt_bitmap_format_17 as CbdtBitmapFormat17 from functools import reduce +from io import BytesIO from nanoemoji.config import FontConfig from nanoemoji.color_glyph import ColorGlyph +from nanoemoji.png import PNG +from nanoemoji.util import only from typing import ( List, NamedTuple, Sequence, Tuple, ) +import statistics import sys @@ -40,7 +44,7 @@ _UINT8_RANGE = range(0, 255 + 1) # https://docs.microsoft.com/en-us/typography/opentype/spec/cbdt#table-structure -_CBDT_HEADER_SIZE = 4 +CBDT_HEADER_SIZE = 4 # https://docs.microsoft.com/en-us/typography/opentype/spec/cbdt#format-17-small-metrics-png-image-data _CBDT_SMALL_METRIC_PNGS = 17 @@ -49,6 +53,16 @@ _CBDT_SMALL_METRIC_PNG_HEADER_SIZE = 5 + 4 +def _nudge_into_range(arange: range, value: int, max_move: int = 1) -> int: + if value in arange: + return value + if value > max(arange) and value - max_move <= max(arange): + return max(arange) + if value < min(arange) and value + max_move >= min(arange): + return min(arange) + return value + + class BitmapMetrics(NamedTuple): x_offset: int y_offset: int @@ -56,7 +70,7 @@ class BitmapMetrics(NamedTuple): line_ascent: int @classmethod - def create(cls, config: FontConfig, ppem: int) -> "BitmapMetrics": + def create(cls, config: FontConfig, image_data: PNG, ppem: int) -> "BitmapMetrics": # https://github.com/googlefonts/noto-emoji/blob/9a5261d871451f9b5183c93483cbd68ed916b1e9/third_party/color_emoji/emoji_builder.py#L109 ascent = config.ascender descent = -config.descender @@ -64,39 +78,60 @@ def create(cls, config: FontConfig, ppem: int) -> "BitmapMetrics": line_height = round((ascent + descent) * ppem / float(config.upem)) line_ascent = ascent * ppem / float(config.upem) + # center within advance metrics = BitmapMetrics( - x_offset=max( - round((_width_in_pixels(config) - config.bitmap_resolution) / 2), 0 + x_offset=_nudge_into_range( + _INT8_RANGE, + max( + round( + ( + _width_in_pixels(config, image_data) + - config.bitmap_resolution + ) + / 2 + ), + 0, + ), ), - y_offset=round( - line_ascent - 0.5 * (line_height - config.bitmap_resolution) + y_offset=_nudge_into_range( + _INT8_RANGE, + round(line_ascent - 0.5 * (line_height - config.bitmap_resolution)), ), line_height=line_height, line_ascent=round(line_ascent), ) + # The FontTools errors when values are out of bounds are a bit nasty + # so check here for earlier and more helpful termination + assert ( + config.bitmap_resolution in _UINT8_RANGE + ), f"bitmap_resolution out of bounds: {config.bitmap_resolution}" + assert metrics.y_offset in _INT8_RANGE, f"y_offset out of bounds: {metrics}" + return metrics -def _width_in_pixels(config: FontConfig) -> int: - return round( - config.bitmap_resolution * config.width / (config.ascender - config.descender) - ) +def _pixels_to_funits(config: FontConfig, bitmap_pixel_height: int) -> Tuple[int, int]: + # the bitmap is vertically scaled to fill the space desc to asc + # this gives us a ratio between pixels and upem + funits = config.ascender - config.descender + return (bitmap_pixel_height, funits) -# https://github.com/googlefonts/noto-emoji/blob/9a5261d871451f9b5183c93483cbd68ed916b1e9/third_party/color_emoji/emoji_builder.py#L53 -def _ppem(config: FontConfig, advance: int) -> int: - return round(_width_in_pixels(config) * config.upem / advance) +def _width_in_pixels(config: FontConfig, image_data: PNG) -> int: + pixels, funits = _pixels_to_funits(config, image_data.size[1]) + width_funits = image_data.size[0] * funits / pixels + width_funits = max(config.width, width_funits) -def _advance(ttfont: ttLib.TTFont, color_glyphs: Sequence[ColorGlyph]) -> int: - # let's go ahead and fail miserably if advances are not all the same - # proportional bitmaps can wait for a second pass :) - advances = { - ttfont["hmtx"].metrics[ttfont.getGlyphName(c.glyph_id)][0] for c in color_glyphs - } - assert len(advances) == 1, "Proportional bitmaps not supported yet" - return next(iter(advances)) + assert width_funits > 0 + return round(width_funits * pixels / funits) + + +# https://github.com/googlefonts/noto-emoji/blob/9a5261d871451f9b5183c93483cbd68ed916b1e9/third_party/color_emoji/emoji_builder.py#L53 +def _ppem(config: FontConfig, bitmap_pixel_height: int) -> int: + pixels, funits = _pixels_to_funits(config, bitmap_pixel_height) + return round(config.upem * pixels / funits) def _cbdt_record_size(image_format: int, image_data: bytes) -> int: @@ -105,11 +140,11 @@ def _cbdt_record_size(image_format: int, image_data: bytes) -> int: def _cbdt_bitmapdata_offsets( - image_format: int, color_glyphs: Sequence[ColorGlyph] + initial_offset: int, image_format: int, color_glyphs: Sequence[ColorGlyph] ) -> List[Tuple[int, int]]: # TODO is this right? - feels dumb. But ... compile crashes if locations are unknown. offsets = [] - offset = _CBDT_HEADER_SIZE + offset = initial_offset for color_glyph in color_glyphs: offsets.append(offset) offset += _cbdt_record_size(image_format, color_glyph.bitmap) @@ -118,17 +153,15 @@ def _cbdt_bitmapdata_offsets( def _cbdt_bitmap_data( - config: FontConfig, metrics: BitmapMetrics, image_data: bytes + config: FontConfig, metrics: BitmapMetrics, image_data: PNG ) -> CbdtBitmapFormat17: bitmap_data = CbdtBitmapFormat17(b"", None) bitmap_data.metrics = SmallGlyphMetrics() - bitmap_data.metrics.height = config.bitmap_resolution - bitmap_data.metrics.width = config.bitmap_resolution - # center within advance + bitmap_data.metrics.width, bitmap_data.metrics.height = image_data.size bitmap_data.metrics.BearingX = metrics.x_offset bitmap_data.metrics.BearingY = metrics.y_offset - bitmap_data.metrics.Advance = _width_in_pixels(config) + bitmap_data.metrics.Advance = bitmap_data.metrics.width bitmap_data.imageData = image_data return bitmap_data @@ -142,18 +175,18 @@ def make_sbix_table( sbix = ttLib.newTable("sbix") ttfont[sbix.tableTag] = sbix - ppem = _ppem(config, _advance(ttfont, color_glyphs)) + bitmap_pixel_height = only({c.bitmap.size[1] for c in color_glyphs}) + ppem = _ppem(config, bitmap_pixel_height) strike = SbixStrike() strike.ppem = ppem strike.resolution = 72 # pixels per inch sbix.strikes[strike.ppem] = strike - metrics = BitmapMetrics.create(config, strike.ppem) - for color_glyph in color_glyphs: # TODO: if we've seen these bytes before set graphicType "dupe", referenceGlyphName image_data = color_glyph.bitmap + metrics = BitmapMetrics.create(config, image_data, strike.ppem) glyph_name = ttfont.getGlyphName(color_glyph.glyph_id) glyph = SbixGlyph( @@ -166,31 +199,19 @@ def make_sbix_table( strike.glyphs[glyph_name] = glyph -# Ref https://github.com/googlefonts/noto-emoji/blob/main/third_party/color_emoji/emoji_builder.py -def make_cbdt_table( +def _make_cbdt_strike( config: FontConfig, ttfont: ttLib.TTFont, + data_offset: int, color_glyphs: Sequence[ColorGlyph], ): - - # bitmap tables don't like it when we're out of order - color_glyphs = sorted(color_glyphs, key=lambda c: c.glyph_id) - min_gid, max_gid = color_glyphs[0].glyph_id, color_glyphs[-1].glyph_id assert max_gid - min_gid + 1 == len( color_glyphs ), "Below assumes color gyphs gids are consecutive" - advance = _advance(ttfont, color_glyphs) - ppem = _ppem(config, advance) - - cbdt = ttLib.newTable("CBDT") - ttfont[cbdt.tableTag] = cbdt - - cblc = ttLib.newTable("CBLC") - ttfont[cblc.tableTag] = cblc - - cblc.version = cbdt.version = 3.0 + bitmap_pixel_height = only({c.bitmap.size[1] for c in color_glyphs}) + ppem = _ppem(config, bitmap_pixel_height) strike = CblcStrike() strike.bitmapSizeTable.startGlyphIndex = min_gid @@ -214,24 +235,26 @@ def make_cbdt_table( line_metrics.pad1 = 0 line_metrics.pad2 = 0 - metrics = BitmapMetrics.create(config, ppem) - # The FontTools errors when values are out of bounds are a bit nasty - # so check here for earlier and more helpful termination - assert ( - config.bitmap_resolution in _UINT8_RANGE - ), f"bitmap_resolution out of bounds: {config.bitmap_resolution}" - assert metrics.y_offset in _INT8_RANGE, f"y_offset out of bounds: {metrics}" + metrics = { + c.glyph_id: BitmapMetrics.create(config, c.bitmap, ppem) for c in color_glyphs + } + data = { + ttfont.getGlyphName(c.glyph_id): _cbdt_bitmap_data( + config, metrics[c.glyph_id], c.bitmap + ) + for c in color_glyphs + } + + line_height = only({m.line_height for m in metrics.values()}) line_metrics.ascender = round(config.ascender * ppem / config.upem) - line_metrics.descender = -(metrics.line_height - line_metrics.ascender) - line_metrics.widthMax = _width_in_pixels(config) + line_metrics.descender = -(line_height - line_metrics.ascender) + line_metrics.widthMax = max(d.metrics.Advance for d in data.values()) strike.bitmapSizeTable.hori = line_metrics strike.bitmapSizeTable.vert = line_metrics - # Simplifying assumption: identical metrics # https://docs.microsoft.com/en-us/typography/opentype/spec/eblc#indexsubtables - # Apparently you cannot build a CBLC index subtable w/o providing bytes & font?! # If we build from empty bytes and fill in the fields all is well index_subtable = CblcIndexSubTable1(b"", ttfont) @@ -241,19 +264,53 @@ def make_cbdt_table( index_subtable.imageFormat = _CBDT_SMALL_METRIC_PNGS index_subtable.imageSize = config.bitmap_resolution index_subtable.names = [ttfont.getGlyphName(c.glyph_id) for c in color_glyphs] + index_subtable.locations = _cbdt_bitmapdata_offsets( - index_subtable.imageFormat, color_glyphs + data_offset, index_subtable.imageFormat, color_glyphs ) strike.indexSubTables = [index_subtable] - cblc.strikes = [strike] - - # Now register all the data - cbdt.strikeData = [ - { - ttfont.getGlyphName(c.glyph_id): _cbdt_bitmap_data( - config, metrics, c.bitmap - ) - for c in color_glyphs - } - ] + + return strike, data + + +# Ref https://github.com/googlefonts/noto-emoji/blob/main/third_party/color_emoji/emoji_builder.py +def make_cbdt_table( + config: FontConfig, + ttfont: ttLib.TTFont, + color_glyphs: Sequence[ColorGlyph], +): + # bitmap tables don't like it when we're out of order + color_glyphs = sorted(color_glyphs, key=lambda c: c.glyph_id) + + cbdt = ttLib.newTable("CBDT") + ttfont[cbdt.tableTag] = cbdt + + cblc = ttLib.newTable("CBLC") + ttfont[cblc.tableTag] = cblc + + cblc.version = cbdt.version = 3.0 + + cblc.strikes = [] + cbdt.strikeData = [] + + data_offset = CBDT_HEADER_SIZE + + while color_glyphs: + # grab the next run w/consecutive gids + min_gid = color_glyphs[0].glyph_id + end = 1 + while ( + len(color_glyphs) > end + and color_glyphs[end].glyph_id == color_glyphs[end - 1].glyph_id + 1 + ): + end += 1 + color_glyph_run = color_glyphs[:end] + color_glyphs = color_glyphs[end:] + + strike, data = _make_cbdt_strike(config, ttfont, data_offset, color_glyph_run) + for sub_table in strike.indexSubTables: + data_offset = max(sub_table.locations[-1][-1], data_offset) + + cblc.strikes.append(strike) + cbdt.strikeData.append(data) diff --git a/src/nanoemoji/color_glyph.py b/src/nanoemoji/color_glyph.py index 24366e49..92e87038 100644 --- a/src/nanoemoji/color_glyph.py +++ b/src/nanoemoji/color_glyph.py @@ -350,7 +350,7 @@ def _painted_layers( return tuple(layers) -def _color_glyph_advance_width(view_box: Rect, config: FontConfig) -> int: +def _advance_width(view_box: Rect, config: FontConfig) -> int: # Scale advance width proportionally to viewbox aspect ratio. # Use the default advance width if it's larger than the proportional one. font_height = config.ascender - config.descender # descender <= 0 @@ -429,10 +429,15 @@ def create( view_box = None if svg: view_box = svg.view_box() + elif bitmap: + view_box = Rect(0, 0, *bitmap.size) if view_box is not None: - base_glyph.width = _color_glyph_advance_width(view_box, font_config) + base_glyph.width = _advance_width(view_box, font_config) else: base_glyph.width = font_config.width + assert ( + base_glyph.width > 0 + ), f"0 width for {ufo_glyph_name} (svg {svg_filename}, bitmap {bitmap_filename})" # Setup direct access to the glyph if possible if len(codepoints) == 1: diff --git a/src/nanoemoji/config.py b/src/nanoemoji/config.py index b0654507..0d18c0d5 100644 --- a/src/nanoemoji/config.py +++ b/src/nanoemoji/config.py @@ -37,8 +37,6 @@ "glyf", "glyf_colr_0", "glyf_colr_1", - "glyf_colr_1_and_picosvg", - "glyf_colr_1_and_picosvg_and_cbdt", "cff_colr_0", "cff_colr_1", "cff2_colr_0", diff --git a/src/nanoemoji/glue_together.py b/src/nanoemoji/glue_together.py index e9e90979..2aaaf76e 100644 --- a/src/nanoemoji/glue_together.py +++ b/src/nanoemoji/glue_together.py @@ -18,14 +18,17 @@ from absl import app from absl import flags from absl import logging +import copy from fontTools.ttLib.tables import otTables as ot +from fontTools.ttLib.tables.C_B_D_T_ import cbdt_bitmap_format_17 as CbdtBitmapFormat17 from fontTools import ttLib +from nanoemoji import bitmap_tables from nanoemoji.colr import paints_of_type from nanoemoji.reorder_glyphs import reorder_glyphs from nanoemoji.util import load_fully import os from pathlib import Path -from typing import Iterable, Tuple +from typing import Iterable, List, Mapping, NamedTuple, Tuple FLAGS = flags.FLAGS @@ -34,7 +37,15 @@ flags.DEFINE_string("target_font", None, "Font assets are copied into.") flags.DEFINE_string("donor_font", None, "Font from which assets are copied.") flags.DEFINE_string("color_table", None, "The color table to copy.") -flags.DEFINE_string("output_file", None, "Font assets are copied into.") +flags.mark_flag_as_required("target_font") +flags.mark_flag_as_required("donor_font") +flags.mark_flag_as_required("color_table") +flags.mark_flag_as_required("output_file") + + +class CbdtGlyphInfo(NamedTuple): + data: CbdtBitmapFormat17 + size: int def _copy_colr(target: ttLib.TTFont, donor: ttLib.TTFont): @@ -84,19 +95,100 @@ def _copy_svg(target: ttLib.TTFont, donor: ttLib.TTFont): target["SVG "] = donor["SVG "] +def _cbdt_data_and_sizes(ttfont: ttLib.TTFont) -> Mapping[str, CbdtGlyphInfo]: + data = {} + for strike_data in ttfont["CBDT"].strikeData: + data.update(strike_data) + + sizes = {} + for strike in ttfont["CBLC"].strikes: + for sub_table in strike.indexSubTables: + for name, (start, end) in zip(sub_table.names, sub_table.locations): + sizes[name] = end - start + + assert data.keys() == sizes.keys(), f"{data.keys()} != {sizes.keys()}" + + return { + glyph_name: CbdtGlyphInfo(data[glyph_name], sizes[glyph_name]) + for glyph_name in data + } + + +def _copy_cbdt(target: ttLib.TTFont, donor: ttLib.TTFont): + cbdt_glyph_info = _cbdt_data_and_sizes(donor) + + # reorder the bitmap table to match the targets glyph order + # we only support square bitmaps so the strikes are all the same + # other than glyph names so we can just construct a new + # order that matches that of target + donor_order = list(cbdt_glyph_info.keys()) + # 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") + new_order = sorted(donor_order, key=target.getGlyphID) + + # now we know the desired order, reshard into runs + # TODO duplicative of make_cbdt_table + strike_template = copy.deepcopy(donor["CBLC"].strikes[0]) + clbc_index_template = copy.deepcopy(strike_template.indexSubTables[0]) + strike_template.indexSubTables = [] + + cblc = donor["CBLC"] + cblc.strikes = [] + cbdt = donor["CBDT"] + cbdt.strikeData = [] + + data_offset = bitmap_tables.CBDT_HEADER_SIZE + while new_order: + # grab the next run w/consecutive gids + min_gid = target.getGlyphID(new_order[0]) + end = 1 + while ( + len(new_order) > end + and target.getGlyphID(new_order[end]) + == target.getGlyphID(new_order[end - 1]) + 1 + ): + end += 1 + glyph_run = new_order[:end] + new_order = new_order[end:] + max_gid = target.getGlyphID(glyph_run[-1]) + + strike = copy.deepcopy(strike_template) + 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) + strike.bitmapSizeTable.hori.widthMax = max_width + strike.bitmapSizeTable.vert.widthMax = max_width + + clbc_index = copy.deepcopy(clbc_index_template) + clbc_index.names = glyph_run + clbc_index.locations = [] + for glyph_name in glyph_run: + clbc_index.locations.append( + (data_offset, data_offset + cbdt_glyph_info[glyph_name].size) + ) + data_offset = clbc_index.locations[-1][-1] + + strike.indexSubTables = [clbc_index] + cblc.strikes.append(strike) + cbdt.strikeData.append({gn: cbdt_glyph_info[gn].data for gn in glyph_run}) + + target["CBDT"] = cbdt + target["CBLC"] = cblc + + def main(argv): target = load_fully(Path(FLAGS.target_font)) donor = load_fully(Path(FLAGS.donor_font)) - # TODO lookup, guess fn name, etc if FLAGS.color_table == "COLR": _copy_colr(target, donor) elif FLAGS.color_table == "SVG": _copy_svg(target, donor) + elif FLAGS.color_table == "CBDT": + _copy_cbdt(target, donor) else: - # TODO: SVG support - # Note that nanoemoji svg reorders glyphs to pack svgs nicely - # The merged font may need to update to the donors glyph order for this to work raise ValueError(f"Unsupported color table '{FLAGS.color_table}'") target.save(FLAGS.output_file) @@ -104,8 +196,4 @@ def main(argv): if __name__ == "__main__": - flags.mark_flag_as_required("target_font") - flags.mark_flag_as_required("donor_font") - flags.mark_flag_as_required("color_table") - flags.mark_flag_as_required("output_file") app.run(main) diff --git a/src/nanoemoji/maximum_color.py b/src/nanoemoji/maximum_color.py index fdf8fffc..11b584f2 100644 --- a/src/nanoemoji/maximum_color.py +++ b/src/nanoemoji/maximum_color.py @@ -19,6 +19,9 @@ https://www.youtube.com/watch?v=HLddvNiXym4 +Use cbdt for bitmaps because sbix is less x-platform than you'd guess +(https://github.com/harfbuzz/harfbuzz/issues/2679) + Sample usage: maximum_color MySvgFont.ttf""" @@ -26,6 +29,7 @@ from absl import flags from absl import logging from fontTools import ttLib +from nanoemoji import config from nanoemoji.colr_to_svg import colr_glyphs from nanoemoji.extract_svgs import svg_glyphs from nanoemoji.ninja import ( @@ -38,6 +42,7 @@ ) from nanoemoji.util import only from pathlib import Path +from typing import List, Tuple _SVG2COLR_GLYPHMAP = "svg2colr.glyphmap" @@ -46,6 +51,9 @@ _COLR2SVG_GLYPHMAP = "colr2svg.glyphmap" _COLR2SVG_CONFIG = "colr2svg.toml" +_CBDT_GLYPHMAP = "cbdt.glyphmap" +_CBDT_CONFIG = "cbdt.toml" + FLAGS = flags.FLAGS @@ -60,7 +68,7 @@ def _vector_color_table(font: ttLib.TTFont) -> str: has_svg = "SVG " in font has_colr = "COLR" in font if has_svg == has_colr: - raise ValueError("Must have one of COLR, SVG") + raise ValueError("Must have exactly one of COLR, SVG") if has_svg: return "SVG " @@ -86,6 +94,14 @@ def picosvg_dest(input_svg: Path) -> Path: return picosvg_dir() / input_svg.name +def bitmap_dir() -> Path: + return build_dir() / "bitmap" + + +def bitmap_dest(input_svg: Path) -> Path: + return bitmap_dir() / input_svg.with_suffix(".png").name + + def _write_preamble(nw: NinjaWriter, input_font: Path): module_rule( nw, @@ -126,6 +142,14 @@ def _write_preamble(nw: NinjaWriter, input_font: Path): ) 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", @@ -142,12 +166,27 @@ def _write_preamble(nw: NinjaWriter, input_font: Path): ) nw.newline() + module_rule( + nw, + "write_font", + f"--glyphmap_file {_CBDT_GLYPHMAP} --config_file {_CBDT_CONFIG} --output_file $out", + rule_name="write_cbdt_font", + ) + nw.newline() + nw.rule( f"picosvg", f"picosvg --output_file $out $in", ) nw.newline() + res = config.load().bitmap_resolution + nw.rule( + "write_bitmap", + f"resvg -h {res} -w {res} $in $out", + ) + nw.newline() + module_rule( nw, "glue_together", @@ -164,8 +203,19 @@ def _write_preamble(nw: NinjaWriter, input_font: Path): ) 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", + ) + nw.newline() + -def _generate_svg_from_colr(nw: NinjaWriter, input_font: Path, font: ttLib.TTFont): +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) @@ -197,16 +247,21 @@ def _generate_svg_from_colr(nw: NinjaWriter, input_font: Path, font: ttLib.TTFon ) nw.newline() - # stick our shiny new COLR table onto the input font and declare victory + # stick our shiny new COLR table onto the input font + output_file = Path(input_font.stem + ".both_vector_tables.ttf") nw.build( - input_font.name, + output_file, "copy_svg_from_colr2svg", "svg_from_colr.ttf", ) nw.newline() + return output_file, picosvgs -def _generate_colr_from_svg(nw: NinjaWriter, input_font: Path, font: ttLib.TTFont): + +def _generate_colr_from_svg( + 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) @@ -238,14 +293,62 @@ def _generate_colr_from_svg(nw: NinjaWriter, input_font: Path, font: ttLib.TTFon ) nw.newline() - # stick our shiny new COLR table onto the input font and declare victory + # stick our shiny new COLR table onto the input font + output_file = Path(input_font.stem + ".both_vector_tables.ttf") nw.build( - input_font.name, + output_file, "copy_colr_from_svg2colr", "colr_from_svg.ttf", ) nw.newline() + return output_file, picosvgs + + +def _generate_cbdt( + nw: NinjaWriter, + input_font: Path, + font: ttLib.TTFont, + color_font: Path, + picosvg_files: List[Path], +): + # generate bitmaps + bitmap_files = [rel_build(bitmap_dest(s)) for s in picosvg_files] + for picosvg, bitmap in zip(picosvg_files, bitmap_files): + 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], + ) + nw.newline() + + # 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 + nw.build( + "cbdt.ttf", + "write_cbdt_font", + [_CBDT_GLYPHMAP, _CBDT_CONFIG], + implicit=[input_font.stem + ".both_vector_tables.ttf"], + ) + nw.newline() + + # stick our shiny new CBDT table onto the input font and declare victory + nw.build( + input_font.name, + "copy_cbdt", + "cbdt.ttf", + ) + nw.newline() + def _run(argv): if len(argv) != 2: @@ -261,7 +364,7 @@ def _run(argv): build_file = build_dir() / "build.ninja" build_dir().mkdir(parents=True, exist_ok=True) - # TODO flag control instead of guessing + # TODO flag control color_table = _vector_color_table(font) if gen_ninja(): @@ -272,9 +375,15 @@ def _run(argv): _write_preamble(nw, input_font) if color_table == "COLR": - _generate_svg_from_colr(nw, input_font, font) + color_font, picosvg_files = _generate_svg_from_colr( + nw, input_font, font + ) else: - _generate_colr_from_svg(nw, input_font, font) + color_font, picosvg_files = _generate_colr_from_svg( + nw, input_font, font + ) + + _generate_cbdt(nw, input_font, font, color_font, picosvg_files) maybe_run_ninja(build_file) diff --git a/src/nanoemoji/nanoemoji.py b/src/nanoemoji/nanoemoji.py index c91d900c..2ac745de 100644 --- a/src/nanoemoji/nanoemoji.py +++ b/src/nanoemoji/nanoemoji.py @@ -209,7 +209,7 @@ def _chrome_command() -> str: def write_config_preamble(nw, font_config: FontConfig): if font_config.has_bitmaps: - # TODO bitmap support for non-square svgs? - baby steps + # conveniently if w != h this already scales w res = font_config.bitmap_resolution nw.rule( "write_bitmap", diff --git a/src/nanoemoji/ninja.py b/src/nanoemoji/ninja.py index dbaf8811..43b24e58 100644 --- a/src/nanoemoji/ninja.py +++ b/src/nanoemoji/ninja.py @@ -54,8 +54,10 @@ def _str_paths(self, args): def rule(self, *args, **kwargs): self._nw.rule(*args, **kwargs) - def build(self, *args): - self._nw.build(*self._str_paths(args)) + def build(self, *args, **kwargs): + self._nw.build( + *self._str_paths(args), **{k: self._str_paths(v) for k, v in kwargs.items()} + ) def newline(self): self._nw.newline() diff --git a/src/nanoemoji/png.py b/src/nanoemoji/png.py index 450b0713..78331735 100644 --- a/src/nanoemoji/png.py +++ b/src/nanoemoji/png.py @@ -12,9 +12,11 @@ # See the License for the specific language governing permissions and # limitations under the License. +from io import BytesIO import os from pathlib import Path -from typing import Union +from PIL import Image +from typing import Tuple, Union class PNG(bytes): @@ -29,8 +31,16 @@ def __new__(cls, *args, **kwargs): header = self[:8] if header != cls.SIGNATURE: raise ValueError("Invalid PNG file: bad signature {header!r}") + self._size = None return self + @property + def size(self) -> Tuple[int, int]: + if self._size is None: + with Image.open(BytesIO(self)) as image: + self._size = image.size + return self._size + @classmethod def read_from(cls, path: Union[str, os.PathLike]) -> "PNG": return cls(Path(path).read_bytes()) diff --git a/src/nanoemoji/write_font.py b/src/nanoemoji/write_font.py index df2d01dc..84b2326b 100644 --- a/src/nanoemoji/write_font.py +++ b/src/nanoemoji/write_font.py @@ -163,23 +163,6 @@ class ColorGenerator(NamedTuple): lambda *args: _sbix_ttfont(*args), ".ttf", ), - # https://github.com/googlefonts/nanoemoji/issues/260 svg, colr - # Non-compressed picosvg because woff2 is likely - # Meant to be subset if used for network delivery - "glyf_colr_1_and_picosvg": ColorGenerator( - lambda *args: _colr_ufo(1, *args), - lambda *args: _svg_ttfont(*args, picosvg=True, compressed=False), - ".ttf", - ), - # https://github.com/googlefonts/nanoemoji/issues/260 svg, colr, cbdt; max compatibility - # Meant to be subset if used for network delivery - # Non-compressed picosvg because woff2 is likely - # cbdt because sbix is less x-platform than you'd guess (https://github.com/harfbuzz/harfbuzz/issues/2679) - "glyf_colr_1_and_picosvg_and_cbdt": ColorGenerator( - lambda *args: _colr_ufo(1, *args), - lambda *args: _picosvg_and_cbdt(*args), - ".ttf", - ), } assert _COLOR_FORMAT_GENERATORS.keys() == set(config._COLOR_FORMATS) diff --git a/src/nanoemoji/write_glyphmap_for_glyph_svgs.py b/src/nanoemoji/write_glyphmap_for_glyph_svgs.py index 83df4c8a..97f5c4d4 100644 --- a/src/nanoemoji/write_glyphmap_for_glyph_svgs.py +++ b/src/nanoemoji/write_glyphmap_for_glyph_svgs.py @@ -25,6 +25,7 @@ FLAGS = flags.FLAGS flags.DEFINE_string("output_file", "-", "Output filename ('-' means stdout)") +flags.DEFINE_bool("bitmaps", False, "True if bitmaps should be included in glyphmap") def main(argv): @@ -35,17 +36,30 @@ def main(argv): glyph_order = ttLib.TTFont(source_font).getGlyphOrder() input_files = sorted( - (Path(f) for f in input_files if f != source_font), key=lambda f: int(f.stem) + (Path(f) for f in input_files if f != source_font), + key=lambda f: f.stem, + reverse=True, ) with util.file_printer(FLAGS.output_file) as print: - for input_file in input_files: + while input_files: + svg_file = input_files.pop() + assert svg_file.suffix in {".png", ".svg"}, f"What is {svg_file}" + bitmap_file = None + if svg_file.suffix == ".png": + bitmap_file = svg_file + svg_file = input_files.pop() + assert svg_file.suffix == ".svg" + assert int(svg_file.stem) == int( + bitmap_file.stem + ), f"Mismatched {svg_file}, {bitmap_file}" + print( GlyphMapping( - svg_file=input_file, - bitmap_file=None, + svg_file=svg_file, + bitmap_file=bitmap_file, codepoints=(), - glyph_name=glyph_order[int(input_file.stem)], + glyph_name=glyph_order[int(svg_file.stem)], ).csv_line() ) diff --git a/tests/bitmap_tables_test.py b/tests/bitmap_tables_test.py new file mode 100644 index 00000000..acfc6977 --- /dev/null +++ b/tests/bitmap_tables_test.py @@ -0,0 +1,33 @@ +# 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 io import BytesIO +from nanoemoji import config +from nanoemoji.bitmap_tables import _cbdt_bitmap_data, BitmapMetrics +from nanoemoji.png import PNG +from PIL import Image +from test_helper import * + + +def test_bitmap_data_for_non_square_image(): + font_config = config.load() + image_bytes = BytesIO() + Image.new("RGBA", (90, 120), color="red").save(image_bytes, format="png") + image_bytes.seek(0) + image_bytes = image_bytes.read() + png = PNG(image_bytes) + + metrics = BitmapMetrics.create(font_config, png, 120) + cbdt_bitmap = _cbdt_bitmap_data(font_config, metrics, png) + assert (cbdt_bitmap.metrics.width, cbdt_bitmap.metrics.height) == (90, 120) diff --git a/tests/compat_font/config.toml b/tests/cbdt/config.toml similarity index 83% rename from tests/compat_font/config.toml rename to tests/cbdt/config.toml index fc990547..a71bd2cf 100644 --- a/tests/compat_font/config.toml +++ b/tests/cbdt/config.toml @@ -1,6 +1,6 @@ family = "Noto Color Emoji Compat Test" output_file = "Font.ttf" -color_format = "glyf_colr_1_and_picosvg_and_cbdt" +color_format = "cbdt" [axis.wght] name = "Weight" diff --git a/tests/compat_font/emoji_u0023.svg b/tests/cbdt/emoji_u0023.svg similarity index 100% rename from tests/compat_font/emoji_u0023.svg rename to tests/cbdt/emoji_u0023.svg diff --git a/tests/compat_font/emoji_u1f1e6_1f1e8.svg b/tests/cbdt/emoji_u1f1e6_1f1e8.svg similarity index 100% rename from tests/compat_font/emoji_u1f1e6_1f1e8.svg rename to tests/cbdt/emoji_u1f1e6_1f1e8.svg diff --git a/tests/maximum_color_test.py b/tests/maximum_color_test.py index 9c5bd803..018e1628 100644 --- a/tests/maximum_color_test.py +++ b/tests/maximum_color_test.py @@ -33,8 +33,8 @@ def _cleanup_temporary_dirs(): @pytest.mark.parametrize( "color_format, expected_new_tables", [ - ("picosvg", {"COLR", "CPAL"}), - ("glyf_colr_1", {"SVG "}), + ("picosvg", {"COLR", "CPAL", "CBDT", "CBLC"}), + ("glyf_colr_1", {"SVG ", "CBDT", "CBLC"}), ], ) def test_build_maximum_font(color_format, expected_new_tables): diff --git a/tests/minimal_static/config_glyf_colr_1_and_picosvg.toml b/tests/minimal_static/config_glyf_colr_1_and_picosvg.toml deleted file mode 100644 index e6314a1b..00000000 --- a/tests/minimal_static/config_glyf_colr_1_and_picosvg.toml +++ /dev/null @@ -1,13 +0,0 @@ -output_file = "Font.ttf" -color_format = "glyf_colr_1_and_picosvg" - -[axis.wght] -name = "Weight" -default = 400 - -[master.regular] -style_name = "Regular" -srcs = ["svg/*.svg"] - -[master.regular.position] -wght = 400 diff --git a/tests/minimal_static/config_glyf_colr_1_and_picosvg_and_cbdt.toml b/tests/minimal_static/config_glyf_colr_1_and_picosvg_and_cbdt.toml deleted file mode 100644 index 4e7acbe0..00000000 --- a/tests/minimal_static/config_glyf_colr_1_and_picosvg_and_cbdt.toml +++ /dev/null @@ -1,13 +0,0 @@ -output_file = "Font.ttf" -color_format = "glyf_colr_1_and_picosvg_and_cbdt" - -[axis.wght] -name = "Weight" -default = 400 - -[master.regular] -style_name = "Regular" -srcs = ["svg/*.svg"] - -[master.regular.position] -wght = 400 diff --git a/tests/nanoemoji_test.py b/tests/nanoemoji_test.py index 1155b162..e90ab2a0 100644 --- a/tests/nanoemoji_test.py +++ b/tests/nanoemoji_test.py @@ -136,17 +136,6 @@ def test_build_untouchedsvg_font(): assert transform != Affine2D.identity(), transform -def test_build_glyf_colr_1_and_picosvg_font(): - tmp_dir = run_nanoemoji( - (locate_test_file("minimal_static/config_glyf_colr_1_and_picosvg.toml"),) - ) - - font = TTFont(tmp_dir / "Font.ttf") - - assert "COLR" in font - assert "SVG " in font - - def _assert_table_size_cmp(table_tag, op, original_font, original_cmd, **options): cmd = original_cmd + tuple(f"--{'' if v else 'no'}{k}" for k, v in options.items()) tmp_dir = run_nanoemoji(cmd) @@ -202,9 +191,8 @@ def test_build_cbdt_font(use_pngquant, use_zopflipng): @pytest.mark.parametrize( "config_file", [ - "minimal_static/config_glyf_colr_1_and_picosvg_and_cbdt.toml", # https://github.com/googlefonts/nanoemoji/issues/385 - "compat_font/config.toml", + "cbdt/config.toml", ], ) @pytest.mark.parametrize("use_zopflipng", [True, False]) @@ -214,8 +202,6 @@ def test_build_compat_font(config_file, use_pngquant, use_zopflipng): tmp_dir = run_nanoemoji_memoized(cmd) font = TTFont(tmp_dir / "Font.ttf") - assert "COLR" in font - assert "SVG " in font assert "CBDT" in font assert "CBLC" in font @@ -279,7 +265,14 @@ def test_glyphmap_games(): ) -def test_omit_empty_color_glyphs(): +@pytest.mark.parametrize( + "color_format, expected_ttx", + [ + ("glyf_colr_1", "omit_empty_color_glyphs_colr.ttx"), + ("picosvg", "omit_empty_color_glyphs_svg.ttx"), + ], +) +def test_omit_empty_color_glyphs(color_format, expected_ttx): svgs = [ "emoji_u200c.svg", # whitespace glyph, contains no paths "emoji_u42.svg", @@ -287,7 +280,7 @@ def test_omit_empty_color_glyphs(): tmp_dir = run_nanoemoji( ( - "--color_format=glyf_colr_1_and_picosvg", + f"--color_format={color_format}", "--pretty_print", "--keep_glyph_names", *(locate_test_file(svg) for svg in svgs), @@ -296,16 +289,17 @@ def test_omit_empty_color_glyphs(): font = TTFont(tmp_dir / "Font.ttf") - colr = font["COLR"].table - assert len(colr.BaseGlyphList.BaseGlyphPaintRecord) == 1 - - svg = font["SVG "] - assert len(svg.docList) == 1 + if "COLR" in font: + colr = font["COLR"].table + assert len(colr.BaseGlyphList.BaseGlyphPaintRecord) == 1 + else: + svg = font["SVG "] + assert len(svg.docList) == 1 assert_expected_ttx( svgs, font, - "omit_empty_color_glyphs.ttx", + expected_ttx, include_tables=["GlyphOrder", "cmap", "glyf", "COLR", "SVG "], ) diff --git a/tests/narrow_rects/a.svg b/tests/narrow_rects/a.svg index c6050978..c8b9644a 100644 --- a/tests/narrow_rects/a.svg +++ b/tests/narrow_rects/a.svg @@ -1,3 +1,3 @@ - + \ No newline at end of file diff --git a/tests/narrow_rects/b.svg b/tests/narrow_rects/b.svg index c6050978..c8b9644a 100644 --- a/tests/narrow_rects/b.svg +++ b/tests/narrow_rects/b.svg @@ -1,3 +1,3 @@ - + \ No newline at end of file diff --git a/tests/narrow_rects/c.svg b/tests/narrow_rects/c.svg index 3ac8facd..e1b62c5f 100644 --- a/tests/narrow_rects/c.svg +++ b/tests/narrow_rects/c.svg @@ -1,3 +1,3 @@ - + \ No newline at end of file diff --git a/tests/omit_empty_color_glyphs.ttx b/tests/omit_empty_color_glyphs_colr.ttx similarity index 88% rename from tests/omit_empty_color_glyphs.ttx rename to tests/omit_empty_color_glyphs_colr.ttx index b1f39756..b972df9e 100644 --- a/tests/omit_empty_color_glyphs.ttx +++ b/tests/omit_empty_color_glyphs_colr.ttx @@ -5,9 +5,9 @@ - + - + @@ -93,15 +93,4 @@ - - - <svg xmlns="http://www.w3.org/2000/svg" version="1.1"> - <g id="glyph3" transform="matrix(120 0 0 120 37.5 -950)"> - <path d="M4,4 L8,4 L8,8 L4,8 L4,4 Z" fill="orange"/> - </g> -</svg> - - - - diff --git a/tests/omit_empty_color_glyphs_svg.ttx b/tests/omit_empty_color_glyphs_svg.ttx new file mode 100644 index 00000000..82933da5 --- /dev/null +++ b/tests/omit_empty_color_glyphs_svg.ttx @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + <svg xmlns="http://www.w3.org/2000/svg" version="1.1"> + <g id="glyph2" transform="matrix(120 0 0 120 37.5 -950)"> + <path d="M4,4 L8,4 L8,8 L4,8 L4,4 Z" fill="orange"/> + </g> +</svg> + + + + + diff --git a/tests/rect_glyf_colr_1_and_picosvg_and_cbdt.ttx b/tests/proportional_cbdt.ttx similarity index 51% rename from tests/rect_glyf_colr_1_and_picosvg_and_cbdt.ttx rename to tests/proportional_cbdt.ttx index 28ebe675..f4cb1688 100644 --- a/tests/rect_glyf_colr_1_and_picosvg_and_cbdt.ttx +++ b/tests/proportional_cbdt.ttx @@ -5,15 +5,15 @@ - - + + - - + + - + @@ -21,10 +21,12 @@ + + @@ -57,30 +59,32 @@ - - - - - - - - - +
- + - - + + + + + + + + + + + + - +
@@ -90,9 +94,9 @@
- - - + + + @@ -104,9 +108,9 @@ - - - + + + @@ -119,89 +123,19 @@ - - - + + + - - + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1"> - <defs> - <path d="M2,2 L8,2 L8,4 L2,4 L2,2 Z" id="e000.0" fill="blue"/> - </defs> - <g id="glyph3" transform="matrix(11 0 0 11 0 -90)"> - <use xlink:href="#e000.0"/> - <use xlink:href="#e000.0" x="2" y="2" opacity="0.8"/> - </g> -</svg> - - - - diff --git a/tests/rect_cbdt.ttx b/tests/rect_cbdt.ttx index 92b65de6..1ab81fde 100644 --- a/tests/rect_cbdt.ttx +++ b/tests/rect_cbdt.ttx @@ -11,7 +11,7 @@ - + @@ -66,7 +66,7 @@ - + @@ -80,7 +80,7 @@ - + @@ -94,7 +94,7 @@ - + diff --git a/tests/rect_glyf_colr_1_and_picosvg.ttx b/tests/rect_glyf_colr_1_and_picosvg.ttx deleted file mode 100644 index c1123587..00000000 --- a/tests/rect_glyf_colr_1_and_picosvg.ttx +++ /dev/null @@ -1,143 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1"> - <defs> - <path d="M2,2 L8,2 L8,4 L2,4 L2,2 Z" id="e000.0" fill="blue"/> - </defs> - <g id="glyph3" transform="matrix(10 0 0 10 0 -100)"> - <use xlink:href="#e000.0"/> - <use xlink:href="#e000.0" x="2" y="2" opacity="0.8"/> - </g> -</svg> - - - - - diff --git a/tests/reused_shape_with_gradient.ttx b/tests/reused_shape_with_gradient_colr.ttx similarity index 83% rename from tests/reused_shape_with_gradient.ttx rename to tests/reused_shape_with_gradient_colr.ttx index 05d977d7..e26b0048 100644 --- a/tests/reused_shape_with_gradient.ttx +++ b/tests/reused_shape_with_gradient_colr.ttx @@ -5,8 +5,8 @@ - - + + @@ -245,37 +245,4 @@ - - - <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1"> - <defs> - <path d="M77.56,63.99 A13.56 13.56 0 1 1 50.44,63.99 A13.56 13.56 0 1 1 77.56,63.99 Z" id="e000.0"/> - <radialGradient id="g1" cx="59.958" cy="61.112" r="13.562" gradientUnits="userSpaceOnUse"> - <stop offset="0.014" stop-color="#FFEB3B"/> - <stop offset="0.626" stop-color="#FCCD31"/> - <stop offset="1" stop-color="#FBC02D"/> - </radialGradient> - <radialGradient id="g2" cx="59.813" cy="60.675" r="13.562" gradientUnits="userSpaceOnUse"> - <stop offset="0.005" stop-color="#81C784"/> - <stop offset="0.201" stop-color="#72BE76"/> - <stop offset="0.719" stop-color="#50A854"/> - <stop offset="1" stop-color="#43A047"/> - </radialGradient> - <radialGradient id="g4" cx="59.765" cy="60.435" r="13.562" gradientUnits="userSpaceOnUse"> - <stop offset="0" stop-color="#E57373"/> - <stop offset="0.197" stop-color="#E56564"/> - <stop offset="0.718" stop-color="#E54542"/> - <stop offset="1" stop-color="#E53935"/> - </radialGradient> - </defs> - <g id="glyph3" transform="matrix(0.781 0 0 0.781 0 -100)"> - <use xlink:href="#e000.0" fill="url(#g1)"/> - <use xlink:href="#e000.0" y="36.94" fill="url(#g2)"/> - <use xlink:href="#e000.0" y="-36.93" fill="url(#g4)"/> - </g> -</svg> - - - - diff --git a/tests/reused_shape_with_gradient_svg.ttx b/tests/reused_shape_with_gradient_svg.ttx new file mode 100644 index 00000000..54e33af6 --- /dev/null +++ b/tests/reused_shape_with_gradient_svg.ttx @@ -0,0 +1,93 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1"> + <defs> + <path d="M77.56,63.99 A13.56 13.56 0 1 1 50.44,63.99 A13.56 13.56 0 1 1 77.56,63.99 Z" id="e000.0"/> + <radialGradient id="g1" cx="59.958" cy="61.112" r="13.562" gradientUnits="userSpaceOnUse"> + <stop offset="0.014" stop-color="#FFEB3B"/> + <stop offset="0.626" stop-color="#FCCD31"/> + <stop offset="1" stop-color="#FBC02D"/> + </radialGradient> + <radialGradient id="g2" cx="59.813" cy="60.675" r="13.562" gradientUnits="userSpaceOnUse"> + <stop offset="0.005" stop-color="#81C784"/> + <stop offset="0.201" stop-color="#72BE76"/> + <stop offset="0.719" stop-color="#50A854"/> + <stop offset="1" stop-color="#43A047"/> + </radialGradient> + <radialGradient id="g4" cx="59.765" cy="60.435" r="13.562" gradientUnits="userSpaceOnUse"> + <stop offset="0" stop-color="#E57373"/> + <stop offset="0.197" stop-color="#E56564"/> + <stop offset="0.718" stop-color="#E54542"/> + <stop offset="1" stop-color="#E53935"/> + </radialGradient> + </defs> + <g id="glyph2" transform="matrix(0.781 0 0 0.781 0 -100)"> + <use xlink:href="#e000.0" fill="url(#g1)"/> + <use xlink:href="#e000.0" y="36.94" fill="url(#g2)"/> + <use xlink:href="#e000.0" y="-36.93" fill="url(#g4)"/> + </g> +</svg> + + + + + diff --git a/tests/write_font_test.py b/tests/write_font_test.py index 98d52550..61ef3f9e 100644 --- a/tests/write_font_test.py +++ b/tests/write_font_test.py @@ -222,8 +222,15 @@ def test_vertical_metrics(ascender, descender, linegap): # https://github.com/googlefonts/nanoemoji/issues/334 ( ("reused_shape_with_gradient.svg",), - "reused_shape_with_gradient.ttx", - {"color_format": "glyf_colr_1_and_picosvg", "pretty_print": True}, + "reused_shape_with_gradient_colr.ttx", + {"color_format": "glyf_colr_1", "pretty_print": True}, + ), + # Check gradient coordinates are correctly transformed after shape reuse + # https://github.com/googlefonts/nanoemoji/issues/334 + ( + ("reused_shape_with_gradient.svg",), + "reused_shape_with_gradient_svg.ttx", + {"color_format": "picosvg", "pretty_print": True}, ), # Confirm we can apply a user transform, override some basic metrics ( @@ -287,24 +294,6 @@ def test_vertical_metrics(ascender, descender, linegap): "reuse_shape_varying_fill.ttx", {"color_format": "picosvg", "pretty_print": True}, ), - # Generate COLRv1 + SVG - ( - ("rect.svg",), - "rect_glyf_colr_1_and_picosvg.ttx", - {"color_format": "glyf_colr_1_and_picosvg", "pretty_print": True}, - ), - # Generate COLRv1 + SVG + cbdt - ( - ("rect.svg",), - "rect_glyf_colr_1_and_picosvg_and_cbdt.ttx", - # we end up with out of bounds line metrics with default ascender/descender - { - "color_format": "glyf_colr_1_and_picosvg_and_cbdt", - "pretty_print": True, - "ascender": 90, - "descender": -20, - }, - ), # Generate simple cbdt ( ("rect2.svg",), @@ -312,6 +301,14 @@ def test_vertical_metrics(ascender, descender, linegap): # we end up with out of bounds line metrics with default ascender/descender {"color_format": "cbdt", "ascender": 90, "descender": -20}, ), + # Generate proportional cbdt + ( + ("rect2.svg", "narrow_rects/a.svg"), + "proportional_cbdt.ttx", + # we end up with out of bounds line metrics with default ascender/descender + # width 0 forces sizing entirely from the input box proportions + {"color_format": "cbdt", "ascender": 90, "descender": -20, "width": 0}, + ), # Generate simple sbix ( ("rect2.svg",), @@ -508,8 +505,6 @@ class InputFormat(enum.Flag): ("picosvgz", InputFormat.SVG), ("untouchedsvg", InputFormat.SVG), ("untouchedsvgz", InputFormat.SVG), - ("glyf_colr_1_and_picosvg", InputFormat.SVG), - ("glyf_colr_1_and_picosvg_and_cbdt", InputFormat.SVG | InputFormat.PNG), ], ) def test_inputs_have_svg_and_or_bitmap(tmp_path, color_format, expected_input_format): @@ -518,8 +513,7 @@ def test_inputs_have_svg_and_or_bitmap(tmp_path, color_format, expected_input_fo # formats don't use that so their InputGlyph.svg attribute is None. # https://github.com/googlefonts/nanoemoji/issues/378 # Also check that inputs have their 'bitmap' attribute set to the PNG bytes for - # all the color formats that include that, including hybrid vector+bitmap formats - # e.g. `glyf_colr_1_and_picosvg_and_cbdt`. + # all the color formats that include that. expected_has_svgs = bool(expected_input_format & InputFormat.SVG) expected_has_bitmaps = bool(expected_input_format & InputFormat.PNG)