Skip to content

Commit

Permalink
work around Safari bug when gradientTransform contains involutory matrix
Browse files Browse the repository at this point in the history
Fixes #268

Safari currently rejects a gradient if gradientTransform attribute contains a matrix whose inverse is equal to itself (aka 'involutory matrix'). These are perfectly valid, non-singular invertible transformations which happen to have an inverse which is equal to themselves (notably, reflection across x/y axis). Safari assumes they are non-invertible and incorrectly rejects the whole gradient. The recommendation from the WebKit maintainer is to hack the matrix by a small, invisible amount enough to trick Safari. I heard this bug has already been fixed internally but will take time until it reaches all users. Until then, we have no choice but doing this.
  • Loading branch information
anthrotype committed Apr 13, 2021
1 parent a7cb5a5 commit 65c2d1d
Show file tree
Hide file tree
Showing 4 changed files with 90 additions and 3 deletions.
21 changes: 18 additions & 3 deletions src/nanoemoji/svg.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@
from typing import MutableMapping, NamedTuple, Optional, Sequence, Tuple, Union


# topicosvg()'s default
_DEFAULT_ROUND_NDIGITS = 3


class InterGlyphReuseKey(NamedTuple):
view_box: Rect
paint: Paint
Expand Down Expand Up @@ -96,12 +100,12 @@ def _glyph_groups(color_glyphs: Sequence[ColorGlyph]) -> Tuple[Tuple[str, ...]]:


def _ntos(n: float) -> str:
return svg_meta.ntos(round(n, 3))
return svg_meta.ntos(round(n, _DEFAULT_ROUND_NDIGITS))


# https://docs.microsoft.com/en-us/typography/opentype/spec/svg#coordinate-systems-and-glyph-metrics
def _svg_matrix(transform: Affine2D) -> str:
return f'matrix({" ".join((_ntos(v) for v in transform))})'
return transform.round(_DEFAULT_ROUND_NDIGITS).tostring()


def _inter_glyph_reuse_key(
Expand Down Expand Up @@ -172,8 +176,19 @@ def _apply_gradient_common_parts(
stop_el.attrib["stop-opacity"] = _ntos(stop.color.alpha)
if paint.extend != Extend.PAD:
gradient.attrib["spreadMethod"] = paint.extend.name.lower()

transform = transform.round(_DEFAULT_ROUND_NDIGITS)
if transform != Affine2D.identity():
gradient.attrib["gradientTransform"] = _svg_matrix(transform)
# Safari has a bug which makes it reject a gradient if gradientTransform
# contains an 'involutory matrix' (i.e. matrix whose inverse equals itself,
# such that M @ M == Identity, e.g. reflection), hence the following hack:
# https://github.com/googlefonts/nanoemoji/issues/268
# https://en.wikipedia.org/wiki/Involutory_matrix
# TODO: Remove once the bug gets fixed
if Affine2D.product(transform, transform) == Affine2D.identity():
transform = transform._replace(a=transform.a + 0.00001)
assert transform.inverse() != transform
gradient.attrib["gradientTransform"] = transform.tostring()


def _define_linear_gradient(
Expand Down
11 changes: 11 additions & 0 deletions tests/involutory_matrix.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
53 changes: 53 additions & 0 deletions tests/involutory_matrix_picosvg.ttx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
<?xml version="1.0" encoding="UTF-8"?>
<ttFont sfntVersion="\x00\x01\x00\x00">

<GlyphOrder>
<!-- The 'id' attribute is only for humans; it is ignored when parsed. -->
<GlyphID id="0" name=".notdef"/>
<GlyphID id="1" name=".space"/>
<GlyphID id="2" name="e000"/>
</GlyphOrder>

<hmtx>
<mtx name=".notdef" width="0" lsb="0"/>
<mtx name=".space" width="100" lsb="0"/>
<mtx name="e000" width="100" lsb="0"/>
</hmtx>

<cmap>
<tableVersion version="0"/>
<cmap_format_4 platformID="0" platEncID="3" language="0">
<map code="0x20" name=".space"/><!-- SPACE -->
<map code="0xe000" name="e000"/><!-- ???? -->
</cmap_format_4>
<cmap_format_4 platformID="3" platEncID="1" language="0">
<map code="0x20" name=".space"/><!-- SPACE -->
<map code="0xe000" name="e000"/><!-- ???? -->
</cmap_format_4>
</cmap>

<loca>
<!-- The 'loca' table will be calculated by the compiler -->
</loca>

<glyf>

<!-- The xMin, yMin, xMax and yMax values
will be recalculated by the compiler. -->

<TTGlyph name=".notdef"/><!-- contains no outline data -->

<TTGlyph name=".space"/><!-- contains no outline data -->

<TTGlyph name="e000"/><!-- contains no outline data -->

</glyf>

<SVG>

<svgDoc endGlyphID="2" startGlyphID="2">
<![CDATA[<svg xmlns="http://www.w3.org/2000/svg" version="1.1"><defs><radialGradient id="g1" fx="-32" fy="32" cx="-64" cy="64" r="64" gradientUnits="userSpaceOnUse" gradientTransform="matrix(-0.99999 0 0 1 0 0)"><stop offset="0" stop-color="red"/><stop offset="1" stop-color="blue"/></radialGradient></defs><g id="glyph2" transform="matrix(0.781 0 0 0.781 0 -100)"><path d="M0,0 H128 V128 H0 V0 Z" fill="url(#g1)"/></g></svg>]]>
</svgDoc>
</SVG>

</ttFont>
8 changes: 8 additions & 0 deletions tests/write_font_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,14 @@ def test_vertical_metrics(ascender, descender, linegap):
"reused_shape_2_picosvg.ttx",
{"color_format": "picosvg"},
),
# Safari can't deal with gradientTransform where matrix.inverse() == self,
# we work around it by nudging one matrix component by an invisible amount
# https://github.com/googlefonts/nanoemoji/issues/268
(
("involutory_matrix.svg",),
"involutory_matrix_picosvg.ttx",
{"color_format": "picosvg"},
),
],
)
def test_write_font_binary(svgs, expected_ttx, config_overrides):
Expand Down

0 comments on commit 65c2d1d

Please sign in to comment.