mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-29 11:18:48 +00:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
95375e03d5 |
@@ -46,6 +46,29 @@ jobs:
|
||||
- name: Install pinned Chromium
|
||||
if: steps.pw.outputs.cache-hit != 'true'
|
||||
run: npx playwright install chromium
|
||||
# oxipng без потерь снимает с набора ~19% (замер в #345 на pyoxipng 9.1.1;
|
||||
# у 10.2.0 пресеты уровней перебалансированы, точная доля может отличаться,
|
||||
# но кадры остаются пиксельно идентичными в любой версии — это перепаковка).
|
||||
#
|
||||
# Пин версии и контрольной суммы намеренно, а не `apt-get install oxipng`:
|
||||
# пакет из образа раннера может пропасть или переехать, а падение шага
|
||||
# съёмки стоит целого цикла приёмки. Тот же урок, что с azure-зеркалом
|
||||
# Playwright (#175, #206).
|
||||
- name: Установить oxipng
|
||||
env:
|
||||
OXIPNG_VERSION: 10.2.0
|
||||
OXIPNG_SHA256: b33f84c73d42cb592bea5d84c431030b1e97784817693380dfcec7d9575f871e
|
||||
run: |
|
||||
set -euo pipefail
|
||||
asset="oxipng-${OXIPNG_VERSION}-x86_64-unknown-linux-gnu.tar.gz"
|
||||
curl -fsSL -o "$asset" \
|
||||
"https://github.com/oxipng/oxipng/releases/download/v${OXIPNG_VERSION}/${asset}"
|
||||
echo "${OXIPNG_SHA256} ${asset}" | sha256sum -c -
|
||||
mkdir -p "$HOME/.local/bin"
|
||||
tar -xzf "$asset" --strip-components=1 -C "$HOME/.local/bin" \
|
||||
"oxipng-${OXIPNG_VERSION}-x86_64-unknown-linux-gnu/oxipng"
|
||||
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
|
||||
"$HOME/.local/bin/oxipng" --version
|
||||
- name: Build the bundle the screenshots must come from
|
||||
run: npm run build
|
||||
- name: Capture
|
||||
@@ -58,16 +81,30 @@ jobs:
|
||||
# разницу рендеринга.
|
||||
- name: Вердикт
|
||||
run: |
|
||||
# Поле манифеста «до» — из закоммиченного состояния, «после» — из
|
||||
# свежего. Читается одинаково для браузера и для упаковщика: оба
|
||||
# переписывают все кадры сразу, и различить их причины обязан вердикт,
|
||||
# а не человек по памяти (#345).
|
||||
field() {
|
||||
node -e "let s='';process.stdin.on('data',d=>s+=d).on('end',()=>{try{console.log(JSON.parse(s).$1||'')}catch{console.log('')}})"
|
||||
}
|
||||
git status --porcelain docs/images
|
||||
changed=$(git diff --name-only docs/images | grep -c png || true)
|
||||
before=$(git show HEAD:docs/images/screenshots.json | node -e \
|
||||
"let s='';process.stdin.on('data',d=>s+=d).on('end',()=>{try{console.log(JSON.parse(s).chromium||'')}catch{console.log('')}})")
|
||||
before=$(git show HEAD:docs/images/screenshots.json | field chromium)
|
||||
after=$(node -e "console.log(require('./docs/images/screenshots.json').chromium)")
|
||||
packer_before=$(git show HEAD:docs/images/screenshots.json | field oxipng)
|
||||
packer_after=$(node -e "console.log(require('./docs/images/screenshots.json').oxipng || '')")
|
||||
echo "--- изменившихся PNG: $changed"
|
||||
echo "--- Chromium: было «${before:-не записан}», стало «$after»"
|
||||
if [ "$before" = "$after" ] && [ "$changed" -gt 0 ]; then
|
||||
echo "ВЕРДИКТ: тот же браузер, а картинки изменились — изменился продукт."
|
||||
echo "Смотрите на кадры: если изменение ожидаемое, принимайте."
|
||||
echo "--- oxipng: было «${packer_before:-не записан}», стало «${packer_after:-нет}»"
|
||||
if [ "$packer_before" != "$packer_after" ] && [ "$changed" -gt 0 ]; then
|
||||
echo "ВЕРДИКТ: изменился упаковщик, поэтому переписаны все кадры сразу."
|
||||
echo "Это перепаковка без потерь: пиксели те же, размер меньше на ~19%."
|
||||
echo "Ожидаемо один раз — при включении oxipng либо при смене его версии."
|
||||
echo "Проверить можно сравнением декодированных кадров, а не байтов."
|
||||
elif [ "$before" = "$after" ] && [ "$changed" -gt 0 ]; then
|
||||
echo "ВЕРДИКТ: тот же браузер и тот же упаковщик, а картинки изменились —"
|
||||
echo "изменился продукт. Смотрите на кадры: если изменение ожидаемое, принимайте."
|
||||
elif [ "$before" != "$after" ]; then
|
||||
echo "ВЕРДИКТ: браузер другой, поэтому переписаны все кадры сразу."
|
||||
echo "Это ожидаемо один раз — при переходе на канонический прогон."
|
||||
|
||||
+50
-2
@@ -1,5 +1,6 @@
|
||||
#!/usr/bin/env node
|
||||
import { createHash } from 'node:crypto';
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import { copyFileSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
||||
import { dirname, resolve } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
@@ -17,6 +18,47 @@ const INTEGRATION_BUNDLE = resolve(ROOT, 'custom_components/houseplan/frontend/h
|
||||
const SCRIPT = fileURLToPath(import.meta.url);
|
||||
const sha256 = (value) => createHash('sha256').update(value).digest('hex');
|
||||
|
||||
/**
|
||||
* Перепаковка кадра без потерь (#345).
|
||||
*
|
||||
* Замер на этом наборе: 2096 КБ превращаются в 1689 КБ, минус 19.4%, и все
|
||||
* десять кадров остаются ПИКСЕЛЬНО идентичными — декодированные RGBA совпадают
|
||||
* по sha256. Это выбор фильтров строки и уровня сжатия, а не квантование:
|
||||
* визуального решения здесь нет вовсе.
|
||||
*
|
||||
* Почему внутри съёмки, а не отдельным проходом по закоммиченным файлам.
|
||||
* Манифест хранит `imageSha256` каждого кадра, поэтому оптимизировать файлы в
|
||||
* репозитории руками нельзя — `check-docs` покраснеет; а если жать после
|
||||
* подсчёта хешей, следующая же съёмка вернёт неоптимизированные байты.
|
||||
*
|
||||
* Отсутствие инструмента не ошибка: локальная съёмка и без него полезна для
|
||||
* глаз, а приёмка всё равно идёт только из артефакта CI, где `oxipng` стоит
|
||||
* пином (`.github/workflows/docs-screenshots.yml`). Но молчать об этом нельзя —
|
||||
* байты кадра зависят от того, был ли инструмент, поэтому его версия попадает
|
||||
* в манифест рядом с версией браузера, по той же причине.
|
||||
*/
|
||||
const oxipngVersion = (() => {
|
||||
const probe = spawnSync('oxipng', ['--version'], { encoding: 'utf8' });
|
||||
if (probe.status !== 0) {
|
||||
console.log('oxipng не найден: кадры пишутся как есть, без перепаковки');
|
||||
return null;
|
||||
}
|
||||
return String(probe.stdout || '').trim().split('\n')[0];
|
||||
})();
|
||||
|
||||
/** Пожать файл на месте и вернуть его новые байты. */
|
||||
const shrinkPng = (path, before) => {
|
||||
if (!oxipngVersion) return before;
|
||||
const run = spawnSync('oxipng', ['-o', '4', '--strip', 'safe', '--quiet', path]);
|
||||
if (run.status !== 0) {
|
||||
throw new Error(`oxipng не смог обработать ${path}: код ${run.status}`
|
||||
+ `${run.stderr ? ` · ${run.stderr}` : ''}`);
|
||||
}
|
||||
const after = readFileSync(path);
|
||||
console.log(` ${(before.length / 1024).toFixed(0)} КБ -> ${(after.length / 1024).toFixed(0)} КБ`);
|
||||
return after;
|
||||
};
|
||||
|
||||
|
||||
const roomCardClip = (page) => page.evaluate(() => {
|
||||
const card = window.__goldenCard;
|
||||
@@ -129,14 +171,18 @@ try {
|
||||
const image = await page.screenshot({
|
||||
...(clip ? { clip } : {}), animations: 'disabled', caret: 'hide', scale: 'css',
|
||||
});
|
||||
writeFileSync(resolve(OUTPUT, scenario.file), image);
|
||||
const path = resolve(OUTPUT, scenario.file);
|
||||
writeFileSync(path, image);
|
||||
// Хеш считается ПОСЛЕ перепаковки: манифест обязан описывать те байты,
|
||||
// которые лежат на диске, иначе приёмка отвергнет свой же кандидат.
|
||||
const stored = shrinkPng(path, image);
|
||||
scenarios[scenario.id] = {
|
||||
file: scenario.file,
|
||||
viewport: scenario.viewport,
|
||||
theme: scenario.theme,
|
||||
language: scenario.language,
|
||||
sourceSha256: fingerprint,
|
||||
imageSha256: sha256(image),
|
||||
imageSha256: sha256(stored),
|
||||
};
|
||||
console.log(`captured ${scenario.id} -> docs/images/${scenario.file}`);
|
||||
}
|
||||
@@ -147,6 +193,8 @@ try {
|
||||
// Кто снимал. Смена браузера переписывает все картинки без содержательных
|
||||
// изменений (#246), поэтому окружение съёмки — часть доказательства.
|
||||
chromium: browser.version(),
|
||||
// Чем жали — тоже часть доказательства: без инструмента байты другие.
|
||||
oxipng: oxipngVersion,
|
||||
sourceFingerprint: fingerprint,
|
||||
captureScriptSha256: sha256(readFileSync(SCRIPT)),
|
||||
command: 'npm run build && node demo/docs/capture.mjs',
|
||||
|
||||
Reference in New Issue
Block a user