Техническа статия

Линеаризиран PDF изход в Delphi: HotPDF hint таблици

HotPDF записва линеаризирани PDF файлове, layout-ът, който Acrobat нарича Fast Web View, чрез свойството LinearizeOutput на THotPDF. Задаването му преди BeginDoc кара HotPDF да пренареди готовия граф от обекти, така че четец, разбиращ байтови диапазони, да може да покаже страница едно, след като е извлякъл само началната част от файла, вместо първо да изтегли целия документ. Механизмът е ISO 32000-1 Приложение F

Причината това да има значение е недекоративна. Обикновен PDF слага таблицата си за кръстосани референции в края, така че четецът трябва да достигне последния байт, преди да знае къде каквото и да е се намира. Дайте на браузър сканиран отчет от 200 страници и потребителят гледа въртящ се индикатор през целия трансфер, макар единственото, което е искал, да е страница 1. Линеаризацията решава това, като плаща цена в момента на записване. Тази статия разглежда конкретно този път на записване, разделянето, цикъла на измерване и твърдите лимити; за концептуалния фон около това какво Fast Web View всъщност купува, по-ранното обяснение на линеаризацията на PDF и Fast Web View покрива тази територия

Какво всъщност гарантира линеаризираният layout

Линеаризиран файл е обикновен PDF с изключително специфична физическа подредба, и всяка гаранция, която предлага, идва от тази подредба, а не от какъвто и да е нов тип обект. HotPDF излъчва частите в реда, предписан от Приложение F: речника с параметри за линеаризация вътре в първите 1024 байта, ранна таблица за кръстосани референции, обектите на ниво документ, основния hint поток, първата страница и нейните частни обекти, после останалите страници, после споделените обекти, после всичко останало и накрая главната таблица за кръстосани референции

Разделянето е изведено, не декларирано. HotPDF обхожда графа от референции от всеки обект на страница и записва, за всеки индиректен обект, колко страници го достигат и коя страница го е достигнала първа. Обект, използван от точно една страница, става частен за тази страница. Обект, достигнат от повече от една, става споделен. Каталогът, плюс всичко, което той реферира под /ViewerPreferences, /OpenAction, /Threads и /AcroForm, плюс речникът за шифроване, когато защита е активна, образуват групата на ниво документ, която трябва да предшества всичко останало. Възлите на дървото на страниците се задържат умишлено, за да не замърсят раздела на първата страница

Речникът с параметри носи числата, от които четецът се нуждае, преди да е прочел каквото и да е друго: /L за общата дължина на файла, /H за офсета и дължината на hint потока, /O за номера на обекта на първата страница, /E за байта, в който завършва разделът на първата страница, /N за броя страници и /T за офсета на записа в главната таблица за кръстосани референции. Всяко от тях е байтов офсет в файл, който все още не съществува в момента, когато трябва да ги запишете

Защо офсетите в hint таблицата трябва да конвергират?

Защото числата в речника с параметри описват файла, който ги съдържа, а промяната на което и да е от тях променя файла. Това е централната трудност на линеаризиращ писател, и точно затова HotPDF измерва многократно, вместо да записва еднократно. Разширете /T от 6 цифри на 7 и речникът с параметри нараства с байт; заглавната част нараства; всеки обект се измества; главната таблица за кръстосани референции се премества; /T вече се нуждае от различна стойност. Layout-ът трябва да достигне фиксирана точка, преди да бъде ангажиран нито един байт от реалния изход

HotPDF се справя с това чрез ограничена итерация. Първо серализира всеки обект в поток за броене, който записва дължина без да пази байтове, така че всеки обект има известен сериализиран размер. После изпълнява проход на layout-а, който присвоява офсети на групата на ниво документ, hint потока, групата на първата страница, по-късните групи страници, споделената група и остатъка, и отчита къде би кацнала главната таблица за кръстосани референции. Този резултат се подава обратно като вход за следващия проход. Цикълът е ограничен до осем опита, а неконвергирането хвърля изключение, вместо да произведе файл с правдоподобно изглеждащи, но грешни офсети

CandidateMainOffset := 0;
for Attempt := 0 to 7 do
begin
  CalculateLayout(CandidateMainOffset, FirstXRefData,
    HintOffset, EndFirstPage, NewMainOffset);
  if NewMainOffset = CandidateMainOffset then
    Break;
  CandidateMainOffset := NewMainOffset;
end;
if NewMainOffset <> CandidateMainOffset then
  raise Exception.Create('Linearization layout did not converge');

Две подробности пазят цикъла от мятане. Речникът с параметри се записва във фиксиран слот от 384 байта, допълнен с интервали, така че собственият му растеж никога не може да дестабилизира layout-а; ако текстът на речника някога надвиши тази резервация, HotPDF хвърля изключение, вместо тихо да измества всичко. И след конвергенцията HotPDF изпълнява още един потвърждаващ проход на layout-а и препроверява дължината на hint потока, защото самият hint поток кодира офсети, които са били известни едва след като layout-ът се е установил. Възвращаемостта на цялото това измерване е, че HotPDF никога не буферира второ копие на документа: щом офсетите са фиксирани, обектите се сериализират директно в целевия поток, с проверка на всяка граница на раздел, че записаните байтове съвпадат с офсета, който е бил обещан

Включване от Delphi

Повърхността на API-то е един булев тип, а единственото му изискване е да го зададете преди генерирането да започне. LinearizeOutput е по подразбиране False, а проходът на layout-а се изпълнява при записа на документа, така че задаването му след EndDoc не постига нищо

var
  PDF: THotPDF;
begin
  PDF := THotPDF.Create(nil);
  try
    PDF.FileName := 'fast-view.pdf';
    PDF.Version := pdf17;
    PDF.LinearizeOutput := True;      // must precede BeginDoc
    PDF.BeginDoc;
    PDF.Canvas.TextOut(72, 72, 'First page');
    PDF.EndDoc;
  finally
    PDF.Free;
  end;
end;

Едно предупреждение за разгръщането надминава всичко от страната на кода. Линеаризацията се изплаща само когато транспортът поддържа HTTP заявки за диапазон. Обслужете същия файл от крайна точка, която го стриймва наведнъж, или от CDN конфигурация, която игнорира Range, и сте си купили по-бавен път на запис и по-голям файл без никаква видима за потребителя полза. Проверете сървъра, преди да проверите кода

Защо линеаризацията надделява над UseXRefStream и UseObjectStreams?

Защото линеаризиращият писател се нуждае от собствен, директно адресируем байтов офсет за всеки обект, а и двете от тези функции точно това отнемат. HotPDF затова излъчва традиционни текстови таблици за кръстосани референции и разопаковани индиректни обекти, когато LinearizeOutput е активирано, дори ако извикващият е задал и UseXRefStream, или UseObjectStreams. Това е умишлено надделяване, не конфликт, който трябва сами да разрешавате

Разсъждението следва от hint таблиците. Hint таблица описва къде започва раздел на страница и колко е дълъг, така че четецът да може да заяви точно този диапазон. Обект, опакован в контейнер /ObjStm, изобщо няма независим офсет; той съществува само като парче вътре в друг компресиран поток, който трябва да бъде извлечен и разкомпресиран като цяло. Ако сте разчитали на обектни потоци за размера на файла, разберете, че линеаризацията и компресията дърпат в противоположни посоки тук, и прочетете компромиса в придружаващия материал за обектни потоци и инкрементални обновявания в HotPDF. Същото напрежение оформя хибридните файлове с референции, които съществуват точно за да поддържат работещи по-стари четци заедно с базирани на потоци таблици, разгледано в статията за хибридни потоци за кръстосани референции в PDF файлове, генерирани от Office

Има и долна граница за версия. Линеаризацията изисква PDF 1.2 или по-нова. Ако избраната версия е по-стара, HotPDF я повишава автоматично, освен ако StrictVersionLock не е зададен, в който случай записването хвърля изключение, вместо тихо да повишава документ, който сте фиксирали нарочно

Стената от 4 GiB и защо HotPDF отказва вместо да съкращава

Hint таблиците за линеаризация съхраняват офсети като 32-битови стойности, така че линеаризиран файл не може да адресира нищо на или над 4 GiB, а HotPDF отхвърля такъв изход с изрично изключение, вместо да записва файл с преливащи офсети. Лимитът не е избор на имплементацията на HotPDF; той е ширината на полетата, дефинирани от Приложение F

Проверката се прилага на три места, и трите имат значение. HotPDF валидира всеки обект, щом сериализираната му дължина е известна, валидира дължината на всеки раздел на страница при изграждане на hint записите и валидира крайната дължина на файла, след като главната таблица за кръстосани референции е оразмерена. Провалянето рано е целият смисъл: hint таблица с тихо съкратен офсет произвежда файл, който се отваря правилно в четец, изтеглящ го изцяло, и се проваля само за клиента с байтови диапазони, за когото линеаризацията е съществувала, което е най-лошият възможен режим на провал, защото тестовият ви четец никога не го възпроизвежда. Ако произвеждате изход от много гигабайти, линеаризацията не е инструментът, а подходът за стрийминг, описан в бележките за Direct File API за работни процеси с големи PDF файлове, е посоката, в която да гледате

Откриване на линеаризация в зареден файл

THotPDF.IsLoadedLinearized отчита дали в момента зареденият документ вече е записан в линеаризирана форма, и отговаря от снимка, взета преди разбора, не от живия поток. HotPDF чете първите 1024 байта от позиция нула на изходния поток, сканира ги за първата ключова дума obj, а после за запис /Linearized със стойност 1, и кешира булевия резултат

var
  PDF: THotPDF;
  PageCount: Integer;
begin
  PDF := THotPDF.Create(nil);
  try
    PageCount := PDF.LoadFromFile('incoming.pdf');
    if (PageCount > 0) and (not PDF.IsLoadedLinearized) then
      Writeln('Source is not Fast Web View ready');
  finally
    PDF.Free;
  end;
end;

Две ограничения в това описание са носещи. Откриването не може да разчита на позицията в потока, защото до момента, в който кодът на приложението задава въпроса, разборчикът вече я е преместил, и не може да прочете наново при нужда, защото LoadFromFile освобождава вътрешния изходен поток, щом зареждането приключи. Оттам дизайнът с улавяне-преди-разбор-и-кеширане. Сканирането е и умишлено буквално относно стойността: приема се само /Linearized 1 или числово еквивалентна форма с изцяло нулева дробна част, защото файл, чийто речник с параметри казва нещо друго, не дава обещанието на Приложение F

Капан в записите на Delphi, който си струва да се открадне

Локалните записи, съдържащи динамични масиви, инициализират своите управлявани полета и нищо друго, и ако държите обикновено поле Count до масива, трябва сами да го изчистите. Това ухапа разделянето за линеаризация по време на разработката, и е от типа бъгове, които струват ден точно защото една платформа го крие

type
  THPDFLinearIndexList = record
    Values: THPDFIntegerArray;  // managed field: cleared for you
    Count: Integer;             // plain field: whatever was on the stack
  end;

// Required, not cosmetic:
Part4 := Default(THPDFLinearIndexList);
Part6 := Default(THPDFLinearIndexList);
Part8 := Default(THPDFLinearIndexList);
Part9 := Default(THPDFLinearIndexList);

Полето с динамичен масив е с преброяване на референции, така че компилаторът го занулява. Count до него е обикновено цяло число без такава гаранция, а неинициализиран Count изпраща самото първо добавяне към произволен индекс. Под Win32 слотът в стека се е оказал да съдържа нула, добавянето е кацнало на индекс 0, и всеки тест е минал. Под Win64 същият код е писал отвъд края на масива. Поуката се обобщава далеч отвъд линеаризацията: когато запис смесва управлявани и неуправлявани полета, присвоете Default(TRecord) и спрете да разсъждавате кои полета компилаторът покрива, и никога не третирайте зелено изпълнение под Win32 като доказателство, че инициализацията е правилна

Членовете LinearizeOutput и IsLoadedLinearized, описани тук, се доставят със стандартния HotPDF Component за Delphi и C++Builder; страницата на продукта носи пълната справка за свойствата, включително правилата за взаимодействие с потоци за кръстосани референции, обектни потоци и заключване на версия