PDFium Component создаёт Fast Web View (линеаризованные) PDF из Delphi через TPdf.SaveAsLinearized. Метод сохраняет текущий документ средствами PDFium, затем переписывает результат по Annex F из ISO 32000-1: объекты первой страницы переезжают вперёд, пишутся два сечения cross-reference, а hint-таблицы смещений страниц и общих объектов говорят вьюеру, какие диапазоны байтов запрашивать для любой страницы
Причина, по которой это кому-то нужно, — сеть. Каталог на 40 MB, раздаваемый из object storage или документного портала, должен показывать первую страницу после первых килобайтов, а прыжок на страницу 380 должен стоить пары запросов HTTP Range, а не полной загрузки. Вьюеры умеют это только на линеаризованном файле. Большинство PDF, выходящих из редакционного конвейера, линеаризованы не потому, что естественный способ записать PDF кладёт таблицу cross-reference в конец и разбрасывает объекты страниц где писателю случилось их выдать
Почему PDFium умеет читать линеаризованные файлы, но не писать их?
У PDFium есть полный читатель линеаризации и ни одного писателя. API доступности FPDFAvail_* распознаёт словарь линеаризации, декодирует hint-таблицы и запрашивает у хоста ровно те диапазоны байтов, что нужны, — на этом построен workflow прогрессивной загрузки и отмены в PDFium Component. Публичный save-ABI, напротив, предлагает лишь простое копирующее сохранение и сохранение с целевой версией PDF. Ничто в нём не переставляет объекты и не выдаёт hint-потоков
PDFium Component закрывает этот зазор, не переизобретая то, что PDFium уже делает хорошо. TPdf.SaveAsLinearized сперва вызывает существующий SaveAs, так что семантическое сохранение остаётся за PDFium: отредактированные страницы, состояние полей форм и обработка версий идут через кодовый путь, обкатанный годами. Затем компонент прогоняет стабильный промежуточный PDF через реструктуризацию по Annex F и пишет линеаризованный результат в вашу цель. При таком разделении работы линеаризации никогда не нужно понимать аннотацию или форму — только объекты, ссылки и смещения байтов
Вызов TPdf.SaveAsLinearized из Delphi
У TPdf.SaveAsLinearized две перегрузки — для TStream и для имени файла, — и обе принимают те же опциональные параметры Option: TSaveOption и PdfVersion: TPdfVersion, что и SaveAs. Обе возвращают True при успехе. Файловая перегрузка пишет во временный файл в целевом каталоге и атомарно подменяет приёмник, так что провалившееся сохранение никогда не оставит недописанный PDF там, где стоял хороший
var
Pdf: TPdf;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := 'catalog-2026.pdf';
Pdf.Active := True;
// ... править страницы, заполнять формы, штамповать — что там делает конвейер ...
if not Pdf.SaveAsLinearized('catalog-2026-web.pdf', saNoIncremental, pv17) then
raise Exception.Create('Linearized save failed');
finally
Pdf.Free;
end;
end;
Правила параметров строги нарочно. saIncremental поднимает ошибку: инкрементальное обновление дописывает новую ревизию в конец файла и немедленно разрушило бы размещённую вперёд раскладку. Целевая версия ниже PDF 1.2 тоже отвергается; pvUnknown сохраняет версию источника. Шифрованному входу нужен saRemoveSecurity: писатель перенумеровывает объекты, а ключи объектного шифрования PDF выводятся из номера объекта. И поскольку линеаризация — полная перезапись, любая существующая цифровая подпись обнуляется. Если надо доставить подписанное и линеаризованное — линеаризуйте сперва, подписывайте потом
Как писатель группирует объекты для Fast Web View
Линеаризация — не просто перестановка первой страницы в начало файла. Писатель разбирает текущую ревизию — будет ли её cross-reference-данные классической таблицей, гибридным файлом или cross-reference-потоком — и разворачивает каждого члена активного object stream в отдельный объект. Затем он строит граф ссылок и сортирует объекты в пять групп: документные объекты, нужные, чтобы открыть файл, объекты первой страницы, объекты, приватные для одной из последующих страниц, объекты, общие для нескольких последующих страниц, и всё остальное
Ловушка этого прохода по графу — /Parent. Каждый словарь страницы указывает вверх по дереву страниц, и наивный обход следует за этим указателем к родительскому узлу, затем вниз через /Kids во всякую сестринскую страницу и её content stream. Через шаг группа первой страницы содержит весь документ, и файл технически линеаризован, но бесполезен для доступа «сначала диапазон». PDFium Component игнорирует ссылку /Parent страницы и узлы дерева страниц вне собираемой страницы — именно это держит секцию первой страницы малой. На 51-страничном регрессионном образце с глубоким деревом страниц секция первой страницы — 5289 байтов, 0,48% от вывода в 1105354 байта
Object stream и cross-reference stream во входе расплющиваются в обычные косвенные объекты поколения 0, каждая ссылка переписывается на новые номера, а вывод использует классические сечения cross-reference. Это стоит некоторого размера файла по сравнению со сжатыми object stream, но упрощает покрытие hint-таблиц и является раскладкой, которую переваривает каждый вьюер, понимающий линеаризацию. Если для конкретного вывода компактность важнее Fast Web View, у PDFium Component есть отдельный писатель object stream для такой работы
Что входит в словарь линеаризации и два сечения xref?
Линеаризованный файл от PDFium Component несёт два классических сечения cross-reference и словарь линеаризации в самом первом объекте. Первое сечение xref следует за словарём линеаризации и покрывает документные объекты и объекты первой страницы, так что вьюер может отрисовать страницу одну из одного лишь начала файла. Главное сечение xref сидит в конце и покрывает всё остальное
Сам словарь линеаризации держит числа, которые вьюеру нужны прежде всего остального: /L — длина файла, /H — смещение и длина hint-потока, /O — номер объекта первой страницы, /E — конец секции первой страницы, /N — число страниц, /T — смещение первой записи в главной таблице xref. Все эти числа зависят от финальной раскладки, а словарь сидит до раскладки. Запись его числами естественной ширины создаёт цикл: длина, выросшая с 999999 до 1000000, добавляет байт, сдвигает каждое последующее смещение, что может сменить ширину другого числа, и так далее
PDFium Component разрывает цикл, резервируя фиксированные 320 байтов под словарь линеаризации. Сначала пишутся все объекты, hint-поток, оба сечения xref и трейлеры; когда все смещения финальны, словарь заполняется на месте и добивается до зарезервированной ширины. Одна тонкая деталь — /T: он обязан указывать на запись объекта 0 главной таблицы xref, а не на ключевое слово xref перед ней. Строгие валидаторы помечают смещение ключевого слова, даже если снисходительные вьюеры файл всё равно открывают
Почему смещения hint-таблиц игнорируют hint-поток?
Смещения hint-таблиц в линеаризованном PDF трактуются так, будто hint-потока в файле нет. Annex F определяет это так, чтобы hint-поток мог сидеть между секцией первой страницы и остальным, не возвращая собственный размер в числа, которые содержит. Практическое следствие для писателя легко испортить: всякое реальное смещение, лежащее после hint-потока, обязано быть уменьшено до попадания в hint-таблицу смещений страниц или общих объектов
Вопрос — уменьшать на сколько. Соблазнительный ответ — длина данных потока, потому что именно она ощущается содержимым hint. Правильный ответ — длина всего косвенного объекта hint, включая N 0 obj, словарь потока, ключевые слова stream и endstream и endobj. Вычтете только длину данных — и каждое последующее смещение уедет на пару десятков байтов. Первая страница всё равно рисуется, потому что живёт до hint-потока, поэтому баг переживает быструю визуальную проверку и проявляется, лишь когда вьюер прыгает на позднюю страницу и запрашивает диапазон, начинающийся посреди не того объекта
Обе hint-таблицы пакуются битовыми полями со старшим битом вперёд — опять же по Annex F. Компонент проверяет результат так, как поступил бы настоящий клиент: разреженное хранилище диапазонов выдаёт только первые 1 KiB из 51-страничного образца, а нативная обработка hint в PDFium запрашивает остальное. Последняя страница открывается без того, чтобы полный файл когда-либо присутствовал
Проверка линеаризованного PDF в Delphi
Самая дешёвая проверка — спросить сам PDFium. TPdf.InspectAvailability прогоняет API доступности по потоку, не загружая документ, и его поле Linearization сообщает plnLinearized, plnNotLinearized или plnUnknown
function IsFastWebView(const FileName: string): Boolean;
var
Stream: TFileStream;
Info: TPdfAvailability;
begin
Stream := TFileStream.Create(FileName, fmOpenRead or fmShareDenyWrite);
try
Info := TPdf.InspectAvailability(Stream);
Result := Info.Linearization = plnLinearized;
finally
Stream.Free;
end;
end;
Это подтверждает, что PDFium распознаёт раскладку; это не доказывает корректность hint. Для настоящего сквозного теста раздайте файл через транспорт с ограниченными диапазонами и откройте позднюю страницу через TPdfProgressiveDocument — так же, как это делает руководство по стримингу больших PDF по требованию с PDFium VCL. Если страница рисуется после выкачивания малой доли файла, hint-таблицы делают свою работу. Стоит добавить в релизный конвейер и второй независимый проверщик линеаризации: вьюеры расходятся в том, насколько прощают сдвинутый /T или слегка неверное смещение
Два предела, о которых помнить. Материализатор объектов по-прежнему отвергает массивы от 2 GiB и больше, так что сверхкрупным документам пока нужна другая стратегия. И линеаризация окупается, только когда сервер уважает запросы HTTP Range; за прокси, обдирающим Accept-Ranges, линеаризованный файл скачивается ровно как любой другой
FAQ
Сохраняет ли TPdf.SaveAsLinearized валидность цифровых подписей?
Нет. TPdf.SaveAsLinearized перенумеровывает и переписывает каждый объект, поэтому существующие подписи обнуляются. Сперва линеаризуйте, затем накладывайте подпись
Может ли PDFium Component линеаризовать шифрованный PDF?
Только если передать saRemoveSecurity. Ключи объектного шифрования зависят от номеров объектов, которые меняет писатель, поэтому вывод пишется без шифрования
Какие версии PDF поддерживает линеаризованный вывод?
TPdf.SaveAsLinearized принимает pvUnknown для сохранения версии источника или любую цель от PDF 1.2 и выше, и отвергает saIncremental, потому что дописанная ревизия сломала бы раскладку
Вывод Fast Web View, прогрессивная загрузка и API доступности — часть PDFium Component for Delphi и C++Builder, оборачивающего PDFium для проектов Delphi, C++Builder и Lazarus