Встроенный emoji-шрифт (Этап 28): субсет Noto Color Emoji (COLRv1, OFL) как base64 @font-face 'EmojiEmbedded' в index.html — 57 глифов (все emoji игры), 78 КБ woff2, +105 КБ base64 (155→260 КБ; публикуемый zip 110.9 КБ, лимит 1.22 — 100 МБ). Внешний .woff2 невозможен: pack.mjs публикует строго один index.html. Шрифт ПЕРВЫМ в font-stack html,body и во всех 10 ctx.font (константа UI_FONT, секция «ОТРИСОВКА») — единый Google-стиль emoji на всех платформах; headless-скриншоты больше не зависят от системных emoji-шрифтов. Первый кадр ждёт document.fonts.load('16px "EmojiEmbedded"') с timeout-фолбэком 1.5 с (canvas-emoji 💥🌀⚡💪🐟💤). tools/emoji-font.py — генератор: вытаскивает emoji-кодопоинты из index.html (одна точка истины) → fontTools.subset (woff2, без SVG-таблицы) → вписывает base64 между маркерами EMOJI-FONT:BEGIN/END; --check — регрессия (падает, если новый emoji не в субсете; ✓№●○↩ в NCE нет — ок). venv tools/.venv + исходник tools/fonts-src/ — в gitignore. Осознанно: ⚡⭐☀⚠⏸⚖⛵⚒ теперь цветные на всех платформах (унификация). Тесты §Y7 (164/164, exit 0), check.mjs OK, pack 5/5, pack-smoke 6/6, emoji-font.py --check OK; рендер-пробы canvas 🪙⚠️⏸⭐ — цветные пиксели. screenshots/ — 14 кадров пересняты (среда: системный шрифт Emoji One — на кадрах встроенный Google-стиль). Доки: AGENTS.md (раздел «Встроенный emoji-шрифт», 164 чека), DEVLOG Этап 28.
@@ -2,6 +2,10 @@
|
||||
orca-*.png
|
||||
tests/shot-*.png
|
||||
|
||||
# Встроенный emoji-шрифт (tools/emoji-font.py): исходник Noto Color Emoji и venv с fonttools
|
||||
tools/fonts-src/
|
||||
tools/.venv/
|
||||
|
||||
# Служебное
|
||||
.po-handoff.md
|
||||
*.log
|
||||
|
||||
@@ -11,13 +11,13 @@
|
||||
npm install # puppeteer-core
|
||||
npm test # = node test.js
|
||||
```
|
||||
Ждать все PASS и exit 0. Сейчас проверок 162 (включая §Y5 i18n и §Y6 паузу).
|
||||
Ждать все PASS и exit 0. Сейчас проверок 164 (включая §Y5 i18n, §Y6 паузу и §Y7 шрифт).
|
||||
3. Браузерный плейтест (вкладка встроенного браузера Orca, через Orca CLI):
|
||||
```bash
|
||||
node tests/orca-playtest.js # из каталога репо; требует ЗАПУЩЕННЫЙ Orca app и orca-ide (или ORCA_CLI_COMMAND)
|
||||
```
|
||||
Проходит тот же геймплей «вживую» в браузере (не headless) и пишет `orca-*.png` в каталог запуска. Если Orca app не запущен — пропустить и НЕ блокировать (главный рубеж — пункт 2). Подробности и подводные камни: `docs/BROWSER-TESTING.md`.
|
||||
4. Скриншоты: `node tests/shots.js` пишет `shot-*.png` в каталог запуска (запускать из `tests/`); README ссылается на `screenshots/` в корне — свежие скопировать туда. В headless нужен шрифт emoji (Noto Color Emoji), иначе в скриншотах квадраты.
|
||||
4. Скриншоты: `node tests/shots.js` пишет `shot-*.png` в каталог запуска (запускать из `tests/`); README ссылается на `screenshots/` в корне — свежие скопировать туда. Emoji рисует встроенный шрифт (раздел «Встроенный emoji-шрифт») — системные emoji-шрифты для скриншотов больше не нужны.
|
||||
5. Публикация в Яндекс Играх: `node tools/pack.mjs` (сборка dist/fishing-yandex.zip + самопроверка 5 пунктов) → `node tests/pack-smoke.js` (smoke играбельности КОПИИ из архива, 6 чек) → загрузить zip в консоль. Доки: раздел README «Публикация в Яндекс Играх», запись Этап 27 в DEVLOG.
|
||||
|
||||
## Подводные камни
|
||||
@@ -44,6 +44,7 @@
|
||||
- Реклама (Y2): константы `AD_FIRST_MS`/`AD_COOLDOWN_MS` (секция Y1), точка interstitial — `tryInterstitial()` в хендлере `btnCardOk` (гейт: первый показ ≥2 мин сессии, кулдаун 3 мин; `lastAdTs` — осознанно «попытка», не показ — при троттлинге платформы wasShown=false кулдаун сгорает), RV-кнопка `#btnRv` «📺 Реклама: ×2 за этот улов» на карточке улова (`showCard` сбрасывает `rvVal`/`rvDone`), методы `YG.showFullscreen/showRewarded` (+watchdog 90 с), `endAd()`; карточка улова БЕЗ `gameplayStop()` — осознанно (часть цикла улова); реклама не персистится в сейв (только `persist()` перед показом и после бонуса).
|
||||
- i18n (треб. 2.14/2.2): таблица `T={ru,en}` (единая точка строк UI), `t(key)`/`tt(key,o)`, `LANG` (дефолт `ru`), `forceLang` (тест-форс), `setLang()` (обновляет `<html lang>`/`<title>` + пере-рендер меню/статичного UI и открытых оверлеев — магазина/карточки), `resolveLang(ysdk)` — резолюция: `forceLang` → `ysdk.environment.i18n.lang` → `ru`; стаб (file:// без SDK) — всегда `ru` (`navigator.language` НЕ используется — ломает headless-тесты). `trName`/`trDesc`/`trRarity` — выбор по `nameEn`/`descEn`/`RARITY_NAMES_EN` в таблицах данных. Меню — `renderMenu()` (device-aware: тач/мышь, `IS_TOUCH`). EN-ветка `questText()` (RU-ветка с `pluralN` не меняется).
|
||||
- Пауза (треб. 6.3/1.3): HUD-кнопка `#btnPause` (⏸) → `openPause()` (гарды MENU/uiOpen/adShowing/pauseOpen; `YG.onPause()` + оверлей `#pause`); `#btnPauseResume` → `closePause()`, `#btnPauseMenu` → MENU (с `YG.onResume()`). Гард `pauseOpen` в `YG.onResume` (после `adShowing`) — blur/focus/visibilitychange во время открытой паузы не снимают паузу (снимут только `closePause()`/«В меню»). `window blur` → `YG.onPause()`, `focus` → `YG.onResume()` (треб. 1.3, дополнение к visibilitychange). Видимость `#btnPause` и `#btnRecall` — в `syncDom()` (вызывается каждый кадр в `frame()` вне `YG.paused` — без этого `#btnPause` «залипает» видимым за оверлеями).
|
||||
- Встроенный emoji-шрифт (Э28): все emoji UI и canvas рисует вшитый шрифт — субсет Noto Color Emoji (COLRv1, OFL) как base64 `@font-face font-family:'EmojiEmbedded'` в index.html между маркерами `<!-- EMOJI-FONT:BEGIN/END -->` (внешний .woff2 невозможен: публикуется только один index.html). В font-stack `html,body` и во всех `ctx.font` (константа `UI_FONT`, секция «ОТРИСОВКА») стоит ПЕРВЫМ — единый Google-стиль emoji на всех платформах; headless-скриншоты не зависят от системных шрифтов. Первый кадр ждёт `document.fonts.load('16px "EmojiEmbedded"')` с timeout-фолбэком 1.5 с. Генератор `tools/emoji-font.py` (venv: `python3 -m venv tools/.venv && tools/.venv/bin/pip install fonttools brotli`; исходник `tools/fonts-src/NotoColorEmoji-Regular.ttf` из google/fonts, всё в gitignore): `python3 tools/emoji-font.py` — перегенерация по текущим emoji index.html (ОБЯЗАТЕЛЬНО при добавлении новых emoji), `--check` — падает, если субсет не покрывает index.html (✓ № ● ○ ↩ — в NCE их нет, это ок, рисуются системными шрифтами). Тест §Y7 (tests/test.js) гоняет `document.fonts.check('16px "EmojiEmbedded"','🪙')`; семейство переименовывать нельзя (тест + @font-face + UI_FONT).
|
||||
|
||||
## Запуск и публикация
|
||||
|
||||
|
||||
@@ -542,3 +542,28 @@ git add -A && git commit -m "что изменилось" && git push
|
||||
**Заметки (осознанные решения и ловушки):**
|
||||
- Самопроверка распаковывает встроенным zip-ридером, а не `unzip` — у скрипта ноль бинарных зависимостей помимо самого сборщика (`zip` → fallback `python3`).
|
||||
- python3-фолбэк тоже deflate (проверено на Python 3.12) — встроенный ридер самопроверки понимает и stored, и deflate; по размеру разница несущественна (40.6 против 40.7 KB).
|
||||
|
||||
## Этап 28. Встроенный emoji-шрифт (субсет Noto Color Emoji)
|
||||
|
||||
**Проблема:** все emoji игры (UI-строки, таблицы данных, canvas `fillText` — 💥🌀⚡💪🐟💤) рисовались системным шрифтом платформы. На системах без emoji-шрифта (headless Linux) — квадраты/монохром (известный подводный камень для скриншотов), а вид emoji разнился между платформами (Segoe/Apple/Android). Публикация в Яндекс Игры — строго один `index.html` (треб. 1.22, `tools/pack.mjs`), поэтому внешний `.woff2` невозможен — шрифт встраивается в HTML.
|
||||
|
||||
**Что сделано:**
|
||||
|
||||
1. **`tools/emoji-font.py`** (Python; dev-зависимости fonttools+brotli в `tools/.venv`) — генератор встроенного шрифта: вытаскивает все emoji-кодопоинты из `index.html` (одна точка истины — сам файл игры; диапазонные правила по Unicode-категориям + явные одиночные: стрелки, U+FE0F), строит субсет через `fontTools.subset` (woff2, без hinting; legacy-таблица SVG отбрасывается — цвет только через COLRv1) и вписывает base64 `@font-face` в `index.html` между маркерами `<!-- EMOJI-FONT:BEGIN/END -->`. Режим `--check` — регрессия: падает с листом недостающих кодопоинтов, если в `index.html` появился emoji, которого нет в субсете (требует только пересечение с cmap исходника — см. заметки).
|
||||
2. **index.html** — блок `@font-face font-family:'EmojiEmbedded'` (57 глифов, 78 КБ woff2 → +105 КБ base64; index.html 155→260 КБ). Шрифт стоит ПЕРВЫМ в font-stack `html,body` и во всех 10 `ctx.font` (константа `UI_FONT` в секции «ОТРИСОВКА») — единый Google-стиль emoji на всех платформах. Перед первым кадром — `document.fonts.load('16px "EmojiEmbedded"')` с timeout-фолбэком 1.5 с (canvas-emoji рисуются загруженным шрифтом; data: URI грузится за миллисекунды, фолбэк страхует от «зависания» первого кадра на file://).
|
||||
3. **tests/test.js** — секция §Y7: @font-face зарегистрирован в `document.fonts` + `document.fonts.check('16px "EmojiEmbedded"','🪙')` (шрифт реально резолвит glyph, а не молча падает в системный фоллбек).
|
||||
4. **`.gitignore`** — `tools/fonts-src/` (исходник Noto Color Emoji, 25 МБ) и `tools/.venv/`.
|
||||
|
||||
**Почему Noto Color Emoji COLRv1 (2022+):** в игре есть emoji из Emoji 15.0 (2022) — 🪙 U+1FA99, 🪱 U+1FAB1, 🪝 U+1FA9D; старые версии шрифта их не знают. COLRv1 поддерживается всеми актуальными браузерами (Chrome/Firefox/Safari с 2023–2024).
|
||||
|
||||
**Результат прогона:** `python3 tools/emoji-font.py` → 57 глифов, woff2 78 КБ; `python3 tools/emoji-font.py --check` — OK; `node tools/pack.mjs` — 5 PASS, zip 110.9 КБ (было ~40 КБ до шрифта; лимит требования 1.22 — 100 МБ).
|
||||
|
||||
**Тесты:** `node tools/check.mjs` — OK; `cd tests && npm test` — **164/164 PASS, exit 0** (+2 новых §Y7); `node tests/pack-smoke.js` — **6/6**. Рендер-пробы (headless, canvas `fillText`): 🪙/⚠️/⏸/⭐ шрифтом «EmojiEmbedded» — цветные пиксели (не фоллбек).
|
||||
|
||||
**Визуальная проверка:** все 14 кадров `node tests/shots.js` пересняты, `screenshots/` обновлены. Среда чекаута — системный emoji-шрифт Emoji One (не Noto): на кадрах виден именно встроенный Google-стиль (🎣🖱️ в меню, ⚡🐟💪 на канвасе вываживания, 🐠🪙🏅📺 в карточке, 🐋 у босса, 🌧 в HUD-пилюле, 🏆 в тосте ачивки) — доказательство, что рисуется встроенный шрифт, а не системный.
|
||||
|
||||
**Заметки (осознанные решения и ловушки):**
|
||||
- ✓ (U+2713), №, ●, ○, ↩ — в Noto Color Emoji их нет (текстовые глифы) — рисуются системными шрифтами, как и раньше; `--check` это учитывает (требует только `wanted ∩ cmap(исходник)`).
|
||||
- ⚡ ⭐ ☀ ⚠ ⏸ ⚖ ⛵ ⚒ стали цветными emoji на всех платформах (на части систем раньше — монохромные) — осознанная унификация вида.
|
||||
- U+FE0F (variation selector, 18 вхождений в «⚠️») в субсете нет: NCE мапит U+26A0 прямо в цветной глиф, FE0F остаётся нулевым формат-знаком — «⚠️» рисуется цветным (рендер-проба).
|
||||
- Перегенерация субсета обязательна при добавлении новых emoji в index.html: `python3 tools/emoji-font.py` (реэкз в `tools/.venv`-интерпретатор встроен в скрипт); `--check` — защита от «добавил emoji, забыл перегенерировать».
|
||||
|
||||
|
Before Width: | Height: | Size: 138 KiB After Width: | Height: | Size: 138 KiB |
|
Before Width: | Height: | Size: 360 KiB After Width: | Height: | Size: 336 KiB |
|
After Width: | Height: | Size: 346 KiB |
|
Before Width: | Height: | Size: 314 KiB After Width: | Height: | Size: 284 KiB |
|
Before Width: | Height: | Size: 134 KiB After Width: | Height: | Size: 131 KiB |
|
Before Width: | Height: | Size: 468 KiB After Width: | Height: | Size: 454 KiB |
|
Before Width: | Height: | Size: 135 KiB After Width: | Height: | Size: 142 KiB |
|
Before Width: | Height: | Size: 440 KiB After Width: | Height: | Size: 447 KiB |
|
Before Width: | Height: | Size: 152 KiB After Width: | Height: | Size: 144 KiB |
|
Before Width: | Height: | Size: 157 KiB After Width: | Height: | Size: 146 KiB |
|
Before Width: | Height: | Size: 250 KiB After Width: | Height: | Size: 255 KiB |
|
Before Width: | Height: | Size: 508 KiB After Width: | Height: | Size: 507 KiB |
|
Before Width: | Height: | Size: 139 KiB After Width: | Height: | Size: 138 KiB |
|
Before Width: | Height: | Size: 160 KiB After Width: | Height: | Size: 166 KiB |
@@ -2472,6 +2472,23 @@ const sleep = ms => new Promise(r => setTimeout(r, ms));
|
||||
y6end.st === 'MENU' && y6end.menu === true && y6end.po === false && y6end.paused === false,
|
||||
JSON.stringify(y6end));
|
||||
|
||||
// --- Y7. Встроенный emoji-шрифт (этап 28) ---
|
||||
// Шрифт вшит в index.html как base64 @font-face (субсет Noto Color Emoji, tools/emoji-font.py).
|
||||
// Проверка: face зарегистрирован и реально резолвит glyph (не молча падает в системный фоллбек).
|
||||
const y7 = await page.evaluate(async () => {
|
||||
const loaded = (document.fonts&&document.fonts.load)
|
||||
? await Promise.race([document.fonts.load('16px "EmojiEmbedded"').then(()=>true).catch(()=>false),
|
||||
new Promise(r=>setTimeout(()=>r(false),3000))])
|
||||
: false;
|
||||
const check = document.fonts ? document.fonts.check('16px "EmojiEmbedded"','🪙') : false;
|
||||
const families = document.fonts ? [...document.fonts].map(f=>f.family) : [];
|
||||
return { loaded, check, families };
|
||||
});
|
||||
ok('Y7 шрифт: @font-face "EmojiEmbedded" зарегистрирован в document.fonts',
|
||||
y7.families.includes('EmojiEmbedded'), JSON.stringify(y7));
|
||||
ok('Y7 шрифт: document.fonts.check("16px EmojiEmbedded","🪙") — встроенный шрифт резолвит glyph',
|
||||
y7.loaded === true && y7.check === true, JSON.stringify(y7));
|
||||
|
||||
// --- 11. Ошибки консоли ---
|
||||
ok('нет ошибок JS (pageerror)', pageErrors.length === 0, pageErrors.join(' | '));
|
||||
ok('нет ошибок консоли', consoleErrors.length === 0, consoleErrors.join(' | '));
|
||||
|
||||
@@ -0,0 +1,185 @@
|
||||
#!/usr/bin/env python3
|
||||
# Встроенный emoji-шрифт игры (этап 28).
|
||||
#
|
||||
# Публикуется ТОЛЬКО index.html (см. tools/pack.mjs), поэтому шрифт нельзя
|
||||
# отдавать отдельным файлом — он встраивается в index.html как base64 @font-face
|
||||
# между маркерами EMOJI-FONT:BEGIN/END.
|
||||
#
|
||||
# Исходник: Noto Color Emoji (COLRv1, OFL 1.1) — tools/fonts-src/NotoColorEmoji-Regular.ttf
|
||||
# (COLRv1/2022+ обязателен: в игре есть emoji из Emoji 15.0 — 🪙🪱🪝).
|
||||
# Субсет строится ТОЛЬКО по emoji, реально использованным в index.html
|
||||
# (одна точка истины — сам файл игры; при добавлении новых emoji перегенерировать).
|
||||
#
|
||||
# Использование:
|
||||
# tools/.venv/bin/python tools/emoji-font.py # перегенерировать субсет и вписать в index.html
|
||||
# tools/.venv/bin/python tools/emoji-font.py --check # только проверка: субсет покрывает все emoji index.html
|
||||
#
|
||||
# Если fonttools не установлен:
|
||||
# python3 -m venv tools/.venv && tools/.venv/bin/pip install fonttools brotli
|
||||
import io
|
||||
import os
|
||||
import pathlib
|
||||
import re
|
||||
import sys
|
||||
import unicodedata
|
||||
|
||||
ROOT = pathlib.Path(__file__).resolve().parent.parent
|
||||
HTML = ROOT / 'index.html'
|
||||
FONT_SRC = pathlib.Path(__file__).resolve().parent / 'fonts-src' / 'NotoColorEmoji-Regular.ttf'
|
||||
BEGIN = '<!-- EMOJI-FONT:BEGIN'
|
||||
END = '<!-- EMOJI-FONT:END -->'
|
||||
FONT_FAMILY = 'EmojiEmbedded'
|
||||
|
||||
# Явные одиночные кодопоинты, которые не попадают в диапазонные правила ниже
|
||||
# (стрелки/знаки, используемые в UI: ↩ и variation selector).
|
||||
EXTRA_CPS = {0x2194, 0x2195, 0x2196, 0x2197, 0x2198, 0x2199, 0x21A9, 0x21AA,
|
||||
0x21AB, 0x21BA, 0x21BB, 0x21C4, 0x21AC, 0x203C, 0x2049, 0x203D,
|
||||
0x200D, 0xFE0F}
|
||||
|
||||
|
||||
def is_emoji_cp(cp: int) -> bool:
|
||||
c = chr(cp)
|
||||
if unicodedata.category(c) == 'So':
|
||||
return True
|
||||
if 0x1F000 <= cp <= 0x1FAFF:
|
||||
return True
|
||||
if 0x2600 <= cp <= 0x27BF: # Miscellaneous Symbols + Dingbats (⚠⚡⚖⛵⚒✓✨❓…)
|
||||
return True
|
||||
if 0x2300 <= cp <= 0x23FF: # технические (⏸…)
|
||||
return True
|
||||
if 0x2B00 <= cp <= 0x2BFF: # ⭐⬆…
|
||||
return True
|
||||
if 0x25A0 <= cp <= 0x25FF: # геометрические (●○; в NCE их нет — субсет их отбросит)
|
||||
return True
|
||||
return cp in EXTRA_CPS
|
||||
|
||||
|
||||
def emoji_codepoints(text: str) -> set:
|
||||
return {ord(c) for c in set(text) if ord(c) > 0x7F and (is_emoji_cp(ord(c)) or ord(c) == 0xFE0F)}
|
||||
|
||||
|
||||
def load_font(path):
|
||||
from fontTools.ttLib import TTFont
|
||||
return TTFont(path) # путь или file-объект (BytesIO)
|
||||
|
||||
|
||||
def font_cmap(font) -> set:
|
||||
return set(font.getBestCmap().keys())
|
||||
|
||||
|
||||
def subset_font(src_path: str, text: str):
|
||||
"""Возвращает (bytes woff2, set кодопоинтов финального cmap)."""
|
||||
from fontTools import subset as ft_subset
|
||||
font = load_font(src_path)
|
||||
opts = ft_subset.Options()
|
||||
opts.flavor = 'woff2'
|
||||
opts.hinting = False
|
||||
opts.legacy_kerning = False
|
||||
opts.notdef_outline = True
|
||||
# legacy-таблицу SVG не нёс: цвет — только через COLRv1 (SVG требует lxml в subsetter)
|
||||
opts.drop_tables = list(opts.drop_tables) + ['SVG']
|
||||
sub = ft_subset.Subsetter(opts)
|
||||
sub.populate(text=text)
|
||||
sub.subset(font)
|
||||
buf = io.BytesIO()
|
||||
font.save(buf)
|
||||
return buf.getvalue(), font_cmap(font)
|
||||
|
||||
|
||||
def ranges_css(cps: set) -> str:
|
||||
"""Набор кодопоинтов → компактный unicode-range (диапазоны U+A-B или одиночные U+A)."""
|
||||
xs = sorted(cps)
|
||||
parts, start, prev = [], xs[0], xs[0]
|
||||
for x in xs[1:]:
|
||||
if x == prev + 1:
|
||||
prev = x
|
||||
continue
|
||||
parts.append(f'U+{start:X}' if start == prev else f'U+{start:X}-U+{prev:X}')
|
||||
start = prev = x
|
||||
parts.append(f'U+{start:X}' if start == prev else f'U+{start:X}-U+{prev:X}')
|
||||
return ','.join(parts)
|
||||
|
||||
|
||||
def embedded_base64(html: str) -> str:
|
||||
m = re.search(re.escape(BEGIN) + r'.*?base64,([A-Za-z0-9+/=]+).*?' + re.escape(END), html, re.S)
|
||||
if not m:
|
||||
sys.exit(f'check: в index.html не найден блок {BEGIN}…{END} с base64 (сначала сгенерируй)')
|
||||
return m.group(1)
|
||||
|
||||
|
||||
def main():
|
||||
check_only = '--check' in sys.argv[1:]
|
||||
try:
|
||||
import fontTools # noqa: F401
|
||||
except ImportError:
|
||||
# если есть venv проекта и мы в нём не запущены — перезапускаемся под ним
|
||||
venv_py = pathlib.Path(__file__).resolve().parent / '.venv' / 'bin' / 'python'
|
||||
if venv_py.exists() and os.path.abspath(sys.executable) != str(venv_py.resolve()):
|
||||
os.execv(str(venv_py), [str(venv_py)] + sys.argv)
|
||||
sys.exit('fonttools не найден: python3 -m venv tools/.venv && tools/.venv/bin/pip install fonttools brotli')
|
||||
|
||||
if not FONT_SRC.exists():
|
||||
sys.exit(f'нет исходника шрифта: {FONT_SRC} (скачай NotoColorEmoji-Regular.ttf из репо google/fonts, ofl/notocoloremoji/)')
|
||||
|
||||
html = HTML.read_text(encoding='utf-8')
|
||||
wanted = emoji_codepoints(html)
|
||||
full_cmap = font_cmap(load_font(FONT_SRC))
|
||||
# Требование к субсету: всё, что есть и в index.html, и в исходнике.
|
||||
# (✓ № ● ○ ↩ и т.п. в NCE отсутствуют — их рисуют системные шрифты, это ок.)
|
||||
required = wanted & full_cmap
|
||||
|
||||
if check_only:
|
||||
import base64
|
||||
data = base64.b64decode(embedded_base64(html))
|
||||
have = font_cmap(load_font(io.BytesIO(data)))
|
||||
missing = sorted(required - have)
|
||||
extra = sorted(have - wanted)
|
||||
if missing:
|
||||
print('CHECK FAIL: в index.html есть emoji, которых нет во встроенном субсете:')
|
||||
for cp in missing:
|
||||
print(f' U+{cp:04X} {chr(cp)}')
|
||||
print('Перегенерируй: tools/.venv/bin/python tools/emoji-font.py')
|
||||
sys.exit(1)
|
||||
print(f'CHECK OK: субсет ({len(data)//1024} КБ в base64-блоке) покрывает все {len(required)} emoji index.html')
|
||||
if extra:
|
||||
print(f' (лишние в субсете, не в index.html: {", ".join(f"U+{cp:04X}" for cp in extra)})')
|
||||
return
|
||||
|
||||
text = ''.join(chr(cp) for cp in sorted(wanted))
|
||||
data, have = subset_font(str(FONT_SRC), text)
|
||||
missing = sorted(required - have)
|
||||
if missing:
|
||||
print('Генерация: часть требуемых кодопоинтов не вошла в субсет (нет в исходнике?):')
|
||||
for cp in missing:
|
||||
print(f' U+{cp:04X} {chr(cp)}')
|
||||
sys.exit(1)
|
||||
|
||||
import base64
|
||||
b64 = base64.b64encode(data).decode('ascii')
|
||||
block = (
|
||||
f'{BEGIN} — генерируется tools/emoji-font.py, не редактировать руками -->\n'
|
||||
'<style>\n'
|
||||
'@font-face{\n'
|
||||
f"font-family:'{FONT_FAMILY}';\n"
|
||||
'font-style:normal;\n'
|
||||
'font-weight:400;\n'
|
||||
'font-display:block;\n'
|
||||
f'src:url(data:font/woff2;base64,{b64}) format(\'woff2\');\n'
|
||||
f'unicode-range:{ranges_css(have & required)};\n'
|
||||
'}\n'
|
||||
'</style>\n'
|
||||
f'{END}'
|
||||
)
|
||||
if re.search(re.escape(BEGIN) + r'.*?' + re.escape(END), html, re.S):
|
||||
html = re.sub(re.escape(BEGIN) + r'.*?' + re.escape(END), lambda _m: block, html, flags=re.S)
|
||||
else:
|
||||
html = html.replace('<title>Рыбалка</title>', f'<title>Рыбалка</title>\n{block}', 1)
|
||||
if '<title>Рыбалка</title>' not in html:
|
||||
sys.exit('не удалось вставить блок: не найден <title>Рыбалка</title> и нет маркеров EMOJI-FONT')
|
||||
HTML.write_text(html, encoding='utf-8')
|
||||
print(f'OK: субсет {len(have)} глифов → woff2 {len(data)//1024} КБ → index.html +{(len(b64)//1024)} КБ base64')
|
||||
print(f' кодопоинты: {", ".join(f"U+{cp:04X}" for cp in sorted(have & required))}')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||