Meridian Office

Meridian PDF — просмотр и редактирование PDF

Документ приложения по соглашениям раздела 8 библии (docs/00-foundation.md). Версия 0.1 · 2026-10-03 · фаза 4 · редакции: просмотр, печать, заполнение форм и аннотации-комментарии — Free; редактирование, OCR, конверсия, защита, подписи — Home и выше; ГОСТ-подписи, политики — Enterprise. Цвет приложения --app-pdf #C92A2A, значок — лист с загнутым углом, формат — стандартный .pdf (своего формата нет), код ядра — kernel/crates/meridian-pdf-*/, оболочка — shell/src/pdf/.


1. Назначение и аналоги

1.1 Назначение

Meridian PDF — приложение для всего жизненного цикла PDF: быстро открыть и прочитать, найти, прокомментировать, заполнить и подписать форму, подписать цифровой подписью, поправить текст и страницы, распознать скан, преобразовать в Write/Sheets/Slides, удалить конфиденциальное навсегда, защитить, привести к PDF/A и ужать. Оно же — движок PDF для всего пакета: экспорт из Write/Sheets/Slides/Draw/Paint проходит через те же крейты (meridian-pdf-write), предпросмотр PDF в Hub и Mail — через тот же рендер.

Роль Как запускается Что умеет
Просмотрщик по умолчанию ассоциация .pdf, из Hub, из Mail (вложение) просмотр, поиск, печать, заполнение форм, комментарии (Free)
Редактор «Редактировать» в просмотрщике всё из раздела 2 (Home+)
Встроенный предпросмотр Hub «Быстрый просмотр», Mail, Notes (вырезки PDF) рендер страниц без интерфейса приложения (PdfView QML-компонент)
Библиотека ядра Write «Экспорт PDF/A», Print «Печать в PDF», Base/Forms «Отчёт в PDF» писатель PDF, PDF/A, PDF/UA, шифрование, подпись

1.2 Аналоги

Продукт Сильные стороны Слабые стороны (для нас — ориентиры)
Adobe Acrobat Reader / Standard / Pro эталон совместимости, редактирование, формы, подписи, redaction, PDF/A, сравнение дорогая подписка, тяжёлый, облачная привязка, Windows/macOS
Foxit PDF Editor быстрый, ConnectedPDF, редактирование подписка, телеметрия, Linux — только Reader
PDF-XChange Editor очень быстрый, богатые аннотации, OCR только Windows, перегруженный интерфейс
Apple Preview мгновенный, формы, подписи, пометки только macOS, нет редактирования текста, OCR — только через Live Text
Okular / Evince свободные, Linux-первый класс слабое редактирование, нет цифровых подписей (Okular — частично)
Sumatra PDF мгновенный запуск, минимализм только просмотр, только Windows
Master PDF Editor / PDF Studio кроссплатформенные редакторы Java/Qt, медленный рендер, качество конверсии в Word низкое
pdf.js / Chrome / Edge везде только просмотр и простые формы

1.3 Чем лучше

1.4 Решение по движку

Сравнение кандидатов для рендера и объектной модели:

Критерий pdfium (Google/Foxit) MuPDF (Artifex) Poppler Свой парсер на Rust (pdf-rs/lopdf + рендер Skia) Системные (PDFKit, Windows.Data.Pdf)
Лицензия BSD-3 — совместима с проприетарным продуктом AGPL-3 или коммерческая (платная, за продукт) — несовместима без покупки GPL-2/3 — несовместима (динамическая линковка не спасает по позиции FSF; риск) MIT/Apache-2 — свободно только одна ОС
Зрелость рендера очень высокая (Chrome, миллиарды файлов), шейдинги, прозрачность, JBIG2/JPX, Type3, повреждённые файлы очень высокая высокая нулевая на старте; 2–3 человеко-года до уровня «99 % корпуса» высокая, но разная
Извлечение текста, формы, аннотации текст (FPDFText), AcroForm (FPDF_FORMFILLINFO), аннотации (FPDFAnnot — чтение/создание базовое), редактирование объектов (FPDFPageObj — ограничено) полное полное только то, что напишем ограничено
Запись/инкрементальное сохранение FPDF_SaveWithVersion + FPDF_INCREMENTAL — пишет всё, но без контроля структуры (нет PDF/A, подписей, redaction) полная есть полный контроль — нужен нам в любом случае нет
Thread-safety нет (глобальная блокировка) нет (контекст на поток, клоны) частично как спроектируем (Rust: Send/Sync) —
Размер/сборка C++, ~15 МБ бинарник, сборка GN/Ninja (сложно) или готовые бинарники pdfium-binaries (bblanchon); Rust-биндинг pdfium-render (MIT), динамическая загрузка C C++, много зависимостей cargo —
Skia-бэкенд есть (pdf_use_skia=true; Chrome перешёл на Skia в 2024) — единый растеризатор с Meridian Render нет cairo/splash Skia напрямую —
WASM (на будущее) есть (pdfium в WASM — в Chrome и сторонних проектах) есть сложно естественно нет
Безопасность большой C++ код → песочница обязательна то же то же память безопасна, но логические ошибки парсера — фаззинг —

Решение.

  1. Рендер и извлечение текста на старте — pdfium (BSD-3) в отдельном песочном процессе (как все парсеры чужих форматов, раздел 6 библии), сборка с pdf_use_skia=true и без V8/XFA (JavaScript форм — свой на QuickJS, XFA — только статический через AcroForm). Биндинг — свой тонкий C-ABI слой (meridian-pdfium-sys) поверх готовых бинарников pdfium-binaries на три ОС; при старте проверяется хэш библиотеки.
  2. Объектная модель, писатель и всё редактирование — собственный крейт meridian-pdf-core (Rust, MIT/Apache): ленивый парсер объектов (xref-таблицы и потоки, объектные потоки, восстановление повреждённых), инкрементальные обновления, полная перезапись, шифрование, аннотации с генерацией внешнего вида, формы, подписи, redaction, оптимизация, PDF/A/UA, линеаризация. Этот крейт в любом случае нужен пакету для экспорта (Skia PDF-бэкенд даёт только «сырой» PDF без тегов/подписей/PDF/A — мы его постобрабатываем). lopdf используется как справочная реализация и для прототипа, не как зависимость в продукте (нужен ленивый mmap-парсинг и инкрементальные записи, которых в нём нет).
  3. План перехода на собственный рендер (meridian-pdf-render: интерпретатор контент-потоков → список отображения Meridian Render → Skia):
Этап Срок Содержание Критерий перехода
A (фаза 4) 16–26 мес pdfium — рендер/текст/формы; свой core — модель и запись; аннотации Meridian рисуются оверлеем поверх pdfium-растра до фиксации выпуск PDF 1.0
B (фаза 5) 22–30 мес свой интерпретатор контента: пути, текст (TrueType/CFF/Type1/Type3/CID через skrifa + свой CFF/Type1), изображения (Flate/LZW/DCT/CCITT/JBIG2(jbig2dec-порт на Rust)/JPX(jpeg2k/OpenJPEG)), цветовые пространства (ICC через lcms2), шейдинги 1–7, прозрачность (группы, soft mask, blend modes), OCG; за флагом --render=native пиксельное совпадение с pdfium ≥ 99 % на корпусе 50 000 файлов (ΔE < 2 на ≥ 99,5 % пикселей)
C (фаза 6) 30–36 мес свой рендер по умолчанию; pdfium — резерв для файлов, где обнаружено расхождение (эвристики: Type3 с нестандартными ресурсами, повреждённые xref, JPX-профили) доля fallback < 0,5 % по телеметрии (по согласию)
D (фаза 7) 36+ мес удаление pdfium из сборки; WASM-просмотрщик (если будет решение владельца) на том же коде —

Причины перехода: один код для рендера и редактирования (сейчас два парсера одного файла), единый растр с Meridian Render (шрифты, цвет), WASM, безопасность Rust, размер дистрибутива, независимость от Google.

Согласование двух парсеров на этапе A: pdfium работает по байтам текущей ревизии документа; после каждой фиксации изменений meridian-pdf-core формирует инкрементальное обновление в память, pdfium перезагружает документ (FPDF_LoadMemDocument, ленивый — миллисекунды) и рендерит изменённые страницы; до фиксации аннотации и правки показываются оверлеем Meridian (векторно, через Skia) — пользователь видит результат мгновенно.

Прочие зависимости: tesseract 5 (Apache-2.0) — OCR; lcms2 (MIT) — ICC; RustCrypto (rsa, p256, p384, sha2, cms, x509-cert) — подписи; rustls-native-certs/ОС API — доверенные корни; zip — XPS/CBZ; DjVu — см. риски.


2. Функции

Приоритеты: P0 — без этого нельзя выпустить PDF 1.0; P1 — в 1.x; P2 — позже.

2.1 Просмотр и навигация

Функция Описание Приоритет
Режимы страниц одна страница, непрерывно, разворот (2 стр.), разворот непрерывно, «первая отдельно» (обложка), автоматически по ширине окна P0
Вкладки несколько документов в одном окне (каждый — свой песочный процесс рендера), перетаскивание вкладки в новое окно, «восстановить вкладки» при запуске P0
Масштаб 8–6400 %, по ширине / по странице / по видимой области, плавный зум к курсору, Ctrl+колесо, жесты трекпада (pinch) P0
Поворот вида 90° шаг, для всех/текущей страницы (без сохранения); «Повернуть страницу» — в редактировании P0
Закладки (outline) дерево, переходы с масштабом (/XYZ, /Fit, /FitH…), раскрытие, редактирование (добавить/переименовать/переместить/удалить — Home) P0 (чтение), P1 (правка)
Миниатюры ленивая генерация, размер регулируется, перетаскивание страниц (режим «Организовать») P0
Слои OCG дерево слоёв (/OCProperties, /Order, радиогруппы), включение/выключение, печать с учётом состояния, «только просмотр» P0
Поиск в документе (Ctrl+F, панель результатов с контекстом, учёт регистра/целого слова/диакритики, регулярные выражения — P1), по всем открытым (Ctrl+Shift+F), подсветка всех совпадений, переходы F3/Shift+F3, поиск в комментариях и закладках P0
Навигация ссылки (внутренние, URI с подтверждением, GoToR, Launch — запрещён), именованные назначения, история назад/вперёд (Alt+←/→), переход на страницу (Ctrl+G), метки страниц (/PageLabels: «iii», «A-1»), первая/последняя/следующая/предыдущая P0
Вложения панель вложений (/EmbeddedFiles и FileAttachment-аннотации), открыть (с предупреждением), сохранить, добавить/удалить (Home) P0
Полноэкранный / презентация F11, переходы страниц, таймер, лазерная указка P0
Режимы чтения ночной (инверсия с сохранением изображений — по признаку «изображение/вектор»), сепия, пользовательские цвета фона/текста, «без фона» P1
Reflow перекомпоновка текста под ширину окна (мобильный стиль) по извлечённому порядку чтения P2
Чтение вслух TTS ОС по порядку чтения, подсветка текущего предложения P1
Свойства документа метаданные Info/XMP, защита, шрифты (встроены/подмножество/нет), страницы (размеры), стандарт (PDF/A, PDF/UA, PDF/X — определение по XMP), версия, размер, линеаризован ли P0
Выделение и копирование текст (по порядку чтения, с колонками), прямоугольная область (текст/изображение), копировать как текст/RTF/изображение, «копировать с форматированием» (шрифт, размер) P0
Измерения линейка, периметр, площадь с масштабом (например 1:100) — для чертежей; аннотации измерений (ISO 32000 /Measure) P1
Сравнение «бок о бок» две вкладки с синхронной прокруткой P1
Автопрокрутка Ctrl+Shift+H, скорость стрелками P1
Автоматическое открытие восстановление последней страницы и масштаба на документ (локальная БД Hub) P0

2.2 Аннотации (ISO 32000-2, раздел 12.5)

Тип (/Subtype) Инструмент Свойства Приоритет
Highlight / Underline / StrikeOut / Squiggly выделение текста → разметка (quadpoints по словам/строкам) цвет, непрозрачность, автор, заметка, ответ P0
Text (заметка, «стикер») щелчок значок (Comment, Note, Help, …), цвет, открытое всплывающее окно (Popup) P0
FreeText рамка с текстом, выноска (callout с линией и /LE), «пишущая машинка» шрифт (встраивается подмножество), размер, цвет, выравнивание, рамка, фон, вращение P0
Line / Square / Circle / Polygon / PolyLine рисование фигур цвет обводки/заливки, толщина, стиль штриха, наконечники (/LE: OpenArrow, ClosedArrow, Circle, Square, Diamond, Butt, Slash…), «облако» (cloudy border — P1) P0
Ink (рукописные) карандаш/маркер с планшетом (нажим → толщина, P1) цвет, толщина, сглаживание (Catmull-Rom → Безье), ластик по штрихам P0
Stamp палитра штампов (стандартные «Approved», «Draft», «Confidential», динамические с датой/автором, свои из изображения/PDF/векторные) масштаб, поворот, непрозрачность P0
FileAttachment прикрепить файл в точку значок, описание P0
Caret / замена текста «вставить текст», «заменить текст» (Caret + StrikeOut группой) — P1
Redact (разметка) см. 2.9 — P0
Link создать ссылку (область → страница/URL/файл) рамка, действие P0
Sound / Movie / Screen / RichMedia / 3D только отображение значка и воспроизведение звука/видео через системный плеер (Movie/Screen — P2) — P1/P2
Measurement (Line/PolyLine/Polygon + /Measure) измерения с масштабом единицы, точность P1
Watermark (аннотация) чтение — P1
Popup всплывающие окна заметок — P0

Общие функции: панель «Комментарии» (список по страницам/авторам/типам/статусам, фильтр, сортировка), ответы (/IRT, /RT /R) и рецензионные состояния (Accepted/Rejected/Completed/Cancelled/None через /RT /Group

2.3 Формы

Функция Описание Приоритет
Заполнение AcroForm текстовые (однострочные, многострочные, со счётчиком /MaxLen, comb), флажки, переключатели, списки, комбинированные (с вводом), кнопки (submit/reset — с подтверждением, без сети по умолчанию), поля подписи, даты (форматы AFDate_), числа (AFNumber_), «богатый текст» (RV — чтение); подсветка полей; Tab-обход по /Tabs или геометрии; автозаполнение из профиля (имя, адрес — по разрешению) P0
JavaScript форм подмножество Acrobat JS API в QuickJS: event, AFNumber_Format/Keystroke, AFDate_*, AFPercent_*, AFSpecial_*, AFSimple_Calculate, this.getField, field.value/valueAsString/display/hidden, app.alert (как встроенный диалог), util.printf/printd; без сети, без файловой системы, без app.launchURL без подтверждения; тайм-аут 1 с на обработчик P1 (расчёты и форматирование — P0)
Экспорт/импорт данных FDF, XFDF, JSON, CSV (пакетный импорт в копии формы — P1) P0
Создание и редактирование полей инструменты: текст, флажок, переключатель, список, комбо, кнопка, подпись, дата, изображение (кнопка с иконкой); свойства: имя (иерархическое a.b.c), значение по умолчанию, формат, проверка (диапазон, regex), расчёт (сумма/произведение/среднее, своя функция), внешний вид (шрифт, цвет, рамка, фон), порядок обхода, обязательность, только чтение; выравнивание/распределение полей, копирование P1
Автоопределение полей по подчёркиваниям, рамкам, подписям «_____», квадратикам на сканах P2
XFA статический XFA (с AcroForm-«тенью») — заполнение как AcroForm; динамический XFA — предупреждение «динамическая форма XFA не поддерживается, показана версия для просмотра» (страница-заглушка документа) P0 (статический), P2 (динамический — не планируется)
Сведение формы поля → содержимое страницы P0
Подписание форм / сброс кнопки Submit — только с явного согласия пользователя (показываем URL) P0

2.4 «Заполнить и подписать» (простые подписи без криптографии)

Функция Описание Приоритет
Подпись рукописная (мышь/планшет/тачпад; сглаживание; трансформируется в векторную кривую), изображение (PNG с удалением фона по порогу), набранный текст рукописным шрифтом (поставляемые шрифты OFL) P0
Инициалы отдельная сущность, те же способы P0
Хранение профиль подписей в хранилище ОС (Keychain/DPAPI/Secret Service), шифрованный, до 5 подписей; синхронизация через Meridian ID — P2 P0
Размещение перетаскивание, масштаб, вращение; вставляется как содержимое страницы (не аннотация), опция «как аннотацию Stamp» P0
Элементы заполнения текст, галочка ✓, крестик ✗, точка •, дата (сегодня), подчёркнутый текст с шагом (comb) P0
Запрос подписей у других по почте с полями подписи (через Mail) P2

2.5 Цифровые подписи

Функция Описание Приоритет
Подписание PAdES-B-B (базовая, ETSI EN 319 142), B-T (метка времени RFC 3161 от настраиваемого TSA), B-LT (встраивание сертификатов/OCSP/CRL в DSS), B-LTA (архивная метка — P2); adbe.pkcs7.detached для совместимости со старыми Acrobat (опция); видимая (поле с изображением/текстом: имя, дата, причина, место, DN) и невидимая; сертификация документа (DocMDP: без изменений / только формы / формы и аннотации); подпись поля подписи формы; несколько подписей (инкрементальные обновления); причина и место, контактные данные P0
Ключи и сертификаты хранилища ОС: Windows CNG/CryptoAPI (NCryptSignHash, в т.ч. смарт-карты через minidriver), macOS Keychain (SecKeyCreateSignature, включая токены через CryptoTokenKit), Linux — PKCS#11 (p11-kit, модули токенов, softhsm), файлы PKCS#12 (.p12/.pfx, пароль — через системный диалог, не сохраняется); алгоритмы RSA-PSS/RSA-PKCS1 v1.5 (2048+), ECDSA P-256/P-384, Ed25519 (PDF 2.0, P1); хэш SHA-256/384/512 P0
Проверка целостность (ByteRange + хэш), подпись, цепочка до доверенных корней (хранилища ОС + AATL/EUTL списки, обновляемые — P1), срок действия на момент подписания/метки времени, отзыв (OCSP, CRL, из DSS для LTV), покрытие документа (инкрементальные изменения после подписи — классификация «разрешённые/запрещённые» по DocMDP/FieldMDP), статус: «действительна», «действительна, подписант не доверен», «недействительна (документ изменён)», «неизвестно (нет сети для отзыва)»; панель подписей с деталями сертификата, цепочкой, отчётом P0
Метки времени документные метки времени (DocTimeStamp, PDF 2.0) — добавление и проверка P1
ГОСТ Р 34.10-2012 / 34.11-2012 через плагин-провайдер (trait SignatureProvider): КриптоПро CSP (CAdES-SDK), ViPNet CSP, также через PKCS#11 ГОСТ-токены (Рутокен, JaCarta); формат CMS с ГОСТ-OID, PAdES-совместимо; проверка цепочек по ГОСТ корням (Минцифры); сертификаты из хранилища КриптоПро P1 (Enterprise)
Политика предприятия требование TSA, запрет самоподписанных, обязательный LTV, разрешённые алгоритмы — из консоли администратора (Business/Enterprise) P1
Проверка при открытии автоматическая, с полосой статуса «Подписано и все подписи действительны» P0

2.6 Редактирование содержимого и страниц

Функция Описание Приоритет
Текст в строке щелчок по абзацу → редактируемый блок: вставка/удаление символов с перекладкой строк внутри блока, сохранение шрифта (встроенный полный — используется; подмножество — недостающие глифы из того же системного шрифта по имени/метрикам, иначе — ближайший с предупреждением «шрифт заменён»), размер, цвет, выравнивание, межстрочный; добавление нового текстового блока; поиск и замена текста по документу (P1) P0
Изображения замена, обрезка, поворот, отражение, масштаб, извлечение, свойства (разрешение, цвет, сжатие); «Редактировать в Paint» (round-trip) P0
Объекты выделение путей/изображений/текста, перемещение, масштаб, удаление, порядок (z), выравнивание; «редактировать как векторы в Draw» (страница → Draw → обратно, P2) P0
Добавить текст, изображение, фигуры (прямоугольник, эллипс, линия, стрелка), ссылки, номера страниц P0
Страницы вставить (пустая, из файла, со сканера, из буфера-изображения), удалить, повернуть (сохраняется /Rotate), переставить (перетаскивание в «Организовать»), обрезать (CropBox, визуально рамкой; MediaBox не меняется — опция «удалить обрезанное» с перезаписью), дублировать, заменить, размер страницы (масштабировать содержимое или добавить поля), объединить несколько PDF (с закладками по файлам), разделить (по N страниц, по закладкам верхнего уровня, по размеру файла, по диапазонам), извлечь (в новый файл, отдельными файлами), «страницы из других форматов» (изображения, docx через Write) P0
Колонтитулы текст с макросами ({page}, {pages}, {date}, {file}), шрифт, положение (6 зон), поля, диапазон страниц, чётные/нечётные, предпросмотр; удаление ранее добавленных (помечаются /Artifact Pagination + своя метка) P0
Водяные знаки текст/изображение/PDF-страница, непрозрачность, угол, масштаб, за/перед содержимым, повтор плиткой, диапазон, удаление P0
Нумерация Бейтса префикс/суффикс, начальный номер, число цифр, положение, несколько файлов подряд (сквозная), удаление P0
Фон цвет/изображение, диапазон P1
Сканирование TWAIN/ICA/SANE, дуплекс, автоподача, авто-OCR, автообрезка/поворот, в существующий документ P1
Таблицы распознавание таблиц в выделении → Sheets P1
Пакетная обработка «Действия» (последовательность: водяной знак → Бейтс → OCR → PDF/A → подпись) на папку, через MeridianScript и графический мастер P1

2.7 OCR

Функция Описание Приоритет
Распознавание tesseract 5 LSTM, языки — пакеты tessdata_best (скачиваются из нашего сервера обновлений, зеркало), несколько языков одновременно (rus+eng), автоопределение страниц без текста (сканы), выбор страниц/областей P0
Результат «Изображение с текстовым слоем» (невидимый текст поверх скана — поиск, копирование, доступность), «Редактируемый текст и изображения» (замена скана распознанным текстом с подбором шрифта — P2) P0 / P2
Предобработка DPI (авто/200/300/400/600), deskew, бинаризация, удаление шума, авто-поворот ориентации (OSD) P0
Качество уверенность по словам (подсветка < 70 % в панели «Проверка OCR» с правкой — P1), словари пользователя P1
Пакет папка файлов/сканов, фон, очередь P1
ZIP-сжатие сканов (MRC) разделение на передний план/фон (текст — JBIG2/CCITT, фон — JPEG низкого разрешения) P2

2.8 Преобразование и экспорт

Направление Описание Приоритет
→ Write (.mwx, .docx) анализ раскладки (5.4): абзацы, стили, колонки, таблицы, списки, заголовки, колонтитулы, изображения, ссылки, сноски; режимы «текучий документ» (по умолчанию) и «точная вёрстка» (рамки, как у Acrobat «Retain Page Layout»); OCR-слой используется для сканов P0
→ Sheets (.msx, .xlsx) таблицы (все/выбранные), числа и даты распознаются, один лист на таблицу/страницу P0
→ Slides (.mpx, .pptx) страница = слайд, текстовые блоки, изображения, фигуры (векторы как изображения — P0, как фигуры — P1) P1
→ Изображения PNG/JPEG/TIFF/WebP, DPI, цвет, диапазон, прозрачный фон (PNG), один файл/многостраничный TIFF P0
→ Текст .txt (порядок чтения, с разбиением страниц), .md (заголовки/списки — P1) P0
→ HTML один файл с CSS (текучий или позиционированный), EPUB (reflowable, P2) P1 / P2
→ SVG страницы как векторы (текст как <text> или контуры) P1
→ PDF/A, PDF/UA, PDF/X-1a/4 (P2) см. 2.11 P0
← Из других форматов «Создать PDF из…»: изображения, docx/odt/xlsx/pptx (через Write/Sheets/Slides), HTML (через Write), текст, скан, буфер обмена, объединение P0
Извлечение все изображения (с исходным сжатием), шрифты (только для анализа — лицензии), вложения P0

2.9 Redaction (удаление с настоящим вычищением)

Функция Описание Приоритет
Разметка прямоугольники (мышью), текст (выделение), поиск и разметка (слово/фраза/regex/шаблоны: e-mail, телефоны, ИНН, СНИЛС, паспорт, карты), страницы целиком, повторяющиеся области на всех страницах P0
Внешний вид цвет заливки, код/текст поверх (например «[УДАЛЕНО]», «Ст. 7 152-ФЗ»), шрифт P0
Применение необратимое: удаление текста/изображений/путей внутри областей из контент-потоков (5.7), обрезка изображений, удаление аннотаций и ссылок в области, OCR-слоя, закладок на удалённые места, полная перезапись без старых ревизий, сохранение в новый файл (_redacted.pdf) по умолчанию P0
Удалить скрытую информацию (sanitize) метаданные (Info, XMP), вложения, JavaScript, скрытые слои, комментарии, данные форм, скрытый текст (Tr 3, за объектами — опция), миниатюры, закладки, ссылки, удалённое содержимое (старые ревизии), встроенные индексы; отчёт P0
Проверка «Проверить документ» перед публикацией — список найденного с выбором P0

2.10 Защита

Функция Описание Приоритет
Пароли на открытие (user) и на права (owner); AES-256 (R=6, SHA-256/384/512 цепочка — ISO 32000-2), AES-128 (R=4), RC4 40/128 — только чтение; Unicode-пароли (SASLprep) P0
Права печать (нет/низкое/высокое качество), изменение, копирование, аннотации, заполнение форм, доступность (извлечение для чтения с экрана), сборка; соблюдаем при просмотре (кроме случаев, когда владелец ввёл owner-пароль) P0
Снятие защиты при известном пароле P0
Сертификатная защита шифрование на открытые ключи получателей (PKCS#7 envelope, /Filter /Adobe.PubSec) P2
Политики Business/Enterprise: обязательное шифрование при экспорте, запрет снятия, водяной знак с именем пользователя P2

2.11 Стандарты: PDF/A, PDF/UA, PDF/X

Функция Описание Приоритет
Конвертация в PDF/A 1b, 2b, 2u, 3b (с вложениями); действия: встраивание шрифтов (включая стандартные 14 — из Liberation/Nimbus), удаление прозрачности для 1b (сведение страниц с прозрачностью — растрирование областей), преобразование цветов с OutputIntent (sRGB IEC61966-2.1 по умолчанию), XMP с pdfaid, удаление JavaScript/шифрования/внешних ссылок на содержимое, замена LZW/JPX(1b), приведение аннотаций (внешний вид, запрещённые типы), отчёт о неисправимом P0
Проверка PDF/A валидатор по правилам ISO 19005-1/2/3 (структурированный набор правил — ~300, с идентификаторами как у veraPDF; тестирование нашим валидатором на корпусе veraPDF и Isartor/BFO) P0
PDF/UA-1 теги структуры: просмотр дерева тегов, автотегирование по анализу раскладки (5.4: заголовки H1–H6, P, L/LI/Lbl/LBody, Table/TR/TH/TD, Figure с Alt, Link, Artifact для колонтитулов), редактор тегов (переназначение, порядок, alt-текст), проверка (подмножество Matterhorn Protocol: заголовок документа, язык, теги на весь контент, Alt у изображений, порядок чтения, таблицы с TH), «Сделать доступным» мастер (OCR → автотеги → alt → язык → заголовок) P1 (просмотр/проверка — P0, автотегирование и редактор — P1)
PDF/X проверка 1a/3/4 (OutputIntent, цвета, шрифты, bleed/trim boxes), конвертация — P2 P2

2.12 Оптимизация и сжатие

Функция Описание Приоритет
Профили «Для экрана» (96 dpi, JPEG 60), «Для печати» (300 dpi, JPEG 85), «Минимальный размер», «Свой» P0
Изображения понижение разрешения (цветные/серые/ч-б отдельно, порог «если выше»), пересжатие (JPEG качество, JPEG2000 → JPEG, Flate для lossless, CCITT G4/JBIG2-generic для ч-б — JBIG2 P2), удаление ICC из изображений (с конверсией в sRGB), 16 → 8 бит P0
Шрифты удаление дубликатов подмножеств (объединение одинаковых FontFile по хэшу), отмена встраивания стандартных 14 (опция, не для PDF/A) P0 (дедупликация), P2 (объединение подмножеств разных глифов)
Структура удаление неиспользуемых объектов, объектные потоки, xref-поток, сжатие всех потоков Flate (уровень 9), удаление миниатюр страниц, метаданных (по выбору), вложений, JavaScript, альтернативных изображений, старых ревизий (полная перезапись), пустых OCG P0
Линеаризация «Fast Web View» по Annex F (первая страница + hint streams) P1
Аудит пространства отчёт: изображения / шрифты / контент / структура / вложения в байтах и процентах, топ-20 объектов P0

2.13 Сравнение версий

Функция Описание Приоритет
Текстовое diff слов по порядку чтения с подсветкой вставок/удалений/замен в обоих документах, навигация по изменениям, отчёт PDF/HTML P1
Визуальное наложение (мигание, разностная карта с подсветкой регионов), бок о бок с синхронной прокруткой и совмещением страниц (вставленные/удалённые страницы) P1

2.14 Печать

Через Meridian Print: диапазоны (включая метки страниц и «текущая/выделенные»), чётные/нечётные, масштаб (по размеру, 100 %, свой, «большие страницы — разбить на листы»), несколько страниц на лист (2/4/6/9/16 с рамками), буклет (порядок и сгиб), автоповорот и центрирование, печать аннотаций/форм/штампов (или только документа), комментарии и сводка, оттенки серого, «как изображение» (растр при проблемных шрифтах), двусторонняя печать, выбор лотка, печать в PDF (виртуальный принтер — через Meridian Print: Windows — наш port monitor, P2; macOS/Linux — системный «Сохранить как PDF» уже есть) — P0 (кроме отмеченного).

2.15 Порталы документов (P2)

Портфолио PDF (Portable Collections, /Collection): просмотр списка файлов с полями, открытие вложенных PDF во вкладках, создание портфолио из файлов, схема полей — P2.

2.16 Доступность

Функция Описание Приоритет
Чтение с экрана по тегам структуры (если есть) или по порядку чтения (5.3); IAccessible2/UIA/NSAccessibility/AT-SPI дерево страниц; озвучивание аннотаций и полей форм P0
Клавиатура все функции с клавиатуры; Tab по ссылкам/полям/аннотациям P0
Высокий контраст, цвета, масштаб интерфейса темы MDS, режим цветов документа P0
Reflow и крупный текст см. 2.1 P2
Создание доступных PDF см. 2.11 P1

2.17 Интеграция с пакетом


3. Экраны

Обозначения: PD-NN. Макеты — mockups/pdf.html. Каркас окна — по разделу 5.3 библии. Ribbon-вкладки: «Файл · Главная · Комментарии · Редактирование · Формы · Защита · Преобразовать · Вид · Скрипты»; в Free вкладки «Редактирование / Защита / Преобразовать» показывают «замок» и предложение редакции Home.

PD-01 Главное окно (просмотр)

┌────────────────────────────────────────────────────────────────────────────────────┐
│ ● Договор-2026.pdf — Meridian PDF                        [поиск ⌘K]  [☁] [👤]       │
├────────────────────────────────────────────────────────────────────────────────────┤
│ Файл  Главная  Комментарии  Редактирование  Формы  Защита  Преобразовать  Вид  Скрипты │
│ [Выделение ▾][Рука][Масштаб −  100 % ▾ +][Страница 3 / 12][Повернуть][Печать][Подписать ▾][OCR] │
├──┬──────────────────────────────────────────────────────────────────┬──────────────┤
│▤ │ ◦ Договор-2026.pdf × │ Счёт.pdf × │ +                              │ Комментарии  │
│☰ ├──────────────────────────────────────────────────────────────────┤──────────────│
│📎│                 ┌──────────────────────────┐                      │ ▸ Стр. 1 (2)  │
│◧ │                 │                          │                      │   Иванов: …   │
│✎ │                 │   страница 3             │                      │ ▸ Стр. 3 (1)  │
│🔏│                 │   (тайлы, тень)          │                      │              │
│  │                 │                          │                      │──────────────│
│  │                 └──────────────────────────┘                      │ Свойства     │
│  │                 ┌──────────────────────────┐                      │ аннотации    │
│  │                 │   страница 4             │                      │              │
├──┴──────────────────────────────────────────────────────────────────┴──────────────┤
│ 🔒 Подписано, 2 подписи действительны · PDF/A-2b · 12 стр. · A4 · [▭ 100 % ▾] · ⟲ 0° │
└────────────────────────────────────────────────────────────────────────────────────┘
Зона Содержимое
Левая полоса значков переключатели панелей навигации: Миниатюры ▤, Закладки ☰, Вложения 📎, Слои ◧, Комментарии ✎, Подписи 🔏, Поля формы (при наличии), Теги (P1), Результаты поиска
Навигационная панель открывается щелчком по значку, ширина запоминается, отделяемая
Вкладки документов над холстом; значки состояния: ● изменён, 🔒 подписан/защищён
Холст страницы с тенью на фоне --m-bg, промежуток 12 px, непрерывная прокрутка; оверлеи аннотаций; подсветка найденного; рамки полей форм (голубые, переключаемо)
Ribbon «Главная» Инструменты (выделение текста/область, рука, лупа), Масштаб, Страница (поле, назад/вперёд), Поворот, Печать, Подписать (меню: Заполнить и подписать / Цифровая подпись / Сертифицировать), OCR, Преобразовать (кнопки), Поделиться
Ribbon «Комментарии» все инструменты 2.2 (галерея), цвет/толщина/непрозрачность, штампы, панель комментариев, экспорт/импорт, сводка, сведение
Ribbon «Редактирование» Текст, Изображение, Объект, Добавить (текст/изображение/фигура/ссылка), Страницы (организовать, вставить, удалить, повернуть, обрезать, объединить, разделить, извлечь), Колонтитулы, Водяной знак, Бейтс, Фон
Ribbon «Формы» Подготовить форму (режим редактора полей), поля (галерея), свойства, порядок обхода, экспорт/импорт данных, сброс, сведение
Ribbon «Защита» Пароль, Права, Снять защиту, Redaction (отметить / найти и отметить / применить), Удалить скрытую информацию, Подписи (панель), Сертификаты
Ribbon «Преобразовать» В Write / Sheets / Slides / Изображения / Текст / HTML; PDF/A, PDF/UA (проверить/исправить), Оптимизировать, Сравнить, Создать PDF из…
Ribbon «Вид» режимы страниц, масштаб, поворот, панели, ночной режим, полный экран, разделить окно, линейки/сетка/направляющие (редактирование), «две страницы синхронно»
Боковая панель Комментарии (список) / Свойства выбранной аннотации или объекта / Подписи / Поля формы / Слои / Теги
Строка состояния подписи и стандарт, число страниц, формат страницы, масштаб, поворот, индикатор OCR-слоя, предупреждение «динамический XFA»
Контекстная мини-панель при выделении текста: выделить/подчеркнуть/зачеркнуть/заметка/копировать/искать/redact; при выборе аннотации: цвет/толщина/удалить

PD-02 Панели навигации

Панель Содержимое
Миниатюры сетка/список, ползунок размера, текущая страница выделена, перетаскивание (переставить), контекстное меню (вставить/удалить/повернуть/извлечь), множественный выбор, drag-and-drop файлов для вставки страниц
Закладки дерево /Outlines, раскрытие, редактирование (Home): новая из текущего вида, переименовать, переместить, удалить, «из заголовков» (по анализу раскладки)
Вложения список (имя, размер, описание, дата), открыть (с предупреждением о типе), сохранить, добавить, удалить
Слои дерево OCG с флажками, радиогруппы, «сбросить к начальному состоянию», печать/экспорт с текущим состоянием
Комментарии список с группировкой (страница/автор/тип/дата/статус), фильтры, поиск, ответы, статусы, «перейти»
Подписи список подписей: статус-значок, подписант, дата, причина; детали (сертификат, цепочка, метка времени, LTV, изменения после подписи), «проверить все», «просмотр подписанной версии»
Поля дерево полей формы с значениями, переход к полю
Теги дерево структуры (P1), выделение тега подсвечивает содержимое, редактирование
Результаты поиска список совпадений с контекстом по страницам, переходы

PD-03 Панель «Свойства» (аннотация / объект / поле)

Аннотация: тип, автор, дата, цвет, непрозрачность, толщина/стиль линии, наконечники, шрифт (FreeText), содержимое (заметка), статус, блокировка, печатать (флаг /Print), «по умолчанию для инструмента». Объект (редактирование): тип (текст/изображение/путь/форма XObject), геометрия (x, y, ширина, высота, угол), шрифт/размер/цвет (текст), разрешение/сжатие (изображение), заливка/обводка (путь). Поле: имя, тип, значение, значение по умолчанию, формат, проверка, расчёт, внешний вид, флаги.

PD-04 Строка поиска и панель результатов

Поле поиска (Ctrl+F) появляется под ribbon справа: текст, счётчик «3 из 17», стрелки, параметры (регистр, целое слово, диакритика, regex (P1), в комментариях, в закладках), «Результаты ▸» открывает панель.

PD-05 Свойства документа

Вкладки: Описание (Title/Author/Subject/Keywords/Creator/Producer/даты — редактируемые в Home, XMP «как есть»), Защита (метод, права — таблица «разрешено/запрещено»), Шрифты (имя, тип, кодировка, встроен/подмножество, «заменён на …»), Страницы (размеры, повороты, метки), Стандарты (PDF/A/UA/X — заявление и результат проверки), Начальный вид (страница, масштаб, режим, панель — редактируемо), Дополнительно (версия, линеаризация, объекты, размер, путь).

PD-06 Защита паролем и правами

Поля: пароль открытия (с подтверждением, индикатор стойкости), пароль владельца, права (флажки по 2.10), шифрование (AES-256 ▾ / AES-128 / «совместимость с Acrobat 7+»), «шифровать метаданные», «не шифровать вложения»; результат применяется при сохранении.

PD-07 Цифровая подпись (мастер)

  1. Сертификат: список из хранилищ ОС (имя, издатель, срок, назначение «подписание документов»), «Из файла PKCS#12…», «Токен/смарт-карта…» (PKCS#11), «Провайдер ГОСТ ▾» (Enterprise).
  2. Внешний вид: невидимая / видимая (область на странице рисуется мышью или выбирается существующее поле); шаблон (имя, дата, причина, место, DN, логотип, рукописная подпись из профиля), предпросмотр.
  3. Параметры: причина, место, контакт, уровень (B-B / B-T / B-LT / B-LTA), TSA (из политики или свой URL), сертификация (нет / только формы / формы и аннотации / без изменений), блокировать поля формы.
  4. Подтверждение: сводка, «после подписи документ будет сохранён как …», кнопка «Подписать» (PIN/пароль запрашивает ОС/токен).

PD-08 Проверка подписи

Диалог с итогом (значок + текст), разделы: «Целостность документа» (хэш совпадает; изменения после подписи: список с классификацией), «Подписант» (сертификат, цепочка, доверие, срок, отзыв с источником OCSP/CRL/DSS и временем проверки), «Время» (метка времени TSA / время из подписи — недоверенное), «Долгосрочная проверка» (LTV: да/нет, что встроено), «Отчёт» (сохранить/копировать), «Добавить сертификат в доверенные» (с предупреждением).

PD-09 Заполнить и подписать

Контекстная панель над страницей: «Текст · ✓ · ✗ · • · Подпись ▾ · Инициалы ▾ · Дата»; меню подписи — профиль подписей (миниатюры, «Добавить…», «Удалить»); диалог «Добавить подпись» с вкладками «Нарисовать» (холст с планшетным нажимом, цвет чернил, толщина), «Изображение» (файл/буфер, порог фона), «Напечатать» (текст, выбор рукописного шрифта), флажок «Сохранить подпись».

PD-10 Организовать страницы

Режим на весь холст: сетка миниатюр 128–512 px, выбор (клик, Ctrl, Shift, рамка), перетаскивание, панель: вставить (пустая/из файла/скан/буфер), удалить, повернуть ←/→, дублировать, извлечь, заменить, разделить здесь, обрезать, размер; поле «диапазон» для операций; счётчик страниц; отмена в истории.

PD-11 Объединить файлы

Список файлов (drag-and-drop, из проводника, «Добавить открытые»), порядок (перетаскивание, по имени/дате), диапазоны страниц для каждого, предпросмотр миниатюр, параметры: закладки по файлам, сохранять формы (переименование конфликтующих полей file1.field), сохранять OCG, объединить одинаковые шрифты, результат — новый документ.

PD-12 Разделить

Правило: по N страниц / по закладкам верхнего уровня (имя файла из закладки) / по размеру файла (МБ) / по диапазонам («1-3, 4-10, 11-») / по пустым страницам-разделителям (P1); шаблон имени ({name}_{index}, {bookmark}), папка, «удалить исходные страницы».

PD-13 Колонтитулы / Водяной знак / Бейтс (общий мастер)

Левая часть — параметры (текст с макросами, шрифт, размер, цвет, положение, поля, угол, непрозрачность, масштаб, за/перед, повтор), диапазон страниц (все/чётные/нечётные/диапазон), «применить к нескольким файлам…»; правая — предпросмотр страницы с перетаскиванием элемента; «Сохранить настройки как…»; вкладка «Удалить» — список ранее добавленных (по нашим меткам /Artifact + /MeridianKind).

PD-14 OCR

Языки (мультивыбор с поиском, значок «скачать» для отсутствующих — размер пакета), страницы (все/текущая/ диапазон/только без текста), результат (текстовый слой / редактируемый текст — P2), DPI, deskew/автоповорот, «показать слова с низкой уверенностью после», кнопка «Распознать» с прогрессом по страницам (фоновая задача с возможностью продолжать работу).

PD-15 Экспорт / Преобразовать

Формат (Write/docx, Sheets/xlsx, Slides/pptx, изображения, текст, HTML, SVG), параметры по формату (режим раскладки «текучий/точный», распознавать таблицы, включать изображения, OCR если нужно; DPI/качество для изображений; диапазон страниц), «открыть после экспорта», предпросмотр результата для Write (первая страница, P1).

PD-16 Redaction

Режим: панель «Отметить · Отметить страницы · Найти и отметить (текст/шаблоны/regex) · Свойства (цвет/текст поверх) · Применить»; отмеченные области — красная рамка с предпросмотром заливки при наведении; «Применить» → диалог подтверждения (число областей и страниц, «также удалить скрытую информацию», «сохранить как новый файл») → отчёт.

PD-17 Удалить скрытую информацию

Список категорий с флажками и найденным количеством (метаданные 12 полей, вложения 2, JavaScript 1, скрытый текст 3 стр., комментарии 7, …), «Удалить выбранное», отчёт.

PD-18 PDF/A: конвертация и отчёт проверки

Уровень (1b/2b/2u/3b), OutputIntent, «встроить шрифты из системы», «растрировать несовместимое», «сохранить вложения (3b)»; результат — отчёт: таблица «правило · описание · страница/объект · исправлено/не исправлено», кнопка «Сохранить отчёт», «Проверить снова».

PD-19 Оптимизация

Профиль ▾, слева — дерево параметров (изображения: цветные/серые/ч-б — порог dpi, целевой dpi, сжатие/ качество; шрифты; структура; очистка), справа — аудит пространства (круговая диаграмма Meridian Chart + таблица), внизу — «Было 48,2 МБ → станет ≈ 6,1 МБ (оценка)», «Оптимизировать» (в новый файл по умолчанию).

PD-20 Сравнение

Выбор двух документов (текущий + файл), режим (текст/визуально), параметры (игнорировать регистр/пробелы, порог визуальных различий), результат — режим на весь холст: бок о бок с синхронной прокруткой, список изменений справа (стр., тип, фрагмент), подсветка (зелёный — добавлено, красный — удалено, жёлтый — заменено), «Отчёт…».

PD-21 Печать

Общий диалог Meridian Print с предпросмотром: принтер, копии, диапазон (поле с поддержкой меток и «1-3, 7»), чётные/нечётные, масштаб, несколько на лист, буклет, ориентация авто, аннотации/формы, оттенки серого, «как изображение», двусторонняя, лоток; «Печать в PDF».

PD-22 Редактор формы (режим «Подготовить форму»)

Холст с полями (рамки с именами), палитра полей на ribbon, выравнивание/распределение, «порядок обхода» (номера на полях, перетаскивание в панели), свойства (PD-03), «предпросмотр» (режим заполнения), «автоопределение» (P2).

PD-23 Редактор тегов (P1)

Панель с деревом структуры, подсветка выделенного тега на странице, «отметить как…» (абзац, заголовок N, рисунок, таблица, артефакт), alt-текст, порядок чтения (перетаскивание), «проверить доступность» с отчётом, «автотегирование».

PD-24 Измерения

Инструменты: расстояние, периметр, площадь; панель: масштаб (например «1 см = 1 м», калибровка по двум точкам), единицы, точность, привязка к путям; результат — аннотации с /Measure и подписями.

PD-25 Штампы

Галерея: «Стандартные» (Approved, Draft, Final, Confidential, For Public Release, …; локализованные варианты: «Утверждено», «Черновик», «Конфиденциально»), «Динамические» (с именем/датой/временем), «Подписи и инициалы», «Пользовательские» (из изображения/PDF/выделения на странице), управление (переименовать/удалить/экспорт набора).

PD-26 Параметры PDF

Разделы: Общие (режим страниц по умолчанию, восстановление позиции, вкладки), Отображение (сглаживание текста/ векторов/изображений, рендер тонких линий, цвет фона, кэш в МБ, GPU), Комментарии (автор, цвета по умолчанию, показывать всплывающие окна), Формы (подсветка, автозаполнение, JavaScript вкл/выкл), Подписи (TSA, политика проверки, доверенные списки AATL/EUTL, провайдеры ГОСТ), Безопасность (доверенные папки для ссылок/вложений, запрет внешних переходов), OCR (языки, папка данных), Доступность (чтение с экрана, порядок чтения), Движок (рендер pdfium/native — после этапа B).

PD-27 Полноэкранный режим / презентация

Чёрный фон, страница по размеру экрана, переходы (Space/PgDn, стрелки, колесо), указка (L), таймер, выход Esc; настройки /ViewerPreferences (/PageMode /FullScreen) учитываются при открытии с подтверждением.

PD-28 Стартовый экран (без документа)

Как Hub в миниатюре: «Открыть», «Создать PDF из…», «Объединить», «Сканировать», недавние файлы с миниатюрами, подсказка «перетащите PDF сюда».

PD-29 Предупреждения безопасности

Единая полоса под ribbon (жёлтая): «Документ содержит JavaScript — выполнение отключено [Разрешить]», «Документ запрашивает переход на внешний адрес …», «Динамическая форма XFA», «Файл повреждён и восстановлен — сохраните копию», «Подпись недействительна».


4. Модель данных

4.1 Объектная модель PDF (meridian-pdf-core)

// kernel/crates/meridian-pdf-core/src/object.rs

#[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct ObjId { pub num: u32, pub gen: u16 }

#[derive(Clone)]
pub enum Object {
    Null, Bool(bool), Integer(i64), Real(f64),
    String(PdfString),                         // байты + признак hex/literal; текст — PDFDocEncoding/UTF-16BE/UTF-8 (2.0)
    Name(Name),                                // интернированное имя / interned name
    Array(Vec<Object>),
    Dictionary(Dict),                          // упорядоченная карта Name → Object (порядок сохраняем для round-trip)
    Stream(Arc<Stream>),                       // словарь + сырые байты (ленивые) + кэш декодированных
    Reference(ObjId),
}

pub struct Stream { pub dict: Dict, raw: StreamData, decoded: OnceCell<Arc<[u8]>> }
pub enum StreamData { Mapped { offset: u64, len: u64 }, Owned(Arc<[u8]>) }  // mmap исходника или новые байты

pub struct Document {
    source: Option<Mmap>,                      // исходный файл (только чтение) / original file, read-only
    xref: XrefTable,                           // объединённая таблица по всем ревизиям (последняя побеждает)
    trailer: Dict,
    cache: DashMap<ObjId, Object>,             // разобранные объекты (ленивая загрузка) / lazily parsed objects
    changes: BTreeMap<ObjId, Change>,          // Change::Set(Object) | Change::Delete — рабочее множество изменений
    next_num: u32,
    encryption: Option<Encryption>,            // параметры и ключ (после ввода пароля)
    revisions: Vec<Revision>,                  // границы ревизий (offset startxref) — для «просмотра подписанной версии»
    repaired: bool,                            // xref восстановлен сканированием / reconstructed by scanning
}

pub enum Change { Set(Object), Delete }

pub struct XrefTable { entries: Vec<XrefEntry> /* индекс = номер объекта */, kind: XrefKind /* Table | Stream | Hybrid */ }
pub enum XrefEntry { Free, Offset { offset: u64, gen: u16 }, InStream { stream: u32, index: u32 }, Unknown }

Поверх объектов — типизированные обёртки (без копирования, валидация при доступе): Catalog, PageTree, Page (наследуемые атрибуты /Resources, /MediaBox, /CropBox, /Rotate через цепочку /Parent), Resources, Font (Type1/TrueType/Type0/Type3 + FontDescriptor, кодировки, CMap, ToUnicode, ширины), XObject (Image/Form), Annotation, AcroForm/Field/Widget, Outline, OptionalContent, StructTree, NameTree, NumberTree, EmbeddedFile, SignatureDict, DSS.

4.2 Страница как дерево объектов содержимого (для редактирования и redaction)

Контент-поток разбирается в список объектов с геометрией (один раз, ленивый кэш по странице; инвалидируется при изменениях):

pub enum PageObject {
    Path { ops: Vec<PathOp>, fill: Option<Paint>, stroke: Option<StrokeStyle>, clip: Option<ClipId>, gs: GState, bbox: Rect, src: OpRange },
    Text { block: TextBlockId, runs: Vec<TextRun>, bbox: Rect, src: OpRange },
    Image { xobject: ObjId, ctm: Matrix, bbox: Rect, smask: Option<ObjId>, inline: Option<InlineImage>, src: OpRange },
    Shading { shading: ObjId, ctm: Matrix, clip: Option<ClipId>, src: OpRange },
    Form { xobject: ObjId, ctm: Matrix, children: Vec<PageObject>, bbox: Rect, src: OpRange },  // рекурсивно / recursive
    MarkedContent { tag: Name, mcid: Option<u32>, children: Vec<PageObject>, src: OpRange },
}

pub struct TextRun {
    pub font: FontRef, pub size: f64, pub matrix: Matrix /* Tm·CTM */, pub color: Paint, pub render_mode: u8,
    pub glyphs: Vec<Glyph>,              // {code, gid, unicode: SmallVec<char>, origin: Point, advance: f64, bbox: Rect}
    pub char_spacing: f64, pub word_spacing: f64, pub hscale: f64, pub rise: f64,
    pub src: OpRange,                    // байтовый диапазон операторов в исходном потоке / operator byte range
}

/// Операторы хранятся как токены с диапазонами — правка = замена диапазонов, остальное байт-в-байт
/// Operators are kept as tokens with ranges — an edit replaces ranges, everything else is byte-identical
pub struct ContentEdit { pub stream_index: usize, pub range: OpRange, pub replacement: Vec<u8> }

Текстовая модель для «текста в строке»: TextBlock { lines: Vec<TextLine { runs, baseline, bbox }>, bbox, align, para_style } строится из TextRun по алгоритму 5.3 (строки → блоки) и при редактировании перекладывается внутри bbox блока; результат сериализуется обратно в операторы (BT, Tf, Tm, TJ с кернингом, ET) с новым подмножеством шрифта (FontSubsetter: TrueType/CFF — skrifa/свой; Type1 — конвертация в CFF).

4.3 Аннотации как слой Meridian

pub struct Annotation {
    pub id: AnnotId, pub page: u32, pub kind: AnnotKind, pub rect: Rect,
    pub author: String, pub created: DateTime, pub modified: DateTime, pub contents: String,
    pub color: Option<Color>, pub interior: Option<Color>, pub opacity: f32, pub flags: AnnotFlags,
    pub border: BorderStyle, pub in_reply_to: Option<AnnotId>, pub state: Option<ReviewState>,
    pub appearance: Appearance,          // Original(ObjId) | Generated(DisplayList) | Dirty
    pub pdf_ref: Option<ObjId>,          // исходный словарь, если аннотация пришла из файла
}
pub enum AnnotKind {
    Markup { subtype: Markup /* Highlight|Underline|StrikeOut|Squiggly */, quads: Vec<Quad> },
    Note { icon: NoteIcon, open: bool },
    FreeText { text: RichText, font: FontSpec, align: Align, callout: Option<[Point; 3]>, rotation: i16 },
    Shape { subtype: Shape /* Line|Square|Circle|Polygon|PolyLine */, vertices: Vec<Point>, line_ends: (LineEnd, LineEnd), cloudy: Option<f32>, measure: Option<Measure> },
    Ink { strokes: Vec<Vec<InkPoint>> },
    Stamp { name: Name, image: Option<Arc<ImageData>>, form: Option<ObjId>, rotation: f32 },
    FileAttachment { file: EmbeddedFileRef, icon: Name },
    Link { action: Action, quads: Vec<Quad> },
    Redact { overlay_text: Option<String>, fill: Color },
    Caret, Popup { parent: AnnotId }, Widget { field: FieldId }, Other { subtype: Name, dict: Dict },
}

До сохранения аннотации живут в модели и рисуются оверлеем (Skia) поверх растра страницы; при сохранении каждая Dirty-аннотация сериализуется в словарь + генерируется /AP /N (форм-XObject по DisplayList через meridian-pdf-write), Original — байт-в-байт. Undo/redo — командный стек над моделью (создание/изменение/ удаление, перемещение, ответы).

4.4 Формы, подписи, структура

pub struct Field { pub id: FieldId, pub fq_name: String, pub kind: FieldKind, pub value: FieldValue, pub default: FieldValue,
    pub flags: FieldFlags, pub options: Vec<(String, String)>, pub widgets: Vec<Widget>, pub actions: Actions, pub da: DefaultAppearance,
    pub max_len: Option<u32>, pub format: Option<FormatSpec>, pub validate: Option<Script>, pub calculate: Option<Script>, pub pdf_ref: Option<ObjId> }

pub struct SignatureInfo { pub field: FieldId, pub byte_range: [u64; 4], pub contents: Vec<u8> /* DER CMS */, pub subfilter: SubFilter,
    pub signer: Option<CertInfo>, pub chain: Vec<CertInfo>, pub signing_time: Option<DateTime>, pub timestamp: Option<TimestampInfo>,
    pub doc_mdp: Option<MdpLevel>, pub status: VerifyStatus, pub revision_end: u64 /* конец подписанной ревизии */ }

pub struct StructNode { pub kind: StructKind /* Document|Part|H1..H6|P|L|LI|Lbl|LBody|Table|TR|TH|TD|Figure|Link|Span|Artifact|… */,
    pub children: Vec<StructChild /* Node(Box<StructNode>) | Content{page, mcid} | Object(ObjId) */>, pub alt: Option<String>, pub lang: Option<String>, pub attrs: Dict }

4.5 Кэш рендера

pub struct TileKey { doc: DocId, page: u32, level: u8 /* zoom bucket: 2^(level/4) */, tx: u16, ty: u16, rot: u8, flags: RenderFlags /* annots, forms, ocg_state_hash, night */ }
pub struct TileCache { lru: LruCache<TileKey, Arc<Bitmap>>, bytes: usize, budget: usize /* по умолчанию 25 % ОЗУ, ≤ 2 ГБ */ }
pub struct PagePreview { page: u32, bitmap: Arc<Bitmap> /* ≤ 256 px по длинной стороне */ }

Тайл — 512×512 px экранных пикселей (с учётом DPR); ключ включает «ведро» масштаба (шаг 2^(1/4) ≈ 19 %), между ведрами — масштабирование ближайшего готового тайла (билинейно) до прихода точного.

4.6 Инкрементальные обновления при сохранении

При сохранении Document.changes сериализуется после исходных байтов: новые/изменённые объекты → новая секция xref (таблица или поток — как в исходнике; гибридные — поток) → trailer с /Prev и обновлённым вторым элементом /ID → startxref. Исходные байты не меняются — существующие подписи остаются действительными (их ByteRange покрывает прежнюю ревизию), а изменения классифицируются при проверке. Полная перезапись (rewrite) — обход от каталога, перенумерация, объектные потоки, удаление мусора; обязательна для redaction, оптимизации, PDF/A, шифрования/снятия защиты и при «удалить старые ревизии».


5. Алгоритмы

5.1 Рендер страниц с кэшем тайлов и предзагрузкой

Поток UI (Qt):                    Поток планировщика:                 Процесс рендера (pdfium, песочница):
 viewport изменился ──────────▶  visible_pages(viewport)               очередь задач по приоритету
                                 for page in visible ∪ prefetch:        ├─ render_tile(key) → Bitmap
                                   tiles = tiles_for(page, zoom)         ├─ render_preview(page) → Bitmap ≤256px
                                   for key in tiles:                     └─ extract_text(page) → TextPage
                                     if cache.has(key): draw             (один документ = один поток: pdfium не thread-safe;
                                     else: enqueue(key, prio)             N документов = N процессов; тайлы одной страницы — последовательно)
 ◀── tile_ready(key, bitmap) ──  cache.put; repaint(dirty rect)

5.2 Поиск текста с учётом переносов и лигатур

  1. Для каждой страницы извлекается TextPage: глифы с Unicode (из ToUnicode/кодировки/имени глифа; при отсутствии — через OCR-слой или FPDFText_GetUnicode) и прямоугольниками.
  2. Нормализация в «поисковую строку» с таблицей отображения индексов norm_idx → glyph_idx:
    • лигатуры: fi fl ff ffi ffl ij œ→ NFKC-разложение (fi, fl, …) — один глиф даёт несколько символов (все ссылаются на тот же глиф);
    • мягкие переносы (U+00AD) удаляются; перенос на конце строки: если строка заканчивается на -/‐/‑ и следующая начинается со строчной буквы — дефис удаляется, слова склеиваются (в таблице отмечается «виртуальное соединение», чтобы подсветка охватила обе части); если следующая строка — с прописной или дефис между цифрами — сохраняется;
    • пробелы: последовательности пробелов/переводов строк → один пробел; разрыв строки без пробела между словами — вставляется пробел, если расстояние между глифами > 0,2 em или строки разные;
    • регистр (при «без учёта регистра») — to_lowercase с учётом локали (ICU4X casemap), диакритика (при «без учёта диакритики») — NFD + удаление Mn-символов (таблица индексов сохраняется);
    • совместимые символы: «»„“” → ", –— → -, неразрывный пробел → пробел.
  3. Поиск запроса (нормализованного тем же способом) — алгоритм двух путей (memchr/two-way из memchr crate) или regex (с ограничением сложности); «целое слово» — проверка границ по Alphabetic/Numeric.
  4. Совпадение [a, b) в нормализованной строке → множество глифов → объединение прямоугольников по строкам → quadpoints для подсветки (и для создания Highlight-аннотации «по поиску»).
  5. Индекс поиска по документу: нормализованные строки страниц кэшируются; для «поиска во всех открытых» и Hub-индекса — SQLite FTS5 с токенизатором unicode61.

5.3 Извлечение текста в порядке чтения

Если есть дерево структуры (теги) — порядок берётся из него (обход с MCID → глифы). Иначе — геометрический анализ:

1. Глифы → слова: сортировка по строкам текстовых операторов; разрыв слова, если
   gap > max(0.25·size, 0.5·avg_advance) или смена шрифта/размера с изменением базовой линии.
2. Слова → строки: слова с |baseline_i − baseline_j| < 0.3·size и пересечением по вертикали ≥ 50 %;
   порядок в строке по x (для RTL — по bidi-уровням, UAX #9 на уровне строки).
3. Строки → блоки (XY-cut, рекурсивно): по проекционным профилям находим горизонтальные/вертикальные
   «белые» разрезы шириной ≥ t_h = 0.7·median_line_gap (гориз.) и ≥ t_v = 1.5·median_char_width (верт.);
   предпочитаем разрез с бо́льшим зазором; вертикальный разрез допускается, только если обе части содержат ≥ 3 строк
   (защита от разрезания таблиц); рекурсия до блоков без разрезов.
4. Порядок блоков: обход дерева разрезов — для горизонтального разреза «верх, низ», для вертикального —
   «лево, право» (RTL-документ — «право, лево»); внутри блока — строки сверху вниз.
5. Пост-обработка: колонтитулы (строки в верхних/нижних 8 % страницы, повторяющиеся на ≥ 60 % страниц по
   нормализованному тексту с заменой цифр на #) — в конец/исключаются; номера страниц; сноски (блок мелким
   шрифтом ≤ 0.85·body внизу, начинается с цифры/надстрочного) — после основного текста.
6. Повёрнутый текст (матрица с углом ≠ 0): отдельные группы по углу, выводятся после.

5.4 Анализ раскладки для конверсии в Write/Sheets/Slides

pub struct LayoutAnalysis {
    pub body_size: f64,                 // модальный размер шрифта / modal font size
    pub columns: Vec<ColumnSpan>,
    pub paragraphs: Vec<Paragraph>,     // text, style_cls, align, indent_first, indent_left, space_before/after, line_height, bbox, column
    pub headings: Vec<(ParaId, u8)>,    // уровень 1..6
    pub lists: Vec<ListBlock>,          // items: Vec<(ParaId, marker: Marker, level)>
    pub tables: Vec<Table>,             // cells[row][col] = { bbox, paragraphs, span }, borders, header_rows
    pub figures: Vec<Figure>,           // image/vector group, bbox, anchor_para, caption
    pub header_footer: HeaderFooter,
    pub footnotes: Vec<Footnote>,
    pub links: Vec<LinkSpan>,
}

Шаги:

  1. Символы → слова → строки — как в 5.3 (с сохранением стилей прогонов: шрифт, размер, жирность по имени шрифта/StemV/FontWeight, курсив по ItalicAngle/флагу/имени, цвет, подчёркивание — по линиям под базовой).
  2. Колонки: проекционный профиль по x строк блока страницы; вертикальные белые полосы высотой ≥ 40 % высоты текстовой области и шириной ≥ 1,5·средней ширины символа → границы колонок; одинаковые границы на ≥ 50 % страниц → макет документа (1/2/3 колонки в Write-разделе), иначе — колонки на уровне раздела страницы.
  3. Абзацы из строк одной колонки: новая строка начинает абзац, если (а) межстрочный зазор > 1,35·медианы, или (б) отступ первой строки > 1 em при предыдущей строке, не заполненной до правого края (< 85 % ширины колонки), или (в) смена стиля (размер/жирность) всей строки, или (г) предыдущая строка заканчивается знаком конца предложения и короче 70 % ширины, или (д) начинается маркером списка. Выравнивание: по правому краю строк (justify — обе стороны ровные при ≥ 2 строках; center — центры совпадают ±1 em; right). Переносы на концах строк склеиваются (правило 5.2).
  4. Заголовки: абзац ≤ 2 строк, размер ≥ 1,15·body или жирный при размере ≥ body, после него зазор ≥ 0,5·line; уровни — кластеризация размеров (k-means на ≤ 6 кластеров, убывающий порядок); нумерация «1.», «1.2.», «Глава N» усиливает признак; → стили «Заголовок 1–6».
  5. Списки: маркер в начале строки (•◦▪–—*, \d+[.)], [a-zа-я][.)], [ivx]+[.)]) + висячий отступ (следующие строки выровнены по тексту после маркера) → ListBlock с уровнем по отступу (кластеры по x) и типом (маркированный/нумерованный с форматом).
  6. Таблицы: (а) по линиям — горизонтальные/вертикальные пути (прямоугольники высотой/шириной ≤ 2 pt, линии) → сетка по пересечениям (допуск 1,5 pt) → ячейки (объединённые — по отсутствию внутренних линий) → текст в ячейках по bbox; (б) без линий — выравнивание: в области ≥ 3 последовательных строк находятся ≥ 2 общих «столбцовых» x-кластеров начала слов (допуск 0,5 em) с межсловными зазорами > 2 em → кандидат-таблица; столбцы по кластерам, строки по базовым линиям; заголовок — первая строка жирная или с линией под ней; числовые столбцы — выравнивание по правому краю/десятичной точке. Таблица → Write (таблица) или Sheets (лист; числа/даты парсятся локалью документа/пользователя).
  7. Изображения и векторная графика: Image-объекты и группы путей (кластеризация путей с пересекающимися bbox, не линии таблиц) → Figure; якорь — абзац, ближайший сверху в той же колонке; обтекание — «в тексте» если занимает всю ширину колонки, иначе «вокруг рамки» с позицией; подпись — абзац непосредственно под, начинающийся с «Рис./Fig./Figure/Таблица»; векторная группа экспортируется как SVG (Write вставляет как векторный объект) или растр (режим «совместимость»).
  8. Колонтитулы, номера страниц, сноски — как в 5.3; колонтитулы → секции Write (верхний/нижний с полями {page} при распознанных номерах); сноски → сноски Write (ссылка по надстрочной цифре в тексте).
  9. Ссылки (Link-аннотации, URI в тексте) → гиперссылки; закладки → не нужны (заголовки дают навигацию).
  10. Стили: кластеризация (семейство, размер, жирность, курсив, цвет, межстрочный) по частоте → «Обычный» = самый частый; остальные — именованные стили Текст 2, Подпись, … или прямое форматирование, если < 3 абзацев.
  11. Сборка документа Write: разделы по страницам с разным макетом (поля из CropBox минус bbox текста, ориентация, колонки), абзацы в порядке чтения, разрывы страниц — только при «точной вёрстке» или если страница заканчивается неполной (< 70 %) — иначе текст «течёт».
  12. Режим «точная вёрстка»: каждый блок — текстовая рамка с абсолютной позицией (как Acrobat), шрифты подбираются по имени/метрикам (таблица замен: Arial → Liberation Sans, Times → Liberation Serif, Calibri → Carlito, Cambria → Caladea, …), встроенные подмножества — не извлекаются (лицензии), кроме случаев, когда шрифт помечен как встраиваемый без ограничений (fsType = 0) — тогда вставляется как шрифт документа (опция).

Качество измеряется на корпусе (см. раздел 9): доля правильно определённых абзацев/заголовков/таблиц.

5.5 Сохранение: инкрементальное обновление и полная перезапись

pub enum SaveMode { Incremental, Rewrite { object_streams: bool, linearize: bool, remove_unused: bool } }

impl Document {
    pub fn save(&mut self, out: &mut impl Write, mode: SaveMode) -> Result<()> {
        match mode {
            SaveMode::Incremental => {
                assert!(self.source.is_some());
                out.write_all(self.source_bytes())?;                        // исходник байт-в-байт / original bytes verbatim
                if !self.source_bytes().ends_with(b"\n") { out.write_all(b"\n")?; }
                let mut offsets = BTreeMap::new();
                for (id, ch) in &self.changes {                              // только изменённые объекты / changed objects only
                    if let Change::Set(obj) = ch { offsets.insert(*id, out.pos()); write_indirect(out, *id, obj, self.encryption.as_ref())?; }
                }
                let xref_pos = out.pos();
                match self.xref.kind {                                       // та же форма xref, что в исходнике / same xref kind as source
                    XrefKind::Table => write_xref_table(out, &offsets, &self.deleted())?,
                    _ => write_xref_stream(out, &offsets, &self.deleted(), self.alloc_id())?,
                }
                let mut trailer = self.trailer.clone();
                trailer.set("Prev", self.last_startxref());  trailer.set("Size", self.next_num);
                trailer.set("ID", [self.id_first(), new_file_id()]);         // второй элемент /ID обновляется
                write_trailer(out, &trailer, xref_pos)?;
            }
            SaveMode::Rewrite { .. } => {
                let reachable = self.collect_reachable();                     // обход от /Root, /Info, /Encrypt — мусор отбрасывается
                let renum = renumber(&reachable);                             // плотная нумерация 1..N
                write_header(out, self.version())?;
                // объекты без потоков — в объектные потоки (ObjStm) пачками по 100; потоки — отдельно
                // non-stream objects go into object streams (batches of 100); streams stay standalone
                ...
                write_xref_stream(...)?;  write_trailer(...)?;
            }
        }
        Ok(())
    }
}

Правила выбора: подписанный документ → только инкрементально (иначе подписи ломаются; интерфейс предупреждает); «Сохранить» обычного документа после мелких правок → инкрементально (быстро, безопасно); «Сохранить как», оптимизация, redaction, PDF/A, смена шифрования, «удалить старые ревизии» → полная перезапись. Запись — во временный файл рядом + атомарная замена (rename), резервная копия по настройке. Шифрование при записи: строки и потоки шифруются ключом объекта (AES-CBC с IV, или AESV3 для R=6), словари /Encrypt, /ID — нет; метаданные — по /EncryptMetadata.

Линеаризация (P1): по Annex F — первая страница и её ресурсы в начале файла, hint-поток (таблицы смещений страниц и общих объектов), /Linearized словарь с /L /H /O /E /N /T; реализуется как отдельный проход после полной перезаписи (двухпроходная запись с известными длинами).

5.6 Цифровая подпись и проверка

Подписание (PAdES, CMS detached):

1. Подготовка: создать/найти поле подписи (/FT /Sig, виджет с /AP при видимой подписи), словарь подписи:
   /Type /Sig, /Filter /Adobe.PPKLite, /SubFilter /ETSI.CAdES.detached (или /adbe.pkcs7.detached),
   /ByteRange [0 0 0 0] (заполнитель фикс. ширины), /Contents <0000…> (заполнитель: 16 КБ для B-B/B-T,
   32 КБ при больших цепочках), /M (дата D:…), /Reason, /Location, /ContactInfo; для сертификации — /Reference
   с /TransformMethod /DocMDP и /P (1/2/3) + в каталоге /Perms /DocMDP.
2. Записать документ инкрементально с заполнителями; вычислить смещения: ByteRange = [0, a, b, len − b],
   где [a, b) — «<…>» содержимого /Contents; переписать /ByteRange на месте (ширина заполнителя сохранена).
3. Хэш SHA-256 (или 384/512 по политике) по двум диапазонам.
4. CMS SignedData (RFC 5652): eContentType id-data, без eContent (detached); signedAttrs: contentType,
   messageDigest, signingCertificateV2 (ESS, RFC 5035 — обязателен для PAdES), [для adbe — signingTime];
   подпись signedAttrs ключом (RSA-PSS/RSA PKCS#1 v1.5/ECDSA) через провайдер ОС; certificates: цепочка
   без корня (корень — опционально); B-T: unsignedAttrs: signatureTimeStampToken (RFC 3161 запрос к TSA с
   хэшем подписи, ответ — TimeStampToken).
5. DER CMS → hex → записать в /Contents на место заполнителя (дополнить нулями до ширины).
6. B-LT: ещё одно инкрементальное обновление: /DSS в каталоге с /Certs (все сертификаты цепочек, TSA),
   /OCSPs, /CRLs (ответы, полученные при подписании), /VRI по хэшу подписи; B-LTA: затем DocTimeStamp
   (поле подписи /SubFilter /ETSI.RFC3161) поверх всего.

Проверка:

verify(sig):
  bytes = file[ByteRange]; требование: диапазоны покрывают весь файл до конца ревизии подписи (кроме /Contents),
    иначе — "подпись не покрывает документ" (критично).
  cms = parse(Contents); digest_ok = hash(bytes) == messageDigest; sig_ok = verify(signedAttrs, cert.pubkey)
  cert_ok = build_chain(cert, cms.certs ∪ DSS.certs ∪ OS stores) → PKIX (RFC 5280): срок, назначение
    (digitalSignature / nonRepudiation; EKU если есть), ограничения; доверие: корень в хранилище ОС /
    AATL / EUTL / пользовательские; время проверки: метка времени (если есть и доверена) иначе текущее
    (для adbe с signingTime — «заявленное время, не доверено»).
  revocation: OCSP (RFC 6960) → CRL → DSS-встроенные (для LTV — на момент метки времени); без сети → "неизвестно".
  modifications: ревизии после подписанной → классифицировать изменённые объекты:
    разрешённые по DocMDP P: 1 — ничего; 2 — заполнение форм, подписи; 3 — + аннотации/комментарии;
    без DocMDP — как P=3; всё остальное (страницы, контент, ресурсы) → "документ изменён после подписи".
  status = Valid | ValidUntrusted | ValidWithWarnings(список) | Invalid(причина) | Unknown(причина)

Криптография — RustCrypto (cms, x509-cert, rsa, ecdsa/p256/p384, sha2, der), построение цепочки — свой PKIX-валидатор + проверка доверия через ОС (CertGetCertificateChain, SecTrustEvaluateWithError, Linux — /etc/ssl/certs + p11-kit trust) — результат объединяется (строже из двух). Плагины ГОСТ реализуют trait SignatureProvider { fn list_certs(); fn sign(hash_alg, data) -> Cms; fn verify(cms, data) -> Status; fn chain(cert) } через C-ABI в отдельном процессе (провайдер — проприетарный, не линкуется в наш бинарник).

5.7 Redaction — удаление содержимого

apply_redactions(doc, marks: [(page, rect | quads)]):
  for page in pages_with_marks:
    objs = parse_content(page)                          // дерево PageObject с OpRange (4.2)
    edits = []
    for obj in objs (рекурсивно, Form XObject → копия XObject на эту страницу, если он разделяемый):
      R = union(marks[page]) в координатах страницы
      match obj:
        Text run:  for glyph in run: if bbox(glyph) ∩ R ≠ ∅:
                     заменить глиф на позиционную поправку в TJ (−advance·1000/size) → соседние глифы не сдвигаются;
                     если удалены все глифы прогона — удалить операторы прогона целиком; MCID сохраняются
        Path:      if bbox ⊂ R: удалить операторы; elif bbox ∩ R ≠ ∅: геометрия = clip_polygon(path, complement(R))
                   (Vatti/`i_overlay` crate; кривые — сплющиваются до ломаных с допуском 0.1 pt; обводки — по
                   контуру с шириной) → перезаписать операторы; штриховые стили сохраняются
        Image:     if bbox ⊂ R: удалить Do и XObject (если не используется больше нигде);
                   elif ∩: декодировать → закрасить пиксели области (в пространстве изображения через CTM⁻¹) →
                   перекодировать (Flate для lossless/маски, JPEG q=90 для DCT) → новый XObject; SMask — так же;
                   inline image — аналогично на месте
        Shading:   if ∩: добавить клип complement(R) вокруг sh (q … W n … Q)
        Form:      рекурсия по копии; если после удаления пуст — удалить Do
    применить edits к потокам (замены диапазонов с конца к началу); перепаковать Contents одним Flate-потоком
    аннотации: удалить все, чей /Rect ∩ R ≠ ∅ (кроме самих Redact — их тоже), ссылки (Link) — тоже
    текстовый OCR-слой — обрабатывается как текст (невидимый)
    нарисовать поверх: прямоугольник заливки + overlay-текст (новый контент в конце потока)
    структура: StructTree-узлы, оставшиеся без контента, удаляются; закладки с назначениями в R — удаляются
  метаданные/вложения — по флажкам sanitize; затем полная перезапись (старые ревизии и мусор исчезают);
  проверка: повторный парсинг → ни один глиф/изображение/путь не пересекает R; иначе — ошибка, файл не записан

Sanitize: удаление /Info (кроме выбранных), XMP, /EmbeddedFiles и FileAttachment, /JavaScript и /AA/OpenAction с JS, /OCProperties со скрытыми OCG (контент скрытых OCG удаляется), аннотаций, значений полей, /Thumb, /Outlines, Link, объектов вне reachable, скрытого текста (Tr 3, текст под изображениями — по флажку), /PieceInfo, /Metadata в объектах, /ID — новый.

5.8 OCR-конвейер

for page in pages (параллельно по страницам, N = cores/2):
  img = if page — «скан» (один Image-объект ≥ 80 % площади, нет текста): извлечь исходное изображение
            (без перерендера, DPI из CTM; если < 200 dpi — рендер при 300)
        else: render(page, dpi=300, без аннотаций)
  gray = to_gray(img)
  angle = deskew(gray):  проекционный профиль по строкам при углах −15°..+15° шаг 0.5°, затем уточнение
                          0.1° около максимума дисперсии профиля (или преобразование Хафа по краям);
                          поворот билинейно, если |angle| > 0.1°
  orient = tesseract OSD (0/90/180/270) → поворот при необходимости
  bin = sauvola(gray, window=25, k=0.2)  // адаптивная бинаризация, лучше Otsu при неравномерной засветке
  bin = remove_specks(bin, max_area=4 px при 300 dpi); опционально — удаление линий (морфология) для форм
  tsv = tesseract(bin, langs, psm=1 (авто с OSD) | 3 (авто), oem=1 (LSTM)) → слова: text, bbox, conf, line/para/block id
  text layer: для каждой строки:
    BT /GlyphLess 1 Tf 3 Tr  Tm = [size·hscale 0 0 size x y]  (размер = высота строки / 1.0; hscale подгоняет
    ширину слова под bbox: Tz = bbox_w / measured_w·100)  [слово] Tj  ET
    шрифт GlyphLessFont: CIDFontType2 (TrueType с одним пустым глифом, как в tesseract), Identity-H,
    ToUnicode CMap: CID → UTF-16 по словам (многосимвольные соответствия через <bfrange>/<bfchar>)
  записать поток поверх исходного контента (/Contents массив: исходный + новый) + /MarkInfo, артефакты
  conf < 70 → список для панели проверки

Таблицы на сканах (P2): линии через морфологию → ячейки → OCR по ячейкам. MRC-сжатие (P2): маска текста (бинаризованная) → JBIG2/CCITT; фон (изображение без текста, inpainting средним) → JPEG 100 dpi; передний план (цвет текста) → JPEG низкого разрешения; сборка через SMask.

5.9 Сжатие и оптимизация

optimize(doc, profile):
  audit = walk_objects(reachable): классификация (image/font/content/struct/attachment/other) и размер
  images: for xobj in images (дедупликация по SHA-256 потока+словаря заранее):
    eff_dpi = min по всем использованиям(width_px / (bbox_w_pt / 72))   // по CTM каждого Do
    if eff_dpi > profile.dpi[kind] · 1.5: downsample до profile.dpi (Lanczos для фото, box для масок/1-бит,
       1-бит — сначала в серый, ресамплинг, затем порог/дизеринг обратно)
    recode: 1-бит → CCITT G4 (или JBIG2 generic, P2); серые/цветные → JPEG q=profile.q (если был lossless и
       profile allows lossy; изображения с /SMask — остаются Flate с предиктором PNG (Paeth) либо JPEG + Flate-маска);
       JPX → JPEG (если профиль требует совместимости) иначе оставить; 16 бит → 8; /ColorSpace ICCBased → sRGB
       и удаление профиля (если выбрано); /Decode/Interpolate сохраняются
  fonts: группировать по (BaseFont-имя без подмножества-префикса, FontFile-хэш) → одинаковые FontFile — один объект;
         unembed Standard 14 (только по явному выбору и не для PDF/A)
  streams: все не-изображения → Flate level 9 (повторное сжатие, если был LZW/ASCII85/ASCIIHex/RunLength
           или без фильтра); контент-массивы страниц — объединить в один поток
  remove: /Thumb, /Metadata (по выбору), /PieceInfo, /EmbeddedFiles (по выбору), /JavaScript, /AlternateImages,
          пустые OCG, неиспользуемые ресурсы страниц (шрифты/XObject, не упомянутые в контенте), дубликаты
          объектов (по хэшу сериализации)
  rewrite(object_streams = true, linearize = profile.linearize)
  report: до/после по категориям

5.10 Сравнение версий

5.11 Загрузка, восстановление, большие файлы

5.12 Генерация внешнего вида аннотаций

Для каждого типа — функция appearance(annot) -> FormXObject через meridian-render display list → PDF-операторы (тот же экспорт, что у Write):

Тип Внешний вид
Highlight прямоугольники quadpoints, заливка цветом, /ExtGState /BM /Multiply, /CA = непрозрачность
Underline / StrikeOut / Squiggly линия на 1/16 em над нижней границей quad / на середине x-height / зигзаг с шагом 2 pt толщиной 1 pt
Text (заметка) значок 20×20 pt векторный (свой набор, не растр) цветом аннотации
FreeText рамка (/BS), фон (/IC или /DA-цвет), текст по /DA + /DS (CSS-подмножество) + /RC (XHTML-подмножество): раскладка движком Write, подмножество шрифта встраивается в /DR; выноска — линия с наконечником /LE
Line / Square / Circle / Polygon / PolyLine путь, обводка /C шириной /BS /W, штрих /BS /D, заливка /IC, наконечники по таблице /LE (размер = 3×ширина, минимум 6 pt), «облако» (/BE /S /C) — дуги с интенсивностью /I
Ink каждая кривая: точки → сглаживание (Catmull-Rom → кубические Безье, допуск 0,2 pt) → m/c с круглыми концами и стыками (1 J 1 j); нажим (P1) — переменная толщина через заливку контура
Stamp растр (JPEG/Flate XObject, ≤ 2 Мпикс) или векторный форм-XObject с рамкой и текстом (динамические: текст по шаблону)
FileAttachment значок (Pushpin/Paperclip/Graph/Tag) 14×20 pt
Redact до применения: рамка; после — не нужен
Widget (поля) по /MK (цвета, рамка, caption), /DA, /Q; текст полей — движок Write с обрезкой, comb-ячейки; флажки/переключатели — ZapfDingbats-символы (check, circle, cross, diamond, square, star) из /MK /CA с нашим встроенным аналогом

Готовые /AP файла не трогаем, пока аннотацию не изменили; при изменении — регенерация (с сохранением /AP /D, /AP /R только если были и совместимы). Текущая дата → /M, /NM — UUID, /F — Print по умолчанию.

5.13 Конвертация в PDF/A

Последовательность (каждый шаг — правило с идентификатором для отчёта):

  1. Шрифты: все используемые шрифты должны быть встроены — не встроенные: подбор по имени/метрикам (Standard 14 → Liberation/Nimbus; иные — системные при fsType, разрешающем встраивание; иначе — замена на Liberation с предупреждением «метрики могут отличаться»); подмножество по использованным глифам; /CIDToGIDMap, /CIDSet (1b/2b для подмножеств), /ToUnicode обязателен для уровня u/2b/3b (при отсутствии — генерируем из кодировки/имён глифов; если невозможно — предупреждение, 2b допускает).
  2. Цвет: /OutputIntent (sRGB IEC61966-2.1 ICC встраивается), DeviceRGB/Gray — допустимы при OutputIntent; DeviceCMYK без OutputIntent CMYK → конверсия в sRGB через ICC (U.S. Web Coated SWOP) или добавление второго OutputIntent — нет (один); ICCBased с неверными профилями — замена; Lab/Indexed/Separation — проверка альтернативных пространств.
  3. Прозрачность (1b): группы прозрачности, /SMask в ExtGState, /CA /ca < 1, blend modes ≠ Normal → растрирование затронутых областей страницы (рендер области с прозрачностью в изображение при 300 dpi, остальное — векторно; редко — вся страница) с предупреждением; для 2b/3b прозрачность разрешена.
  4. Сжатие: LZW запрещён (1b) → Flate; JPX запрещён (1b) → JPEG/Flate; шифрование — снять; внешние потоки (/F) запрещены.
  5. Аннотации: запрещённые типы (Sound, Movie, 3D, RichMedia, Screen) — удалить; /AP /N обязателен — генерация (5.12); флаг Hidden; Link с URI — допустимы; JavaScript//AA — удалить; Launch/Sound/Movie-действия — удалить.
  6. Метаданные: XMP с pdfaid:part/conformance, синхронизированный с /Info (dc:title, creator, …), /Metadata поток не сжат (1b); /MarkInfo /Marked true и теги — для уровня a (не планируем, только b/u).
  7. Структура: /ID в trailer; /Version ≥ 1.4 (1b) / 1.7 (2b, 3b); нет /OCProperties (1b); /EmbeddedFiles — только 3b (с /AFRelationship, /MIME), для 1b/2b — удалить вложения с предупреждением; XFA — удалить; формы — /NeedAppearances false, /AP у всех полей.
  8. Полная перезапись, затем проверка валидатором; отчёт — список правил с результатами.

Валидатор: таблица правил (идентификатор ISO 19005-x: пункт, описание, предикат над моделью), обход объектов и контент-потоков (операторы, цвета, шрифты, глифы с отсутствующими gid, недопустимые операторы PS, неэкранированные имена, строки > 32767 (1b), массивы > 8191 (1b), целые вне диапазона, вещественные

32767, вложенность q/Q ≤ 28, …); сверка с корпусами veraPDF (corpus/), Isartor, BFO — наш результат должен совпадать с эталоном на ≥ 99 % файлов.

5.14 Редактирование текста в строке

  1. Щелчок → TextBlock под курсором (из 4.2/5.3); блок становится «редактируемым»: строки, прогоны со стилями, исходные глифы и их операторы.
  2. Ввод символов: определяем шрифт прогона; наличие глифа — в встроенном шрифте (по cmap/кодировке: simple-шрифты — через Differences/StandardEncoding → имя глифа → gid; CID — через CMap Identity → gid или через ToUnicode-обратный поиск); если встроено подмножество без нужного глифа — ищем системный шрифт с тем же PostScript-именем/семейством (fontdb, DirectWrite/CoreText/fontconfig) и расширяем подмножество (новый FontFile с объединением глифов: TrueType — слияние glyf/loca/hmtx/cmap через fontations-крейты write-fonts; CFF — пересборка CharStrings); если нет — ближайший по метрикам (Liberation/Carlito/…), прогон получает новый шрифт, пользователь видит предупреждение «шрифт заменён» на панели свойств.
  3. Перекладка строк внутри bbox блока: ширины глифов из /Widths (или из шрифта), межсловные — по /Tw и исходным пробелам; переносы по hyphenation (язык документа); выравнивание — как у блока; при переполнении — блок расширяется вниз (с предупреждением о наложении на объекты ниже; опция «сдвинуть объекты ниже» — P1).
  4. Сериализация: прогоны → BT /F size Tf Tm [(..) kern (..)] TJ ET с Tc/Tw/Tz/TL/Ts по стилям, цвет rg/k/g/cs sc; замена исходного OpRange блока (объединённого по всем операторам блока) на новый текст; MCID сохраняются (BDC/EMC вокруг); ToUnicode шрифта дополняется для новых глифов.
  5. Undo — на уровне команд редактирования блока (модель блока), рендер — оверлей блока (Skia, тот же текстовый движок) до фиксации; фиксация → инкрементальное обновление в память → перерендер страницы pdfium.
  6. Текст в Type3-шрифтах, вертикальный (Identity-V), с Tr 7 (клип) — только удаление/перемещение целиком, не редактирование (предупреждение).

5.15 Чтение XPS/OXPS, DjVu, CBZ


6. MeridianScript API

Пространство имён pdf (типы — meridian-script/types/pdf.d.ts). Все операции над документом — асинхронные, выполняются в ядре; скрипты документа (из /JavaScript PDF) — отдельная песочница Acrobat-совместимого API (2.3), не этот API.

declare namespace pdf {
  function open(path: string, opts?: { password?: string; repair?: boolean }): Promise<PdfDocument>;
  function openBytes(data: Uint8Array, opts?: { password?: string }): Promise<PdfDocument>;
  function create(opts?: { pageSize?: PageSize; version?: "1.7" | "2.0" }): PdfDocument;
  function activeDocument(): PdfDocument | null;
  function documents(): PdfDocument[];
  function merge(sources: (string | PdfDocument | { doc: string | PdfDocument; pages?: PageRange })[], opts?: { bookmarksFromFiles?: boolean }): Promise<PdfDocument>;
  function fromImages(paths: string[], opts?: { pageSize?: PageSize | "image"; margin?: number; compress?: ImageCompress }): Promise<PdfDocument>;
  function fromDocument(path: string, opts?: { pdfa?: PdfALevel; tagged?: boolean }): Promise<PdfDocument>; // docx/odt/xlsx/pptx через Write/Sheets/Slides
  const certificates: CertificateStore;   // list(), find(), import(p12, pwd via dialog)
  const stamps: StampLibrary;
  const ui: PdfUi;
}

type PageRange = string | number[] | { from: number; to?: number };   // "1-3, 7, 10-" или индексы (0-based)
type PdfALevel = "1b" | "2b" | "2u" | "3b";

interface PdfDocument {
  readonly path: string | null; readonly modified: boolean; readonly pageCount: number;
  readonly version: string; readonly encrypted: boolean; readonly signed: boolean; readonly repaired: boolean;
  readonly pages: PdfPageList;  metadata: Metadata;  readonly xmp: string | null;
  readonly outline: OutlineList; readonly attachments: AttachmentList; readonly layers: LayerList;
  readonly form: PdfForm | null; readonly signatures: SignatureList; readonly structure: StructTree | null;
  readonly standards: { pdfa: PdfALevel | null; pdfua: boolean; pdfx: string | null };

  save(opts?: { mode?: "incremental" | "rewrite"; linearize?: boolean }): Promise<void>;
  saveAs(path: string, opts?: SaveOptions): Promise<void>;
  toBytes(opts?: SaveOptions): Promise<Uint8Array>;
  close(discard?: boolean): void;

  insertPages(index: number, source: PdfDocument | string, pages?: PageRange): Promise<void>;
  insertBlankPage(index: number, size?: PageSize): PdfPage;
  deletePages(pages: PageRange): void;  movePages(pages: PageRange, toIndex: number): void;
  rotatePages(pages: PageRange, degrees: 90 | 180 | 270 | -90): void;
  cropPages(pages: PageRange, box: Rect, opts?: { removeOutside?: boolean }): void;
  extract(pages: PageRange): Promise<PdfDocument>;
  split(rule: { every?: number; byBookmarks?: 1 | 2; maxBytes?: number; ranges?: string[] }, namePattern: string, dir: string): Promise<string[]>;

  text(opts?: { pages?: PageRange; readingOrder?: boolean; pageBreaks?: boolean }): Promise<string>;
  find(query: string | RegExp, opts?: { pages?: PageRange; matchCase?: boolean; wholeWord?: boolean; ignoreDiacritics?: boolean }): Promise<Match[]>;
  render(page: number, opts?: { dpi?: number; scale?: number; rect?: Rect; annotations?: boolean; forms?: boolean; transparent?: boolean }): Promise<Bitmap>;
  exportImages(dir: string, opts?: { pages?: PageRange; format?: "png" | "jpeg" | "tiff" | "webp"; dpi?: number; quality?: number }): Promise<string[]>;
  extractImages(dir: string, opts?: { pages?: PageRange; minSize?: number }): Promise<string[]>;
  convert(target: "write" | "sheets" | "slides" | "text" | "html" | "svg", path: string, opts?: ConvertOptions): Promise<void>;

  ocr(opts?: { pages?: PageRange | "scans"; languages?: string[]; dpi?: number; deskew?: boolean; onProgress?: (p: Progress) => void }): Promise<OcrResult>;
  redact(marks: RedactMark[] | { find: string | RegExp | PatternName; pages?: PageRange }, opts?: { overlayText?: string; fill?: Color; sanitize?: boolean }): Promise<RedactReport>;
  sanitize(opts?: SanitizeOptions): Promise<SanitizeReport>;
  optimize(opts?: OptimizeOptions | "screen" | "print" | "minimum"): Promise<OptimizeReport>;
  toPdfA(level: PdfALevel, opts?: { outputIntent?: "sRGB" | { icc: Uint8Array }; rasterizeTransparency?: boolean }): Promise<ConformanceReport>;
  validate(standard: "pdfa" | "pdfua", level?: PdfALevel): Promise<ConformanceReport>;
  autoTag(opts?: { language?: string; title?: string }): Promise<void>;

  sign(opts: SignOptions): Promise<SignatureInfo>;
  verifySignatures(opts?: { online?: boolean }): Promise<SignatureInfo[]>;
  addTimestamp(tsaUrl?: string): Promise<void>;

  setPassword(opts: { user?: string; owner?: string; permissions?: Permissions; cipher?: "aes256" | "aes128" }): void;
  removePassword(): void;

  addWatermark(spec: WatermarkSpec, pages?: PageRange): void;
  addHeaderFooter(spec: HeaderFooterSpec, pages?: PageRange): void;
  addBates(spec: { prefix?: string; suffix?: string; start: number; digits?: number; position?: Position; font?: FontSpec }, pages?: PageRange): number; // возвращает следующий номер
  removeAdded(kind: "watermark" | "headerFooter" | "bates" | "background"): void;

  compare(other: PdfDocument | string, opts?: { mode: "text" | "visual"; threshold?: number }): Promise<CompareResult>;
  flatten(what?: ("annotations" | "forms")[]): void;
  print(opts?: PrintOptions): Promise<void>;
}

interface PdfPage {
  readonly index: number; readonly label: string | null; readonly size: { width: number; height: number }; // pt, с учётом /Rotate
  rotation: number; mediaBox: Rect; cropBox: Rect;
  text(opts?: { readingOrder?: boolean }): Promise<string>;
  words(): Promise<Word[]>;                           // { text, bbox, font, size, line, block }
  layout(): Promise<LayoutAnalysis>;                  // раздел 5.4
  objects(): Promise<PageObject[]>;                   // { kind, bbox, text?, imageRef?, ... }
  readonly annotations: AnnotationList;
  addAnnotation(spec: AnnotationSpec): Annotation;
  addText(text: string, at: Point, style: FontSpec & { color?: Color; width?: number; align?: Align }): Promise<PageObject>;
  addImage(image: Uint8Array | string, rect: Rect, opts?: { compress?: ImageCompress }): Promise<PageObject>;
  addShape(spec: ShapeSpec): PageObject;
  addLink(rect: Rect, target: number | string | { page: number; zoom?: Zoom }): Annotation;
  editText(blockId: string, newText: string): Promise<void>;
  removeObjects(ids: string[]): void;
  moveObject(id: string, dx: number, dy: number): void;
  render(opts?: RenderOptions): Promise<Bitmap>;
}

type AnnotationSpec =
  | { type: "highlight" | "underline" | "strikeout" | "squiggly"; quads?: Quad[]; text?: string /* найти и разметить */; color?: Color; contents?: string }
  | { type: "note"; at: Point; icon?: NoteIcon; contents: string; color?: Color }
  | { type: "freeText"; rect: Rect; text: string; font?: FontSpec; align?: Align; border?: BorderSpec; callout?: [Point, Point, Point] }
  | { type: "line" | "polyline" | "polygon"; points: Point[]; stroke?: StrokeSpec; lineEnds?: [LineEnd, LineEnd]; measure?: MeasureSpec }
  | { type: "square" | "circle"; rect: Rect; stroke?: StrokeSpec; fill?: Color }
  | { type: "ink"; strokes: Point[][]; stroke?: StrokeSpec }
  | { type: "stamp"; rect: Rect; name?: string; image?: Uint8Array; rotation?: number }
  | { type: "fileAttachment"; at: Point; path: string; description?: string }
  | { type: "redact"; rect?: Rect; quads?: Quad[]; overlayText?: string };

interface Annotation { readonly id: string; readonly type: string; readonly page: number; rect: Rect; author: string; contents: string;
  color: Color | null; opacity: number; readonly created: Date; modified: Date; locked: boolean; printable: boolean; hidden: boolean;
  state: "none" | "accepted" | "rejected" | "completed" | "cancelled" | null; readonly replies: Annotation[];
  reply(text: string, author?: string): Annotation; remove(): void; flatten(): void; }

interface PdfForm {
  readonly fields: FieldList;  field(name: string): Field | null;
  export(format: "fdf" | "xfdf" | "json" | "csv"): Promise<Uint8Array>;  import(data: Uint8Array, format?: "fdf" | "xfdf" | "json"): Promise<void>;
  addField(spec: FieldSpec): Field;  flatten(): void;  reset(): void;  readonly isXfa: boolean; readonly xfaDynamic: boolean;
}
interface Field { readonly name: string; readonly type: "text" | "checkbox" | "radio" | "combo" | "list" | "button" | "signature";
  value: string | boolean | string[] | null; readonly options: { value: string; label: string }[]; readOnly: boolean; required: boolean;
  readonly widgets: { page: number; rect: Rect }[]; format: FormatSpec | null; }

interface SignOptions { certificate: Certificate | { p12: string }; field?: string | { page: number; rect: Rect };
  level?: "B-B" | "B-T" | "B-LT" | "B-LTA"; tsa?: string; reason?: string; location?: string; contact?: string;
  certify?: "none" | "formsOnly" | "formsAndAnnotations" | "noChanges"; appearance?: SignatureAppearance; subfilter?: "cades" | "pkcs7"; provider?: string /* "gost-cryptopro" */; }
interface SignatureInfo { readonly field: string; readonly signer: CertInfo | null; readonly signedAt: Date | null; readonly timestamp: { time: Date; tsa: CertInfo } | null;
  readonly status: "valid" | "validUntrusted" | "validWithWarnings" | "invalid" | "unknown"; readonly warnings: string[]; readonly coversWholeDocument: boolean;
  readonly modificationsAfter: { allowed: boolean; description: string }[]; readonly ltv: boolean; report(): string; }

Примеры:

// 1. Пакет: водяной знак «Черновик», нумерация Бейтса сквозная, PDF/A-2b, подпись — для всех PDF в папке
// 1. Batch: "Draft" watermark, continuous Bates numbering, PDF/A-2b, signature — for all PDFs in a folder
import { fs, path } from "meridian/host";
const dir = await pdf.ui.pickFolder("Папка с документами");
const cert = (await pdf.certificates.list({ usage: "sign" })).find(c => c.subject.includes("Иванов"))!;
let bates = 1;
for (const file of await fs.list(dir, { ext: ["pdf"] })) {
  const doc = await pdf.open(file);
  doc.addWatermark({ text: "ЧЕРНОВИК", opacity: 0.15, angle: 45, size: 72, behindContent: true });
  bates = doc.addBates({ prefix: "ABC-", start: bates, digits: 6, position: "bottom-right" });
  const report = await doc.toPdfA("2b");
  if (!report.ok) console.warn(file, report.issues.map(i => i.rule));
  await doc.saveAs(path.join(dir, "out", path.name(file)), { mode: "rewrite" });
  await doc.sign({ certificate: cert, level: "B-LT", tsa: "https://tsa.example.ru", reason: "Согласовано" });
  await doc.save();           // инкрементально — подпись не ломается / incremental — keeps signature valid
  doc.close();
}

// 2. Таблицы из PDF в Sheets с проверкой
// 2. PDF tables into Sheets with validation
import { sheets } from "meridian/apps";
const doc = pdf.activeDocument()!;
const book = sheets.newWorkbook();
for (const page of doc.pages) {
  const { tables } = await page.layout();
  tables.forEach((t, i) => {
    const sh = book.addSheet(`Стр ${page.index + 1} табл ${i + 1}`);
    t.cells.forEach((row, r) => row.forEach((cell, c) => sh.cell(r, c).value = cell.text));
    sh.autoFitColumns();
  });
}

// 3. Найти и удалить (redaction) персональные данные по шаблонам, затем проверить
// 3. Find and redact personal data by patterns, then verify
const d = await pdf.open("/docs/contract.pdf");
const rep = await d.redact({ find: ["email", "phone-ru", "passport-ru", /ИНН\s*\d{10,12}/g] }, { overlayText: "[УДАЛЕНО]", sanitize: true });
console.log(`Удалено областей: ${rep.areas}, страниц: ${rep.pages}`);
await d.saveAs("/docs/contract_redacted.pdf");
const check = await d.find(/@|\+7/);
if (check.length) throw new Error("Остались совпадения");

// 4. Проверка подписей всех входящих PDF из Mail и пометка письма
// 4. Verify signatures of incoming PDFs from Mail and label the message
import { mail } from "meridian/apps";
mail.on("message", async (m) => {
  for (const att of m.attachments.filter(a => a.mime === "application/pdf")) {
    const doc = await pdf.openBytes(await att.bytes());
    const sigs = await doc.verifySignatures({ online: true });
    if (sigs.length && sigs.every(s => s.status === "valid")) m.addLabel("Подписано ✓");
    else if (sigs.some(s => s.status === "invalid")) m.addLabel("Подпись недействительна ✗");
    doc.close();
  }
});

7. Форматы

Формат Чтение Запись Реализация Особенности
PDF 1.0–1.7, 2.0 (ISO 32000-1/2) ✔ ✔ pdfium (рендер, этап A) + meridian-pdf-core все фильтры (Flate, LZW, ASCII85/Hex, RunLength, DCT, CCITT, JBIG2, JPX, Crypt), шифрование RC4/AES-128/AES-256, объектные и xref-потоки, гибридные файлы, инкрементальные ревизии, восстановление повреждённых, PDF 2.0 (UTF-8 строки, AES-256 R6, Associated Files, DocTimeStamp)
PDF/A-1b/2b/2u/3b ✔ (определение, проверка) ✔ (конвертация) свой конвертер и валидатор раздел 5.13; PDF/A-1a/2a/3a (уровень a с тегами) — P2
PDF/UA-1 ✔ (проверка) ✔ (автотеги, редактор) свой P1
PDF/X-1a/3/4 ✔ (проверка) — свой P2
XPS / OXPS ✔ — (→ PDF) свой (meridian-codecs/xps) 5.15; шрифты ODTTF; печать через конверсию
DjVu ✔ — (→ PDF) DjVuLibre как внешний процесс (опц. пакет) / свой декодер (P2) 5.15
CBZ / CB7 / CBR ✔ ✔ (CBZ) zip, sevenz-rust, unrar (динамически, P1) режим чтения комиксов
FDF / XFDF ✔ ✔ свой данные форм и аннотации (обмен с Acrobat)
Изображения (PNG/JPEG/TIFF/WebP/BMP/GIF/HEIC) ✔ (как страницы) ✔ (экспорт страниц) Meridian Codecs (Paint) многостраничный TIFF → страницы
DOCX/ODT/XLSX/PPTX/HTML/TXT/MD ✔ (создать PDF через Write/Sheets/Slides) ✔ (конверсия из PDF) приложения пакета тегированный PDF из структуры документа
SVG — ✔ (страницы) Meridian Render текст как <text> или контуры
EPUB — ✔ (P2) через Write reflowable
Печать — ✔ Meridian Print PDF-принтер ОС

Особенности PDF-кодека для пакета: экспорт из Write/Sheets/Slides/Draw/Paint — через Skia PDF-бэкенд (векторы, текст с подмножествами шрифтов) с постобработкой meridian-pdf-write (теги из структуры документа, закладки, ссылки, метаданные XMP, PDF/A OutputIntent, вложение исходника PDF/A-3, шифрование, подпись); импорт PDF в Write — pdf.convert("write") из этого документа.


8. Горячие клавиши

Схема по умолчанию — как в Acrobat (на macOS Ctrl → ⌘); альтернативные схемы (Foxit, Preview) — в настройках.

Действие Клавиши Действие Клавиши
Открыть / сохранить / сохранить как / закрыть Ctrl+O / Ctrl+S / Ctrl+Shift+S / Ctrl+W Печать Ctrl+P
Поиск / следующий / предыдущий / во всех Ctrl+F / F3 / Shift+F3 / Ctrl+Shift+F Перейти на страницу Ctrl+G (Ctrl+Shift+N)
Следующая / предыдущая / первая / последняя страница →,PgDn / ←,PgUp / Home / End Назад / вперёд по истории Alt+← / Alt+→
Масштаб + / − / 100 % / по ширине / по странице Ctrl++ / Ctrl+− / Ctrl+0 / Ctrl+2 / Ctrl+1 Повернуть вид по / против Ctrl+Shift++ / Ctrl+Shift+−
Режимы: одна / непрерывно / разворот Ctrl+Alt+1 / Ctrl+Alt+2 / Ctrl+Alt+3 Полный экран / чтение Ctrl+L / Ctrl+H
Панели: миниатюры / закладки / комментарии / вложения / слои / подписи F4 затем T/B/C/A/L/S; или Ctrl+Shift+1…6 Скрыть все панели F4
Инструменты: выделение / рука / лупа V / H (пробел — временно) / Z Выделить всё / копировать Ctrl+A / Ctrl+C
Выделение текста / подчёркивание / зачёркивание U / Shift+U / Ctrl+Shift+U→ назначено: K Заметка / свободный текст / карандаш S / T / N
Фигуры: линия / прямоугольник / эллипс / стрелка L / R / O / A Штамп Shift+S
Заполнить и подписать Ctrl+Shift+F→ занято поиском: Ctrl+Shift+W Цифровая подпись Ctrl+Shift+D
Редактировать текст / изображение Ctrl+E / Ctrl+Shift+E Организовать страницы Ctrl+Shift+O
Вставить / удалить / повернуть страницы Ctrl+Shift+I / Ctrl+Shift+Delete / Ctrl+Shift+R (по) Ctrl+Shift+L→ занято: Ctrl+Alt+R (против) Обрезать страницы Ctrl+Shift+T
Redaction: отметить / применить Ctrl+Shift+Y / Ctrl+Shift+Alt+Y Удалить скрытую информацию Ctrl+Shift+Alt+H
OCR Ctrl+Shift+Alt+O В Write / Sheets Ctrl+Shift+Alt+W / Ctrl+Shift+Alt+X
Свойства документа Ctrl+D Защита паролем Ctrl+Shift+P
Отменить / повторить Ctrl+Z / Ctrl+Y Удалить аннотацию Delete
Следующее / предыдущее поле формы Tab / Shift+Tab Следующий комментарий Ctrl+Shift+] / [
Автопрокрутка Ctrl+Shift+H Ночной режим Ctrl+Shift+N→ занято: Ctrl+Alt+N
Вкладки: следующая / предыдущая / закрыть Ctrl+Tab / Ctrl+Shift+Tab / Ctrl+W Командная палитра Ctrl+K

Отмеченные стрелкой конфликты разрешаются в общем реестре docs/shell/keymap.md.


9. Критерии готовности

# Критерий Метрика / способ проверки
1 Открытие и первая страница 100-страничный текстовый PDF: ≤ 300 мс до первой отрисовки; 2000-страничный скан 500 МБ: ≤ 1 с (ленивый xref), память ≤ 300 МБ + кэш
2 Прокрутка 60 fps на непрерывном режиме с превью; точный тайл видимой страницы ≤ 150 мс после остановки (A4 при 100 %, iGPU)
3 Совместимость рендера корпус 50 000 PDF (pdfium test corpus, Ghostscript, pdf.js, свои): 0 падений (песочница), визуальная сверка с Acrobat на 2000 эталонах — ΔE < 2 на ≥ 99 % пикселей
4 Поиск 1000 страниц: индексация ≤ 3 с, поиск ≤ 50 мс; переносы и лигатуры — тест-набор 200 случаев
5 Аннотации все типы 2.2 создаются/читаются/сохраняются; файлы, сохранённые нами, открываются в Acrobat/Foxit/Preview/Chrome с тем же видом (ручная и снимочная проверка 100 файлов); XFDF round-trip
6 Формы 300 реальных форм (гос., банковские, анкеты): заполнение, расчёты, форматы, экспорт FDF/XFDF; статический XFA — отображение
7 Подписи PAdES B-B/B-T/B-LT проверяются валидаторами ETSI (DSS demo), Acrobat показывает «действительна»; проверка — тест-набор ETSI плюс 50 подписанных документов (включая ГОСТ через плагин на стенде)
8 Редактирование текста 100 документов с встроенными подмножествами: правка без смены шрифта в ≥ 90 % (системные шрифты доступны), визуальное совпадение нетронутого текста байт-в-байт
9 Конверсия в Write корпус 500 документов (отчёты, статьи, договоры, прайсы): абзацы верно ≥ 95 %, заголовки ≥ 90 %, таблицы (по линиям) ≥ 90 %, (без линий) ≥ 75 %, колонки ≥ 95 %; измерение — сравнение со вручную размеченным эталоном
10 OCR скан 300 dpi русский/английский: точность по словам ≥ 97 % (tesseract best), 1 страница ≤ 3 с на 4 ядрах, текстовый слой позиционируется в пределах 2 px
11 Redaction после применения ни одно слово/пиксель из областей не извлекается никаким инструментом (pdfium, MuPDF, qpdf — проверка в CI); старые ревизии удалены
12 PDF/A конвертация 1000 документов — валидатор veraPDF подтверждает соответствие ≥ 98 %; наш валидатор совпадает с veraPDF на корпусе veraPDF ≥ 99 %
13 Оптимизация профиль «экран» уменьшает сканы ≥ 60 %, текстовые документы ≥ 20 %, без визуальных дефектов (ΔE < 4 при 96 dpi)
14 Безопасность фаззинг парсера 72 ч; JS форм — песочница без доступа к сети/ФС (тесты), Launch-действия заблокированы; процесс рендера падает → документ не теряется
15 Печать буклет, N-up, масштабирование, аннотации — на трёх ОС; совпадение с Acrobat по положению ±1 мм
16 Доступность NVDA/VoiceOver/Orca читают теги; теги из Write сохраняются при экспорте; проверка PDF/UA проходит на наших экспортах
17 Скрипты API раздела 6 покрыт тестами; 4 примера выше работают end-to-end

10. Риски

Риск Вероятность Влияние Снижение
Лицензии движков: соблазн MuPDF/Poppler ради функций; pdfium-binaries — сторонняя сборка средняя высокое только BSD/MIT/Apache в продукте; своя сборка pdfium в CI (GN) как запасной путь; юридическая проверка списка зависимостей (SBOM)
Два парсера (pdfium + свой) расходятся на нестандартных файлах высокая среднее pdfium — только рендер/текст по байтам, свой core — запись; тесты «записали → pdfium открыл»; режим «только просмотр» при невозможности разобрать структуру
Качество конверсии в Write ниже ожиданий пользователей (эталон — Acrobat/ABBYY) высокая высокое режим «точная вёрстка» как гарантированный; корпус и метрики с фазы 4 начала; итерации по жалобам; ML-классификатор блоков — P2
Шрифты при редактировании текста (подмножества, лицензии на встраивание) высокая среднее честные предупреждения, таблица замен, fsType; никогда не извлекаем шрифты для переиспользования
Криптография и доверие: ошибки проверки = юридические риски средняя очень высокое RustCrypto + ОС-валидаторы (двойная проверка), тест-наборы ETSI, внешний аудит модуля подписи перед 1.0, консервативные статусы («неизвестно» вместо «действительна»)
ГОСТ-провайдеры: проприетарные SDK, лицензии КриптоПро, сертификация ФСБ высокая среднее (Enterprise) плагин вне нашего бинарника, партнёрство; без провайдера функция недоступна, не ломает остальное
DjVu: DjVuLibre GPL средняя низкое внешний процесс и отдельный пакет; свой декодер — P2
JBIG2/JPX патенты и уязвимости (исторически в декодерах) средняя среднее декодеры только в песочном процессе; на этапе B — Rust-реализации с фаззингом
XFA динамические формы (госорганы) средняя среднее честное сообщение; конверсия XFA → AcroForm для простых форм (P2)
Производительность pdfium на огромных страницах (чертежи A0 с миллионами путей) средняя среднее прерываемый рендер, превью, ограничение DPI тайлов, предупреждение «сложная страница»
Объём стандартов (PDF/A валидатор ~300 правил, PDF/UA) высокая среднее приоритет уровня b; корпус veraPDF для верификации; UA — P1
Печать в PDF на Windows (драйвер/port monitor, подпись драйвера) средняя низкое P2; пока — системный «Microsoft Print to PDF» + наш «Сохранить как PDF» из всех приложений

11. Вопросы к библии

  1. Редакция для просмотра PDF. Раздел 9.1 даёт «PDF-просмотр» в Free; предлагается явно включить в Free также заполнение форм, простые подписи «Заполнить и подписать» и комментарии (Highlight/Note/Ink) — это ожидание от любого бесплатного просмотрщика (Preview, Edge); редактирование/OCR/конверсия/защита — Home; подписи PAdES — Home; ГОСТ — Enterprise. Флаги: Feature::PdfView, PdfAnnotate, PdfEdit, PdfOcr, PdfConvert, PdfSign, PdfSignGost.
  2. Процессная модель. «Один процесс на документ-окно» + вкладки: предлагается уточнить — окно PDF с вкладками = один процесс оболочки, каждый документ — свой песочный процесс рендера (рекомендованная схема в этом документе).
  3. Движок pdfium в стеке. В разделе 4 библии pdfium не упомянут; предлагается внести в стек как временную зависимость этапа A с планом перехода (1.4) и зафиксировать запрет на AGPL/GPL-движки.
  4. tesseract и языковые данные. Пакеты tessdata (десятки МБ на язык) распространяются через update-service как «дополнительные компоненты»; требуется решение о зеркале и подписи пакетов.
  5. Общий модуль криптографии и доверия (kernel/crates/meridian-crypto/): нужен также Write (подпись .mwx, раздел 3.1 signatures/), Mail (S/MIME), License. Предлагается выделить общий крейт с провайдерами ОС и плагинами ГОСТ, а не держать его в PDF.
  6. Печать в PDF как виртуальный принтер Windows — требует подписанного драйвера (EV-сертификат есть для MSIX); нужно решение, делать ли (P2) или ограничиться экспортом из приложений.