diff --git a/docs/specs/039-large-backdrops.md b/docs/specs/039-large-backdrops.md index 7d349a40..b83c48d4 100644 --- a/docs/specs/039-large-backdrops.md +++ b/docs/specs/039-large-backdrops.md @@ -2,7 +2,7 @@ - Issue: https://github.com/Matysh/houseplan-card/issues/39 - Приоритет: P3 (триаж владельца 2026-08-15), полный трек -- Ревизия: 2 (2026-08-29) — research-фаза проведена, константы предложены по бенчмарку +- Ревизия: 3 (2026-08-29) — по SPEC-REVIEW-39-r1: сценарий, i18n-ключи, риски, release-артефакты, двухфазный hard ## Цель @@ -10,6 +10,21 @@ память планшета/WebView, и предлагать уменьшенную копию — не трогая текущий план и оригинал файла при любой ошибке. +## Сценарий + +Владелец дома настраивает план с планшета или ноутбука: в диалоге пространства +выбирает «Файл» и указывает скан поэтажного плана — обычно это фото или скан +150–600 DPI, который «как есть» весит десятки мегапикселей. Сегодня такой выбор +молча вешает вкладку на слабом планшете (полный decode + тройная конвертация в +base64 до каких-либо проверок). + +**До**: выбрал большой файл → вкладка замирает или падает без объяснений; +план, который был на экране, можно потерять вместе с вкладкой. +**После**: выбрал большой файл → мгновенно появляется понятный диалог «Файл +очень большой (10000×10000, 58 МБ, потребует ~380 МБ памяти)» с кнопкой +«Загрузить уменьшенную копию»; одно нажатие — и на плане та же картинка, но +безопасного размера. Текущий план в любом исходе остаётся цел. + ## Текущее состояние (анкеры кода) `_pickPlanFile` (`houseplan-editor-runtime.ts:8243`) принимает PNG/JPEG/WebP/SVG @@ -76,15 +91,49 @@ fail-closed к диалогу, не к тихому продолжению. SVG - `warn` — диалог (`hp-dialog`, паттерн существующих подтверждений): разрешение, размер файла, оценка decoded-памяти; действия «Загрузить уменьшенную копию» (основное), «Оставить оригинал», «Отмена». Числа — из probe, до decode. -- `hard` (сторона > 16384, decode-ошибка или таймаут 10 с на этапе - уменьшения) — только «Отмена» + совет уменьшить файл на десктопе; текущая - подложка и staging не меняются. +- `hard` — ДВЕ фазы с разным моментом наступления и одним исходом + («ничего не изменилось»): + - **фаза 1, синхронная** (сторона > 16384 по заголовку, до какого-либо + decode): диалог `backdrop.too_large_*` с единственной кнопкой «Отмена» и + советом уменьшить файл на десктопе; + - **фаза 2, асинхронная** (пользователь выбрал «Уменьшенную копию», а decode + упал или не уложился в таймаут 10 с): warn-диалог закрывается, показывается + тост `backdrop.downscale_failed`; поле выбора файла сброшено, staging + чист — пользователь может выбрать «Оставить оригинал», повторив выбор + файла, либо уменьшить файл сам. Автоматического фолбэка на оригинал нет: + молча грузить то, от чего пользователь только что отказался, нечестно. - Уменьшение: `createImageBitmap(file, {imageOrientation: 'from-image'})` (EXIF учтён) → OffscreenCanvas (fallback ``), aspect сохраняется; alpha из probe: PNG/WebP c alpha → PNG, opaque → JPEG q0.9 (WebP-энкод не берём — Safari не пишет). Уменьшенный Blob идёт тем же путём planFile → upload → квота/copy-on-write, что и оригинал. -- Тексты en/ru/de; диалог на мобильной ширине без горизонтального скролла. +- Диалог на мобильной ширине без горизонтального скролла. + +## i18n (ключи en/ru; de — перевод тех же ключей) + +- `backdrop.large_title` — "Large image" / «Большое изображение»; +- `backdrop.large_body` — "This image is {w}×{h} ({fileMb} MB file) and needs + about {decodedMb} MB of memory to display. On tablets this can crash the + page." / «Изображение {w}×{h} ({fileMb} МБ), для показа потребуется около + {decodedMb} МБ памяти. На планшетах это может привести к падению страницы.»; +- `backdrop.unknown_body` — "The image dimensions could not be read — the file + may be damaged. Continuing may crash the page on tablets." / «Не удалось + прочитать размеры изображения — файл может быть повреждён. Продолжение может + привести к падению страницы на планшете.»; +- `backdrop.use_downscaled` — "Upload a reduced copy" / «Загрузить уменьшенную + копию»; +- `backdrop.keep_original` — "Keep the original" / «Оставить оригинал»; +- `backdrop.too_large_title` — "Image is too large" / «Изображение слишком + большое»; +- `backdrop.too_large_body` — "This image exceeds what browsers can display + ({w}×{h}, limit {limit} px per side). Please reduce it in a desktop editor + and upload again." / «Изображение превышает возможности браузера ({w}×{h}, + предел {limit} px по стороне). Уменьшите его в редакторе на компьютере и + загрузите снова.»; +- `backdrop.downscale_failed` — "Could not create the reduced copy. The + original plan was not changed." / «Не удалось создать уменьшенную копию. + Текущий план не изменён.»; +- `toast.plan_formats` — существующий, без изменений. ## Транзакция (без изменений, закрепляется тестом) @@ -103,8 +152,11 @@ issue = оригинальный ФАЙЛ пользователя никогд 2. «Уменьшенная копия»: длинная сторона 4096, aspect сохранён; PNG с alpha → PNG (alpha жива), opaque → JPEG; результат уходит существующим upload-путём. 3. «Оставить оригинал» — прежнее поведение байт-в-байт. -4. `hard`: сторона >16384 либо decode-fail/таймаут — только Отмена; текущая - подложка, staging и конфиг не изменились. +4. `hard` фаза 1: сторона >16384 из заголовка — диалог «слишком большое» с + одной «Отменой», без decode; подложка/staging/конфиг не изменились. +4б. `hard` фаза 2: decode-fail или таймаут 10 с после выбора «Уменьшенной + копии» — тост `backdrop.downscale_failed`, staging чист, подложка/конфиг не + изменились, повторный выбор файла работает. 5. SVG: только прежняя валидация, растеризации нет. 6. Битые/усечённые заголовки (PNG без IHDR, JPEG без SOF, WebP-огрызок) → `unknown` → warn-диалог без чисел; продолжение возможно только явным выбором. @@ -126,6 +178,33 @@ window.createImageBitmap). Мутанты реестра: (1) probe всегда `hard` понижен до warn — смок красный; (4) staging не чистится при отказе — тест транзакции красный. +## Риски + +- **Пороги не с reference-планшета**: калибровка десктопная с консервативным + запасом; ложные срабатывания дешевы (лишний диалог), пропуски дороже — при + полевых жалобах правится один модуль констант. Смягчение: числа печатаются в + диалоге, жалобу легко диагностировать по скриншоту. +- **Зоопарк заголовков** (прогрессивный JPEG, WebP VP8L, EXIF в необычном + месте): парсер обязан отвечать `unknown`, не бросать — fail-closed к + warn-диалогу; таблица юнит-фикстур покрывает все подвиды из спецификаций + форматов. +- **OffscreenCanvas недоступен** (старые WebView): фолбэк на `` в том + же кадре; ветка покрыта юнитом через подмену глобала. +- **Regress существующего upload-пути**: safe-путь меняет только механизм + base64 (паритет-юнит AC9) — поведенчески байт-в-байт. + +## Release-артефакты + +- CHANGELOG.md + CHANGELOG.ru.md — одна запись (User-Visible: yes); +- docs/BACKDROP.md — раздел «Большие изображения» (пороги, поведение, совет); +- docs/USER-GUIDE.md + USER-GUIDE.ru.md — абзац в теме подложек; +- docs/TESTING.md — чек-лист сценариев (warn/уменьшение/hard×2/SVG); +- i18n en/ru/de — ключи из раздела i18n (паритет-тесты стерегут); +- golden: не задевается (диалог не входит в golden-матрицу; скриншоты + документации пересъёмка capture по общему правилу src-правок); +- performance: бенчмарк-скрипт `demo/benchmark_backdrop_decode.mjs` в repo, + в перф-джобу CI НЕ добавляется (разовая калибровка, не регресс-гейт). + ## Вне скоупа Автопревращение SVG в растр; серверная перекодировка; изменение