Meridian PDF — просмотр и редактирование PDF
Документ приложения по соглашениям раздела 8 библии (
docs/00-foundation.md). Версия 0.1 · 2026-10-03 · фаза 4 · редакции: просмотр, печать, заполнение форм и аннотации-комментарии — Free; редактирование, OCR, конверсия, защита, подписи — Home и выше; ГОСТ-подписи, политики — Enterprise. Цвет приложения--app-pdf#C92A2A, значок — лист с загнутым углом, формат — стандартный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 Чем лучше
- Три ОС, один интерфейс (MDS); запуск и первая страница за < 300 мс (ленивый парсинг, кэш тайлов).
- Единый пакет: «Преобразовать в Write» открывает редактируемый документ с настоящими абзацами, таблицами и стилями (анализ раскладки — раздел 5.4), а не «рамки с текстом» как у большинства конвертеров.
- Настоящее удаление (redaction) и «Удалить скрытую информацию» — требования юристов и госсектора.
- Цифровые подписи PAdES с проверкой LTV и поддержка ГОСТ через плагины российских криптопровайдеров (Enterprise); сертификаты из хранилищ ОС без импорта.
- Безопасность: парсинг и рендер чужих PDF в песочном процессе; JavaScript форм — только безопасное подмножество в QuickJS, никаких внешних соединений без разрешения.
- Скрипты: пакетные операции (штампы, нумерация Бейтса, OCR папки, подпись всех файлов) — MeridianScript.
- Открытость: собственный писатель и парсер на Rust без копилефтных зависимостей (раздел 1.4), один код для экспорта из всех приложений пакета.
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++ код → песочница обязательна | то же | то же | память безопасна, но логические ошибки парсера — фаззинг | — |
Решение.
- Рендер и извлечение текста на старте — pdfium
(BSD-3) в отдельном песочном процессе (как все парсеры чужих форматов,
раздел 6 библии), сборка с
pdf_use_skia=trueи без V8/XFA (JavaScript форм — свой на QuickJS, XFA — только статический через AcroForm). Биндинг — свой тонкий C-ABI слой (meridian-pdfium-sys) поверх готовых бинарниковpdfium-binariesна три ОС; при старте проверяется хэш библиотеки. - Объектная модель, писатель и всё редактирование —
собственный крейт
meridian-pdf-core(Rust, MIT/Apache): ленивый парсер объектов (xref-таблицы и потоки, объектные потоки, восстановление повреждённых), инкрементальные обновления, полная перезапись, шифрование, аннотации с генерацией внешнего вида, формы, подписи, redaction, оптимизация, PDF/A/UA, линеаризация. Этот крейт в любом случае нужен пакету для экспорта (Skia PDF-бэкенд даёт только «сырой» PDF без тегов/подписей/PDF/A — мы его постобрабатываем).lopdfиспользуется как справочная реализация и для прототипа, не как зависимость в продукте (нужен ленивый mmap-парсинг и инкрементальные записи, которых в нём нет). - План перехода на собственный рендер
(
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
- /State), автор по умолчанию (Meridian ID или имя пользователя), дата, копирование/вставка аннотаций между страницами и документами, выравнивание и группировка, блокировка (/F Locked), печать с аннотациями или без, экспорт/импорт комментариев FDF/XFDF, «сводка комментариев» (PDF/HTML), сведение (flatten) аннотаций в содержимое, аннотации на планшетах с нажимом (P1), автоматическая генерация внешнего вида (/AP) для всех создаваемых аннотаций (чтобы любой просмотрщик показывал их одинаково).
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 Интеграция с пакетом
- Write/Sheets/Slides/Draw/Paint: «Экспорт в PDF» использует
meridian-pdf-write(теги PDF/UA из структуры документа, закладки из заголовков, ссылки, вложение исходника.mwxкак PDF/A-3 — опция) — P0. - Mail: предпросмотр и аннотирование вложений, «Отправить как PDF» — P0.
- Hub: быстрый просмотр, недавние, поиск по содержимому PDF (индекс текста) — P0/P1.
- Notes: вырезка страницы/области PDF — P1.
- Forms: экспорт формы Meridian Forms в заполняемый AcroForm и импорт ответов — P1.
- Assist (фаза 7): резюме документа, ответы по документу — вне ядра.
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 Цифровая подпись (мастер)
- Сертификат: список из хранилищ ОС (имя, издатель, срок, назначение «подписание документов»), «Из файла PKCS#12…», «Токен/смарт-карта…» (PKCS#11), «Провайдер ГОСТ ▾» (Enterprise).
- Внешний вид: невидимая / видимая (область на странице рисуется мышью или выбирается существующее поле); шаблон (имя, дата, причина, место, DN, логотип, рукописная подпись из профиля), предпросмотр.
- Параметры: причина, место, контакт, уровень (B-B / B-T / B-LT / B-LTA), TSA (из политики или свой URL), сертификация (нет / только формы / формы и аннотации / без изменений), блокировать поля формы.
- Подтверждение: сводка, «после подписи документ будет сохранён как …», кнопка «Подписать» (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)
- Приоритет задачи:
prio = dist_to_viewport_center · 1 + (не текущий масштаб ? 100 : 0) + (prefetch ? 1000 : 0); очередь пересортировывается при каждом изменении viewport; задачи, вышедшие из видимости, отменяются (FPDF_RenderPageBitmap_StartсIFSDK_PAUSE— прерываемый рендер). - Предзагрузка: ±2 страницы от видимых в текущем масштабе; превью (≤ 256 px) для всех страниц — фоновая очередь низкого приоритета (нужна миниатюрам и быстрому показу при прокрутке).
- При прокрутке быстрее 2 страниц/с рисуются превью (масштабированные), точные тайлы — после остановки.
- Рендер страницы в pdfium:
FPDF_RenderPageBitmapWithMatrix(матрица тайла, флагиFPDF_ANNOTпри показе аннотаций файла,FPDF_LCD_TEXTвыкл.,FPDF_NO_NATIVETEXT), результат BGRA →Bitmap(разделяемая память между процессами —memfd/shm/CreateFileMapping, без копирования). - Аннотации Meridian (несохранённые/изменённые) и поля форм рисуются поверх тайлов оверлеем через Skia в координатах страницы (матрица = тайловая) — не инвалидируют кэш тайлов.
- Прозрачность страницы: рендер на прозрачный фон
(
FPDF_RenderPageBitmapс alpha) — для ночного режима и наложений; ночной режим — инверсия в LAB по яркости только для не-изображений (маска изображений строится из списка объектов страницы). - Бюджет кэша по умолчанию 25 % ОЗУ (≤ 2 ГБ), вытеснение LRU по страницам, далёким от viewport.
- Печать/экспорт изображений — отдельный рендер в нужном DPI без кэша (потоковая запись страниц).
5.2 Поиск текста с учётом переносов и лигатур
- Для каждой страницы извлекается
TextPage: глифы с Unicode (из ToUnicode/кодировки/имени глифа; при отсутствии — через OCR-слой илиFPDFText_GetUnicode) и прямоугольниками. - Нормализация в «поисковую строку» с таблицей отображения индексов
norm_idx → glyph_idx:- лигатуры:
fi fl ff ffi ffl ij œ→ NFKC-разложение (fi,fl, …) — один глиф даёт несколько символов (все ссылаются на тот же глиф); - мягкие переносы (U+00AD) удаляются; перенос на конце
строки: если строка заканчивается на
-/‐/‑и следующая начинается со строчной буквы — дефис удаляется, слова склеиваются (в таблице отмечается «виртуальное соединение», чтобы подсветка охватила обе части); если следующая строка — с прописной или дефис между цифрами — сохраняется; - пробелы: последовательности пробелов/переводов строк → один пробел; разрыв строки без пробела между словами — вставляется пробел, если расстояние между глифами > 0,2 em или строки разные;
- регистр (при «без учёта регистра») —
to_lowercaseс учётом локали (ICU4Xcasemap), диакритика (при «без учёта диакритики») — NFD + удалениеMn-символов (таблица индексов сохраняется); - совместимые символы:
«»„“”→",–—→-, неразрывный пробел → пробел.
- лигатуры:
- Поиск запроса (нормализованного тем же способом) — алгоритм двух
путей (
memchr/two-wayизmemchrcrate) илиregex(с ограничением сложности); «целое слово» — проверка границ поAlphabetic/Numeric. - Совпадение
[a, b)в нормализованной строке → множество глифов → объединение прямоугольников по строкам → quadpoints для подсветки (и для создания Highlight-аннотации «по поиску»). - Индекс поиска по документу: нормализованные строки страниц
кэшируются; для «поиска во всех открытых» и 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>,
}Шаги:
- Символы → слова → строки — как в 5.3 (с сохранением
стилей прогонов: шрифт, размер, жирность по имени
шрифта/
StemV/FontWeight, курсив поItalicAngle/флагу/имени, цвет, подчёркивание — по линиям под базовой). - Колонки: проекционный профиль по x строк блока страницы; вертикальные белые полосы высотой ≥ 40 % высоты текстовой области и шириной ≥ 1,5·средней ширины символа → границы колонок; одинаковые границы на ≥ 50 % страниц → макет документа (1/2/3 колонки в Write-разделе), иначе — колонки на уровне раздела страницы.
- Абзацы из строк одной колонки: новая строка начинает абзац, если (а) межстрочный зазор > 1,35·медианы, или (б) отступ первой строки > 1 em при предыдущей строке, не заполненной до правого края (< 85 % ширины колонки), или (в) смена стиля (размер/жирность) всей строки, или (г) предыдущая строка заканчивается знаком конца предложения и короче 70 % ширины, или (д) начинается маркером списка. Выравнивание: по правому краю строк (justify — обе стороны ровные при ≥ 2 строках; center — центры совпадают ±1 em; right). Переносы на концах строк склеиваются (правило 5.2).
- Заголовки: абзац ≤ 2 строк, размер ≥ 1,15·body или жирный при размере ≥ body, после него зазор ≥ 0,5·line; уровни — кластеризация размеров (k-means на ≤ 6 кластеров, убывающий порядок); нумерация «1.», «1.2.», «Глава N» усиливает признак; → стили «Заголовок 1–6».
- Списки: маркер в начале строки
(
•◦▪–—*,\d+[.)],[a-zа-я][.)],[ivx]+[.)]) + висячий отступ (следующие строки выровнены по тексту после маркера) →ListBlockс уровнем по отступу (кластеры по x) и типом (маркированный/нумерованный с форматом). - Таблицы: (а) по линиям — горизонтальные/вертикальные пути (прямоугольники высотой/шириной ≤ 2 pt, линии) → сетка по пересечениям (допуск 1,5 pt) → ячейки (объединённые — по отсутствию внутренних линий) → текст в ячейках по bbox; (б) без линий — выравнивание: в области ≥ 3 последовательных строк находятся ≥ 2 общих «столбцовых» x-кластеров начала слов (допуск 0,5 em) с межсловными зазорами > 2 em → кандидат-таблица; столбцы по кластерам, строки по базовым линиям; заголовок — первая строка жирная или с линией под ней; числовые столбцы — выравнивание по правому краю/десятичной точке. Таблица → Write (таблица) или Sheets (лист; числа/даты парсятся локалью документа/пользователя).
- Изображения и векторная графика:
Image-объекты и группы путей (кластеризация путей с пересекающимися bbox, не линии таблиц) →Figure; якорь — абзац, ближайший сверху в той же колонке; обтекание — «в тексте» если занимает всю ширину колонки, иначе «вокруг рамки» с позицией; подпись — абзац непосредственно под, начинающийся с «Рис./Fig./Figure/Таблица»; векторная группа экспортируется как SVG (Write вставляет как векторный объект) или растр (режим «совместимость»). - Колонтитулы, номера страниц, сноски — как в 5.3;
колонтитулы → секции Write (верхний/нижний с полями
{page}при распознанных номерах); сноски → сноски Write (ссылка по надстрочной цифре в тексте). - Ссылки (Link-аннотации, URI в тексте) → гиперссылки; закладки → не нужны (заголовки дают навигацию).
- Стили: кластеризация (семейство, размер, жирность,
курсив, цвет, межстрочный) по частоте → «Обычный» = самый частый;
остальные — именованные стили
Текст 2,Подпись, … или прямое форматирование, если < 3 абзацев. - Сборка документа Write: разделы по страницам с разным макетом (поля из CropBox минус bbox текста, ориентация, колонки), абзацы в порядке чтения, разрывы страниц — только при «точной вёрстке» или если страница заканчивается неполной (< 70 %) — иначе текст «течёт».
- Режим «точная вёрстка»: каждый блок — текстовая рамка с абсолютной позицией (как 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 Сравнение версий
- Текстовое: для каждой пары страниц (совмещение страниц —
динамическое программирование по сходству текстов страниц, Jaccard по
словам, порог 0,5; вставленные/удалённые страницы — пропуски) — diff по
словам алгоритмом Майерса (
similarcrate, с эвристикой patience), нормализация как в 5.2; результат — spans с quadpoints для подсветки; статистика. - Визуальное: рендер обеих страниц при 150 dpi → размытие 1 px → разность по каналам → порог (настраиваемый, по умолчанию 24/255) → морфологическое закрытие → связные компоненты → прямоугольники изменений (объединение близких); режимы «наложение» (две страницы полупрозрачно, разные цвета), «мигание», «разность».
5.11 Загрузка, восстановление, большие файлы
- mmap исходника; чтение
startxrefс конца (поиск в последних 2 КБ, допускается мусор до 1 КБ); цепочка/Prevи/XRefStm(гибридные файлы); проверка: объект по смещению начинается сN G objс нужными номерами, иначе — реконструкция: сканирование всего файла регулярным выражением\d+ \d+ objиtrailer, последний найденный объект побеждает, корень — по/Type /Catalog; объектные потоки раскрываются при реконструкции; документ помечаетсяrepaired(полоса PD-29, сохранение только полной перезаписью). - Повреждённые потоки (неверный /Length): поиск
endstream; Flate с ошибкой — берётся распакованная часть (inflateдо ошибки) с предупреждением. - Файлы > 2 ГБ: 64-битные смещения (xref-поток), mmap частями (окна 256 МБ) на 32-битных ОС (не целевые); страницы с тысячами объектов — ленивое чтение дерева /Kids (счётчик /Count для индексации без обхода).
- Шифрованные: вычисление ключа по алгоритмам 2 (RC4/AES-128, с MD5) и 2.A/2.B (AES-256, SHA-256/384/512 цикл 64 раундов), проверка user/owner пароля; пустой user-пароль — автоматически.
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
Последовательность (каждый шаг — правило с идентификатором для отчёта):
- Шрифты: все используемые шрифты должны быть встроены — не
встроенные: подбор по имени/метрикам (Standard 14 → Liberation/Nimbus;
иные — системные при fsType, разрешающем встраивание; иначе — замена на
Liberation с предупреждением «метрики могут отличаться»); подмножество
по использованным глифам;
/CIDToGIDMap,/CIDSet(1b/2b для подмножеств),/ToUnicodeобязателен для уровня u/2b/3b (при отсутствии — генерируем из кодировки/имён глифов; если невозможно — предупреждение, 2b допускает). - Цвет:
/OutputIntent(sRGB IEC61966-2.1 ICC встраивается), DeviceRGB/Gray — допустимы при OutputIntent; DeviceCMYK без OutputIntent CMYK → конверсия в sRGB через ICC (U.S. Web Coated SWOP) или добавление второго OutputIntent — нет (один); ICCBased с неверными профилями — замена; Lab/Indexed/Separation — проверка альтернативных пространств. - Прозрачность (1b): группы прозрачности,
/SMaskв ExtGState,/CA /ca < 1, blend modes ≠ Normal → растрирование затронутых областей страницы (рендер области с прозрачностью в изображение при 300 dpi, остальное — векторно; редко — вся страница) с предупреждением; для 2b/3b прозрачность разрешена. - Сжатие: LZW запрещён (1b) → Flate; JPX запрещён (1b) → JPEG/Flate; шифрование — снять; внешние потоки (/F) запрещены.
- Аннотации: запрещённые типы (Sound, Movie, 3D, RichMedia, Screen) —
удалить;
/AP /Nобязателен — генерация (5.12); флаг Hidden; Link с URI — допустимы; JavaScript//AA— удалить; Launch/Sound/Movie-действия — удалить. - Метаданные: XMP с
pdfaid:part/conformance, синхронизированный с /Info (dc:title, creator, …),/Metadataпоток не сжат (1b);/MarkInfo /Marked trueи теги — для уровня a (не планируем, только b/u). - Структура:
/IDв trailer;/Version≥ 1.4 (1b) / 1.7 (2b, 3b); нет/OCProperties(1b);/EmbeddedFiles— только 3b (с/AFRelationship,/MIME), для 1b/2b — удалить вложения с предупреждением; XFA — удалить; формы —/NeedAppearances false,/APу всех полей. - Полная перезапись, затем проверка валидатором; отчёт — список правил с результатами.
Валидатор: таблица правил (идентификатор ISO 19005-x: пункт,
описание, предикат над моделью), обход объектов и контент-потоков
(операторы, цвета, шрифты, глифы с отсутствующими gid, недопустимые
операторы PS, неэкранированные имена, строки > 32767
(1b), массивы > 8191 (1b), целые вне диапазона, вещественные
32767, вложенность q/Q ≤ 28, …); сверка с корпусами veraPDF (
corpus/), Isartor, BFO — наш результат должен совпадать с эталоном на ≥ 99 % файлов.
5.14 Редактирование текста в строке
- Щелчок →
TextBlockпод курсором (из 4.2/5.3); блок становится «редактируемым»: строки, прогоны со стилями, исходные глифы и их операторы. - Ввод символов: определяем шрифт прогона; наличие глифа — в
встроенном шрифте (по 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/…), прогон получает новый шрифт, пользователь видит предупреждение «шрифт заменён» на панели свойств. - Перекладка строк внутри
bboxблока: ширины глифов из /Widths (или из шрифта), межсловные — по /Tw и исходным пробелам; переносы поhyphenation(язык документа); выравнивание — как у блока; при переполнении — блок расширяется вниз (с предупреждением о наложении на объекты ниже; опция «сдвинуть объекты ниже» — P1). - Сериализация: прогоны →
BT /F size Tf Tm [(..) kern (..)] TJ ETсTc/Tw/Tz/TL/Tsпо стилям, цветrg/k/g/cs sc; замена исходногоOpRangeблока (объединённого по всем операторам блока) на новый текст; MCID сохраняются (BDC/EMC вокруг); ToUnicode шрифта дополняется для новых глифов. - Undo — на уровне команд редактирования блока (модель блока), рендер — оверлей блока (Skia, тот же текстовый движок) до фиксации; фиксация → инкрементальное обновление в память → перерендер страницы pdfium.
- Текст в Type3-шрифтах, вертикальный (Identity-V), с
Tr 7(клип) — только удаление/перемещение целиком, не редактирование (предупреждение).
5.15 Чтение XPS/OXPS, DjVu, CBZ
- XPS/OXPS: ZIP (OPC) →
FixedDocumentSequence→FixedDocument→FixedPage(XAML:Path,Glyphs(Indices, UnicodeString, шрифт ODTTF — обфусцированный TrueType, деобфускация XOR GUID),Canvas,ImageBrush,VisualBrush,LinearGradientBrush,RadialGradientBrush, прозрачность, клипы) → список отображения Meridian Render → рендер Skia и/или конвертация в PDF (meridian-pdf-write) для аннотирования и сохранения («Сохранить как PDF»); текст извлекается изGlyphs. - DjVu: формат IFF с чанками
DJVU/DJVM/INFO/Sjbz(JB2)/BG44(IW44)/FG44/FGbz/TXTz/ANTz; декодер — через внешний процессddjvu(DjVuLibre, GPL — вызывается как отдельная программа через pipe, устанавливается отдельным загружаемым пакетом; см. риски) с выводом PNM страниц и текста (djvused), либо собственный декодер JB2/IW44 на Rust (P2, ~2 человеко-месяца); результат — страницы-изображения + текстовый слой; аннотации — только после «Сохранить как PDF». - CBZ/CBR: ZIP/RAR (RAR — через
unrarбиблиотеку с её лицензией freeware, динамически, P1; CB7 —sevenz-rust), изображения в естественном порядке имён (натуральная сортировка), режим «разворот справа налево» (манга),ComicInfo.xml— метаданные/закладки; экспорт в PDF.
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. Вопросы к библии
- Редакция для просмотра PDF. Раздел 9.1 даёт
«PDF-просмотр» в Free; предлагается явно включить в Free также
заполнение форм, простые подписи «Заполнить и подписать» и комментарии
(Highlight/Note/Ink) — это ожидание от любого бесплатного просмотрщика
(Preview, Edge); редактирование/OCR/конверсия/защита — Home; подписи
PAdES — Home; ГОСТ — Enterprise. Флаги:
Feature::PdfView,PdfAnnotate,PdfEdit,PdfOcr,PdfConvert,PdfSign,PdfSignGost. - Процессная модель. «Один процесс на документ-окно» + вкладки: предлагается уточнить — окно PDF с вкладками = один процесс оболочки, каждый документ — свой песочный процесс рендера (рекомендованная схема в этом документе).
- Движок pdfium в стеке. В разделе 4 библии pdfium не упомянут; предлагается внести в стек как временную зависимость этапа A с планом перехода (1.4) и зафиксировать запрет на AGPL/GPL-движки.
- tesseract и языковые данные. Пакеты tessdata
(десятки МБ на язык) распространяются через
update-serviceкак «дополнительные компоненты»; требуется решение о зеркале и подписи пакетов. - Общий модуль криптографии и доверия
(
kernel/crates/meridian-crypto/): нужен также Write (подпись.mwx, раздел 3.1signatures/), Mail (S/MIME), License. Предлагается выделить общий крейт с провайдерами ОС и плагинами ГОСТ, а не держать его в PDF. - Печать в PDF как виртуальный принтер Windows — требует подписанного драйвера (EV-сертификат есть для MSIX); нужно решение, делать ли (P2) или ограничиться экспортом из приложений.