Технічна стаття

Збереження PDF із точною версією в Delphi: відповідність у PDFiumPas

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

Ця відмінність найбільше важить у поліграфічному виробництві, де профіль PDF/X називає точну версію PDF, а інструмент preflight чи 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 трактує його як живий вміст. ISO 32000-1 розділ 7.5.8.4 навіть описує випадок гібридного посилання, де класична таблиця сумісності позначає об'єкт вільним, тоді як запис /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 чи значення /BitsPerComponent 16, обидва вимагають PDF 1.5, причому шістнадцятибітний випадок прямо слідує правилам компонентів зображення з розділу 4.8 PDF Reference 1.5. Анотації RichMedia та дії RichMediaExecute вимагають /BaseVersion /1.7 з /ExtensionLevel 3 чи вищим. Потоки PRC 3D, ідентифіковані словником, що несе одразу /Type /3D та /Subtype /PRC, вимагають ту саму базову версію, але лише /ExtensionLevel 1. Словники геопросторового вимірювання (Geospatial Measure) та анотації проєкції вимагають /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 на цілі з точною версією як звіт preflight, а не як помилку: повідомлення називає точний пункт, який порушує вихідний документ, а це саме та інформація, яка потрібна друкарні чи архівному конвеєру ще до того, як файл піде далі. Шлях збереження з явною версією, резолвер активної ревізії xref та перевірки рівня розширення Adobe, описані тут, постачаються як частина стандартного компонента PDFiumPas для Delphi та C++Builder; сторінка продукту містить повний довідник TPdf.SaveAs поряд з рештою API відповідності та форм