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

Записване на PDF в точна версия с Delphi: съответствие на PDFiumPas

PDFiumPas, обвивката за Delphi и C++Builder около енджина PDFium на Google, записва документ в точно зададена PDF версия от 1.3 до 1.7 чрез параметъра PdfVersion на метода TPdf.SaveAs. Самото извикване FPDF_SaveWithVersion на PDFium променя само заглавката %PDF-M.m, без да проверява дали действителното съдържание на документа е допустимо за тази версия. PDFiumPas запълва тази празнина с проверка за съответствие след записването, която обхожда активната верига от ревизии на таблицата за кръстосани препратки и проверява декларациите за ниво на разширение на Adobe, преди файлът да напусне метода

Това разграничение е най-важно при печатното производство, където профилът PDF/X задава точна PDF версия, а инструментът за предпечатна проверка или RIP отхвърля всеки файл, чиято заглавка тихомълком противоречи на изискването; този сценарий от гледната точка на изходния файл е разгледан в проверката на готови за печат PDF/X документи с PDFiumPas. SaveAs излага целевата версия чрез изброимия тип TPdfVersion, от pv13 до pv17 наред с по-старите стойности pv10 до pv12, както и чрез отделния TSaveOption за инкрементално или пълно презаписване. Подайте PdfVersion и PDFiumPas ще изпълни две задачи в едно извикване: ще поиска от PDFium да постави заявената заглавка, след което ще прочете отново току-що записаните байтове и ще откаже да върне файл, чието активно съдържание не може законно да съществува в тази версия

var
  Pdf: TPdf;
begin
  Pdf:= TPdf.Create(nil);
  try
    Pdf.FileName:= 'source.pdf';
    Pdf.Active:= True;
    try
      Pdf.SaveAs('press-ready.pdf', saNoIncremental, pv17);
    except
      on E: Exception do
        // E.Message names the offending feature and the version or
        // extension level it actually needs, for example:
        // "RichMedia annotations and RichMediaExecute actions require
        // /Extensions /ADBE with /BaseVersion /1.7 and /ExtensionLevel 3
        // or newer."
        raise;
    end;
  finally
    Pdf.Free;
  end;
end;

Защо последната дефиниция на обект във файла не е надеждна?

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

PDFiumPas се сблъска точно с този режим на отказ, преди да започне изрично да следи ревизиите на xref: анотация Redact, останала без собственик след по-късно презаписване на обект на страница, или речник /MarkInfo, физически останал във файла без xref запис към него, можеше да се появи при сканиране на байтовете и да задейства проверка за функция, която вече не се отнасяше за документа, който четецът действително би отворил. Посоката на грешката беше фалшиво отхвърляне, а не фалшиво приемане: файл, който в текущата си ревизия наистина вече не използва дадена функция, можеше да бъде блокиран при записване в по-ниска версия заради съдържание, до което никой вече не можеше да достигне

Как PDFiumPas определя кои дефиниции на обекти са действително активни?

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

Членовете на потоците от обекти получават допълнителна проверка, която обикновеното търсене по отместване не може да осигури самостоятелно; механизмът е разгледан по-подробно в проверката на обекти и потоци за кръстосани препратки с PDFiumPas. Компресиран обект, възстановен от /ObjStm, трябва да има потвърден активен родителски поток в същото обхождане, а индексът му трябва да съвпада със собствената позиция на члена в заглавката на потока, преди PDFiumPas да го приеме за живо съдържание. Раздел 7.5.8.4 на ISO 32000-1 дори описва хибриден случай, при който класическа таблица за съвместимост отбелязва обект като освободен, докато записът /XRefStm в трейлъра едновременно дефинира същия обект като компресиран член на друго място; PDFiumPas слива допълнителния xref поток в същата ревизия, преди да приложи класическите записи, така че компресираната дефиниция печели според замисъла на спецификацията

Нива на разширение на Adobe: прагът над номера на версията

Заглавка %PDF-1.7 обещава само набора от функции, стандартизиран от ISO 32000-1 през 2008 г., докато няколко възможности, на които производителите на PDF разчитат днес, са доставени по-късно като добавки, специфични за Adobe, надградени върху същия номер на версията. Adobe регистрира всяка такава добавка като двойка BaseVersion и ExtensionLevel, записана в речника /Extensions на каталога на документа под префикс на разработчика, като ADBE за собствените разширения на Adobe, за да може четецът да различи обикновен PDF 1.7 от файл, който прилага и номерирано ниво на разширение. Записването с pv17 без тази декларация само по себе си не е грешка; то става грешка едва когато активното съдържание действително зависи от функция, която декларацията трябва да покрива

Кои функции, изискващи по-висока версия, задействат проверката за изрично зададена версия?

PDFiumPas проверява конкретен списък, изведен от спецификацията, вместо да гадае само по номера на версията. Речници на изображения с изричен запис /SMaskInData или със стойност 16 на /BitsPerComponent изискват PDF 1.5, като случаят с шестнадесетте бита следва пряко правилата за компонентите на изображението от раздел 4.8 на PDF Reference 1.5. Анотациите RichMedia и действията RichMediaExecute изискват /BaseVersion /1.7 с /ExtensionLevel 3 или по-високо. Потоковете PRC 3D, разпознавани по речник с едновременно /Type /3D и /Subtype /PRC, изискват същата базова версия, но само /ExtensionLevel 1. Речниците Measure за геопространствени данни и анотациите Projection изискват /BaseVersion /1.7 с /ExtensionLevel 3, същата добавка на Adobe, от която зависи RichMedia

Проверката на геопространственото съдържание има детайл от тълкуването на спецификацията, който е важно да се знае, ако изграждате собствена логика за ограничаване по версия върху PDFiumPas. Таблица 254 на ISO 32000-1 отбелязва записа /Type в речника Measure като незадължителен и посочва само, че „ако присъства, трябва да бъде Measure“, докато таблица 311 прави /Type задължителен за речника на 3D поток, в който се намира съдържанието PRC. Реалният изход GeoPDF от картографски инструменти редовно пропуска /Type в речника Measure и записва само /Subtype /GEO, затова геопространственият детектор на PDFiumPas съвпада само по /Subtype, вместо да изисква двата ключа, както безопасно може да направи детекторът за PRC 3D. Изискването за /Type и в двата речника би позволило на съвместимо GeoPDF съдържание да премине незабелязано през проверката и да попадне в обикновен PDF 1.7 без декларация за ниво на разширение, която да го подкрепя

PDFiumPas автоматично ли понижава версията за неподдържани функции?

Не като обща възможност и именно това е предположението, което трябва да се избегне. SaveAs предава целевата версия към вътрешна процедура ValidatePdfVersionCompliance и когато тя открие функция, която целевата версия или декларацията за нейното ниво на разширение не може да поддържа, SaveAs повдига изключение с текста за грешка на процедурата, вместо да записва файла; извикващият получава точно обяснение с името на функцията, а не тихо пренаписан документ. Единственото място, където PDFiumPas автоматично пренаписва съдържание, е целева версия PDF 1.3, при която премахва семантично неутралните стойности по подразбиране за прозрачност /BM /Normal, /CA 1 и /ca 1, които PDFium винаги записва в речниците ExtGState независимо от целевата версия, тъй като тези конкретни стойности нямат визуално значение, а PDF 1.3 предхожда самите ключове

// PDF 1.3 targets rewrite the saved bytes to strip transparency
// defaults PDFium always emits, so incremental mode cannot apply
Pdf.SaveAs('legacy-archive.pdf', saIncremental, pv13);
// raises: PDF 1.3 normalization is incompatible with incremental
// save mode

Истинската прозрачност, различна от стойностите по подразбиране, и меките маски на изображенията все пак се отхвърлят незабавно при целева версия PDF 1.3, защото премахването им би променило действителния вид на страницата, а PDFiumPas няма да вземе това решение вместо вас. Преди да включите точна версия в пакетен процес, е добре да планирате още две свързани ограничения. Изходът с изрично зададена версия никога не съдържа речник /Encrypt; записването се проваля веднага, ако източникът е защитен, което съвпада с профилите PDF/X и PDF/A, забраняващи шифроването, но означава, че дешифрирането е отделна стъпка във вашия процес, а не нещо, което SaveAs извършва вместо вас. PDFiumPas също няма публичен метод за записване на декларация /Extensions /ADBE в каталога, така че изходен файл с RichMedia, PRC 3D или геопространствено съдържание, но без тази декларация, няма да премине проверката независимо от заявеното PdfVersion; декларацията трябва вече да съществува в източника, обикновено защото инструментът за създаване я е записал, или функцията трябва да бъде премахната преди записването. Свойството само за четене TPdf.PdfVersion заслужава проверка още преди опита за записване в точна версия, тъй като разрешава същата ефективна версия с отчитане на каталога — заглавката или заместването чрез /Version, което е актуално — на която разчита и валидаторът при записване

Pdf.FileName:= 'incoming.pdf';
Pdf.Active:= True;
// PdfVersion resolves the same catalog-aware effective version the
// save-time validator uses, so a mismatch here is worth investigating
// before spending a full SaveAs attempt on it
LogSourceVersion('incoming.pdf', Pdf.PdfVersion);

Приемайте изключението от SaveAs при целева точна версия като отчет от предпечатната проверка, а не като грешка: съобщението назовава точната клауза, която изходният документ нарушава, и това е именно информацията, от която печатницата или архивният процес се нуждае, преди файлът да продължи нататък. Пътят за записване с изрично зададена версия, разрешаването на активните xref ревизии и проверките на нивата на разширение на Adobe, описани тук, са част от стандартния PDFiumPas Component за Delphi и C++Builder; продуктовата страница съдържа пълната справка за TPdf.SaveAs заедно с останалата част от API за съответствие и формуляри