Technical Article

Валидиране на компресирани PDF файлове: Обектни и XRef потоци

Пишете малък валидатор. Той отваря PDF, търси до края, намира startxref, чете отместването и очаква да попадне на ключовата дума xref с таблица с кръстосани препратки с фиксирана ширина под нея. От тази таблица той събира отместванията на обектите, след което сканира назад за ключовата дума trailer, за да научи /Root и /Size. Работи перфектно на всеки файл, който сте генерирали, за да го тествате. Тогава пристига файл, произведен от съвременна версия на Word или от библиотека, която е насочена към PDF 1.5, и валидаторът го обявява за повреден. Няма ключова дума xref там, където сочи отместването, няма ключова дума trailer никъде, а таблицата с обекти, която валидаторът е изградил, е почти празна. Файлът е валиден. Валидаторът го чете през петнадесетгодишен обектив.

Това е най-честата причина, поради която проверка на ниво байт в PDF, написана спрямо класическото оформление, се проваля на модерни документи. Структурата, от която зависи тя, обикновената текстова таблица с кръстосани препратки и ключовата дума trailer, стана незадължителна в PDF 1.5 и често отсъства. Две функции я замениха: потокът от кръстосани препратки (cross-reference stream) и компресираният обектен поток (compressed object stream). И двете са описани в ISO 32000-1 и валидатор, който не знае за тях, вижда здравия файл как купчина липсващи обекти.

Ключовете на трейлъра са в текстов формат, дори в компресиран файл

Успокояващата част е, че четенето на трейлъра на поток от кръстосани препратки не изисква декомпресиране на нищо. Обект на поток се записва как речник, последван от ключовата дума stream и след това компресираните байтове. Речникът е в чист текстов формат. Така че, когато startxref сочи към поток от кръстосани препратки, байтовете непосредствено след номера на обекта изглеждат как обикновен речник, а /Root, /Size и /ID стоят там в чист вид, преди да започнат ключовата дума stream и Flate данните.

Това означава, че валидаторът може да научи трите факта, от които най-много се нуждае: къде е каталогът, колко обекта твърди, че има файлът, и идентификатора на файла, като парсира само речника на потока. Той не трябва да декомпресира данните за кръстосани препратки и не трябва да интерпретира двоичните записи в тях. Работата, която побеждава простия парсер, не е четенето на трейлъра; тя е намирането на самите обекти. Това са два отделни проблема и решаването на първия е евтино.

Обектни потоци: заглавна част, след това Flate блог

Обектният поток (object stream) е контейнер. Неговият речник носи /Type /ObjStm, запис /N, даващ броя на обектите, опаковани вътре, и запис /First, даващ байтовото отместване в рамките на декомпресираните данни, където започва тялото на първия обект. Компресираният полезен товар, веднъж декомпресиран, започва с малка заглавна част от /N двойки цели числа. Всяка двойка е номер на обект и отместването на тялото на този обект спрямо /First. След заглавната част идват самите тела на обектите, съединени едно след друго.

Разширяването на такъв поток е механично, след като байтовете бъдат декомпресирани. Четете речника, за да получите /N и /First, декомпресирате потока с Flate декодер, обхождате водещите /N двойки, за да разберете кой номер на обект живее на кое отместване, и след това извличате всяко тяло, сякаш е обикновен косвен обект. Единствената реална зависимост е Flate декодерът, а вие вече имате такъв: Delphi доставя System.ZLib, а Free Pascal доставя модула zstream, като и двата обвиват zlib и декомпресират суров Flate поток без никакъв код от трети страни. Подпрограма, която добавя всеки извлечен обект към таблицата с обекти на валидатора, кара останалата част от валидатора, частта, която обхожда /Root и проверява дървото на страниците, да се държи точно така, както би се държала при класически файл.

Какво не е необходимо да имплементирате

Лесно е да се преоцени работата. Четенето на ключовете на трейлъра от компресиран файл не изисква декодиране на двоичните записи в потока за кръстосани препратки. Потокът за кръстосани препратки в §7.5.8 използва три типа записи и записът от тип 2, този, който казва: този обект живее в обектен поток N на индекс i, е това, което бихте декодирали, за да изградите пълна карта на отместванията. Нуждаете се от тази карта, за да разрешавате произволни обекти по номер. Не се нуждаете от нея, за да прочетете /Root, /Size и /ID, които са в чистия текстов речник, и не се нуждаете от нея, за да разширявате обектни потоци, тъй като всеки /ObjStm съобщава собственото си съдържание чрез /N и /First.

Също така не трябва да обработвате филтриращите функции за предсказване (predictor functions) на PNG и TIFF, които потокът за кръстосани препратки може да прилага чрез своя /DecodeParms, само за да получите ключовете на трейлъра. Предсказателите филтрират двоичните редове на кръстосаните препратки, за да ги компресират по-добре; те нямат нищо общо с речника, който предхожда потока. Минималният ъпгрейд, който прави класическия валидатор съвместим с модерния PDF, следователно е малък: когато startxref попадне на поток вместо на ключовата дума xref, парсирайте речника на потока за ключовете на трейлъра и разширете всички /ObjStm обекти, които срещнете, така че съдържанието им да влезе в таблицата с обекти. Декодирането на записи от тип 2 и предсказатели е отделна, по-голяма задача, която можете да отложите, докато действително не се нуждаете от произволно разрешаване на обекти.

Защо проверката за съвместимост трябва първо да разшири потоците

Това спира да бъде академично в момента, в който стартирате проверка на профила. Валидатор на PDF/A or PDF/X инспектира конкретни обекти: каталога на документа за масива /OutputIntents, потока /Metadata за XMP пакет с правилния идентификатор, всеки дескриптор на шрифт за вграден файл с шрифт, трейлъра за /ID. В компресиран файл повечето от тези обекти са вътре в обектни потоци. Валидатор, който не е разширил обектните потоци, не може да види ключовете на каталога, не може да намери метаданните и не може да изброи шрифтовете. Той ще докладва напълно съвместим документ как такъв с липсващо изходно намерение, липсващ XMP и липсваща половина от структурата му, тъй като доказателствата, от които се нуждае, все още стоят в Flate блок, който той никога не е декомпресирал.

Редът има значение. Разширяването трябва да се случи преди провеждането на проверките, а не успоредно с тях, защото всяка проверка предполага, че може да достигне до обект по номер. Ако свържете проверка на профил директно към сурово сканиране на байтове, тя наследява слепотата на класическия парсер и произвежда фалшиви нарушения точно при модерни файлове, които е най-вероятно да бъдат добре оформени, тъй като идват от инструменти, достатъчно нови, за да пишат потоци от кръстосани препратки на първо място.

Оставяне на PDFium да извърши парсирането вместо вас

Компонентът PDFium парсира потоците от кръстосани препратки и обектните потоци като част от зареждането на документ, което е практическият начин да се избегне ръчното изпълнение на стъпката на декомпресиране и разширяване. Когато заредите файл с компонента TPdf, обектите, опаковани в контейнери /ObjStm, вече са разрешени и входните точки за валидиране виждат напълно разширения документ. ValidatePdfA връща запис TPdfAValidationResult, чието поле Conformance е стойност на TPdfAConformance (като pac1b или pacNone), чието поле Issues е набор от конкретните намерени проблеми и чийто метод IsCompliant е истина (true) само когато е открито ниво на съвместимост и наборът от проблеми е празен. Тъй като обектите са били разширени по време на зареждането, масивът /OutputIntents или вграден шрифт, който е живял в обектен поток, се намират, а не се отчитат как липсващи.

uses
  PDFium, FPdfPdfa;

function CheckPdfA(const FileName: string): TPdfAValidationResult;
var
  Pdf: TPdf;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := FileName;
    Pdf.Active := True;            // parses xref/object streams on load
    Result := Pdf.ValidatePdfA;    // sees the expanded object table
  finally
    Pdf.Free;
  end;
end;

Същото се отнася и за ValidatePdfX, който връща TPdfXValidationResult със същата форма. Точката на преминаване през PDFium е, че описаната по-горе структурна декомпресия се случва веднъж, правилно, вътре в лоадера, така че вашият код за валидиране никога не вижда разликата между класически файл и напълно компресиран такъв. И двата пристигат при валидатора как разрешен набор от обекти.

var
  Pdf: TPdf;
  R  : TPdfXValidationResult;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := 'Press_Ready.pdf';
    Pdf.Active := True;
    R := Pdf.ValidatePdfX;
    if R.IsCompliant then
      Writeln('PDF/X conformance: ', Ord(R.Conformance))
    else
      Writeln('Not conformant; issue count = ', SizeOf(R.Issues));
  finally
    Pdf.Free;
  end;
end;

Ако байтовете вече са в паметта, а не на диска, същата последователност зареждане-след-това-валидиране работи чрез претоварването LoadDocument(const Data: TBytes), което приема суровото съдържание на файла и парсира неговите потоци от кръстосани препратки и обекти по същия начин, както и пътя до файла. Изводът за ръчно написан валидатор е структурното правило, а не API: прочетете ключовете на трейлъра от речника на потока в чист текстов формат, разширете всеки /ObjStm обект с Flate декодер, преди да обходите документа, и третирайте декодирането на двоични записи за кръстосани препратки как по-голямата, незадължителна работа, която е.

След като структурата бъде разширена, валидаторът може да стартира останалата част от работния процес върху нея. За команден ред за предпечатен контрол, който докладва съвместимостта в папка с входове, вижте нашето ръководство за изграждане на CLI за партиден предпечатен отчет. Когато валидирането е портал преди разделянето на голям документ на части, техниките в нашето ръководство за разделяне на PDF документи на няколко файла се съчетават естествено с показания тук модел за зареждане и проверка. И двете се изграждат върху повърхността за зареждане и валидиране на PDFium Component за Delphi и C++Builder.