mirror of
https://github.com/block/buzz.git
synced 2026-10-29 05:21:31 +00:00
Use native color emoji for warning signs too
Signed-off-by: Tom Brow <tomb@block.xyz>
This commit is contained in:
Binary file not shown.
Binary file not shown.
@@ -1,8 +1,9 @@
|
||||
# Inter heart fallback
|
||||
# Inter heart and warning fallback
|
||||
|
||||
The two Inter 4.001 variable fonts are derived from the previously bundled
|
||||
fonts by removing only U+2764 from their Unicode character maps. Flutter can
|
||||
then choose a native font for heart characters, including emoji presentation
|
||||
fonts by removing only U+2764 and U+26A0 from their Unicode character maps.
|
||||
Flutter can then choose a native font for heart and warning characters,
|
||||
including emoji presentation
|
||||
in reactions and message text. No glyphs, language coverage, other symbols,
|
||||
font metrics, or variation tables are removed. The license remains in
|
||||
`Inter-LICENSE.txt`.
|
||||
@@ -23,20 +24,20 @@ python3 -m venv /tmp/buzz-inter-tools
|
||||
The script checks pristine source hashes and verifies every character mapping
|
||||
and every other font table. Only the `cmap` table and the font checksum may
|
||||
change. Repeated generation from the same sources produces identical bytes.
|
||||
Warning signs, ©, ®, ™, arrows, math symbols, accents, and supported languages
|
||||
©, ®, ™, arrows, math symbols, accents, and supported languages
|
||||
keep their original Inter mappings.
|
||||
|
||||
Use `test/visual/heart_sim_app.dart` on an actual iOS simulator to compare
|
||||
reactions, plain/emoji/text-presentation hearts, bold/italic message text,
|
||||
warning signs, and preserved symbols. Font fallback for plain and explicit
|
||||
text-presentation hearts is platform-dependent; do not infer their appearance
|
||||
from a macOS widget-test renderer. On iOS 26.5, all three heart presentations
|
||||
fall back to the color emoji, including explicit VS15. Message line metrics
|
||||
text-presentation hearts and warnings is platform-dependent; do not infer their appearance
|
||||
from a macOS widget-test renderer. On iOS 26.5, all three heart and warning
|
||||
presentations fall back to the color emoji, including explicit VS15. Message line metrics
|
||||
can change with the fallback font.
|
||||
|
||||
The native screenshot regression check (requires Pillow) covers both reaction
|
||||
states, each message-heart presentation, bold/italic text, and unchanged warning
|
||||
signs and symbols:
|
||||
states, each heart and warning presentation, bold/italic text, and unchanged
|
||||
symbols:
|
||||
|
||||
```sh
|
||||
python3 mobile/test/visual/verify_heart_screenshots.py before.png after.png
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Remove only Inter's U+2764 mapping so hearts use native font fallback.
|
||||
"""Remove Inter's heart and warning mappings for native emoji fallback.
|
||||
|
||||
Requires fonttools==4.60.1. See assets/fonts/README.md for source and commands.
|
||||
"""
|
||||
@@ -15,7 +15,7 @@ SOURCES = {
|
||||
"InterVariable.ttf": "4989b125924991b90d05b2d16e0e388c48f7d5bb8b30539bbf9c755278d0ccaf",
|
||||
"InterVariable-Italic.ttf": "d6f1f6a172d9e588438db9f986fd5cfad7b30f644374080a8a9d4d91e344586f",
|
||||
}
|
||||
HEART = 0x2764
|
||||
NATIVE_EMOJI = (0x2764, 0x26A0)
|
||||
|
||||
|
||||
def verify(source: Path, output: Path) -> None:
|
||||
@@ -41,9 +41,10 @@ def verify(source: Path, output: Path) -> None:
|
||||
raise ValueError("Character-map identity changed")
|
||||
expected = dict(old.cmap)
|
||||
if old.isUnicode():
|
||||
del expected[HEART]
|
||||
for codepoint in NATIVE_EMOJI:
|
||||
del expected[codepoint]
|
||||
if new.cmap != expected:
|
||||
raise ValueError("Character mappings changed beyond U+2764")
|
||||
raise ValueError("Character mappings changed beyond U+2764 and U+26A0")
|
||||
|
||||
|
||||
def main() -> None:
|
||||
@@ -61,15 +62,16 @@ def main() -> None:
|
||||
if not args.check:
|
||||
with TTFont(source, lazy=True, recalcTimestamp=False) as font:
|
||||
tables = [t for t in font["cmap"].tables if t.isUnicode()]
|
||||
if not tables or any(HEART not in t.cmap for t in tables):
|
||||
raise ValueError(f"Expected heart mapping in {name}")
|
||||
if not tables or any(codepoint not in t.cmap for t in tables for codepoint in NATIVE_EMOJI):
|
||||
raise ValueError(f"Expected heart and warning mappings in {name}")
|
||||
# FontTools shares dictionaries between equivalent subtables.
|
||||
for cmap in {id(t.cmap): t.cmap for t in tables}.values():
|
||||
del cmap[HEART]
|
||||
for codepoint in NATIVE_EMOJI:
|
||||
del cmap[codepoint]
|
||||
args.output_dir.mkdir(parents=True, exist_ok=True)
|
||||
font.save(output, reorderTables=False)
|
||||
verify(source, output)
|
||||
print(f"{name}: only U+2764 mapping removed; other mappings and glyph data unchanged")
|
||||
print(f"{name}: only U+2764 and U+26A0 mappings removed; other mappings and glyph data unchanged")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -23,7 +23,7 @@ void main() => runApp(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const SizedBox(height: 24),
|
||||
const Text('Hearts and text rendering'),
|
||||
const Text('Hearts, warnings, and text'),
|
||||
const SizedBox(height: 24),
|
||||
const Text('Selected'),
|
||||
_reactions(true),
|
||||
@@ -34,7 +34,7 @@ void main() => runApp(
|
||||
const Text('Message text'),
|
||||
const SizedBox(height: 8),
|
||||
_message('Plain ❤ Emoji ❤️ Text ❤︎'),
|
||||
_message('**Bold ❤️** *Italic ❤️*'),
|
||||
_message('**Bold ❤️ ⚠️** *Italic ❤️ ⚠️*'),
|
||||
_message('Warnings: ⚠ ⚠️ ⚠︎'),
|
||||
const SizedBox(height: 16),
|
||||
const Text('Preserved symbols and text'),
|
||||
@@ -43,7 +43,7 @@ void main() => runApp(
|
||||
_message('Ελληνικά · Кириллица · Tiếng Việt'),
|
||||
const SizedBox(height: 16),
|
||||
const Text('Emoji-only message'),
|
||||
_message('❤️'),
|
||||
_message('❤️ ⚠️'),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -56,7 +56,7 @@ void main() => runApp(
|
||||
Widget _reactions(bool selected) => ReactionRow(
|
||||
messageId: 'heart-$selected',
|
||||
reactions: [
|
||||
for (final emoji in ['❤️', '👍', '🎉'])
|
||||
for (final emoji in ['❤️', '⚠️', '👍'])
|
||||
TimelineReaction(
|
||||
emoji: emoji,
|
||||
count: 5,
|
||||
|
||||
@@ -10,13 +10,13 @@ import sys
|
||||
from PIL import Image, ImageChops
|
||||
|
||||
|
||||
def red_pixels(image, box):
|
||||
def color_pixels(image, box, predicate):
|
||||
left, top, right, bottom = box
|
||||
return [
|
||||
(x, y)
|
||||
for y in range(top, bottom)
|
||||
for x in range(left, right)
|
||||
if is_red(image.getpixel((x, y)))
|
||||
if predicate(image.getpixel((x, y)))
|
||||
]
|
||||
|
||||
|
||||
@@ -25,6 +25,11 @@ def is_red(color):
|
||||
return red > 140 and red > green * 1.5 and red > blue * 1.5
|
||||
|
||||
|
||||
def is_yellow(color):
|
||||
red, green, blue = color
|
||||
return red > 180 and green > 130 and blue < 90
|
||||
|
||||
|
||||
def main():
|
||||
before, after = [Image.open(path).convert("RGB") for path in sys.argv[1:]]
|
||||
assert before.size == after.size == (1206, 2622), "Use the documented simulator and fixture"
|
||||
@@ -36,25 +41,35 @@ def main():
|
||||
# Native iOS fallback currently renders VS15 as a color heart too.
|
||||
"text-presentation message heart": (550, 980, 620, 1035),
|
||||
"bold message heart": (170, 1040, 220, 1090),
|
||||
"italic message heart": (355, 1040, 425, 1090),
|
||||
"italic message heart": (430, 1040, 490, 1090),
|
||||
"emoji-only message": (65, 1490, 125, 1545),
|
||||
}
|
||||
for name, box in regions.items():
|
||||
assert not red_pixels(before, box), f"{name}: original must reproduce monochrome heart"
|
||||
pixels = red_pixels(after, box)
|
||||
assert len(pixels) > 500, f"{name}: expected native red heart"
|
||||
if "reaction" in name:
|
||||
center = (min(y for _, y in pixels) + max(y for _, y in pixels)) / 2
|
||||
pill_center = (box[1] + box[3] - 1) / 2
|
||||
assert abs(center - pill_center) <= 1.5, f"{name}: heart is not centered"
|
||||
print(f"PASS: {name}")
|
||||
# The two preceding message lines gain one logical pixel each from the
|
||||
warnings = {
|
||||
"selected warning reaction": (260, 522, 325, 606),
|
||||
"unselected warning reaction": (260, 744, 325, 828),
|
||||
"plain message warning": (275, 1090, 335, 1150),
|
||||
"emoji-presentation message warning": (350, 1090, 415, 1150),
|
||||
"text-presentation message warning": (430, 1090, 490, 1150),
|
||||
"bold message warning": (230, 1040, 290, 1090),
|
||||
"italic message warning": (490, 1040, 550, 1090),
|
||||
"emoji-only warning": (130, 1490, 190, 1550),
|
||||
}
|
||||
for cases, predicate in ((regions, is_red), (warnings, is_yellow)):
|
||||
for name, box in cases.items():
|
||||
assert not color_pixels(before, box, predicate), f"{name}: original must reproduce monochrome glyph"
|
||||
pixels = color_pixels(after, box, predicate)
|
||||
assert len(pixels) > 500, f"{name}: expected native color emoji"
|
||||
if "reaction" in name:
|
||||
center = (min(y for _, y in pixels) + max(y for _, y in pixels)) / 2
|
||||
pill_center = (box[1] + box[3] - 1) / 2
|
||||
assert abs(center - pill_center) <= 1.5, f"{name}: emoji is not centered"
|
||||
print(f"PASS: {name}")
|
||||
# The three preceding message lines gain one logical pixel each from the
|
||||
# fallback font's metrics. Compare unchanged text after that translation.
|
||||
for name, box in {
|
||||
"warning signs": (65, 1090, 1000, 1145),
|
||||
"preserved symbols and languages": (65, 1260, 1100, 1385),
|
||||
}.items():
|
||||
shifted = (box[0], box[1] + 6, box[2], box[3] + 6)
|
||||
shifted = (box[0], box[1] + 9, box[2], box[3] + 9)
|
||||
difference = ImageChops.difference(before.crop(box), after.crop(shifted))
|
||||
assert difference.getbbox() is None, f"{name}: unexpected rendering change"
|
||||
print(f"PASS: {name} is pixel-identical")
|
||||
|
||||
Reference in New Issue
Block a user