PDFlibPas (PDF Library for Delphi) звіряє кожен об'єкт із таблицею правил версій PDF, перш ніж записати файл, і донедавна той PDF-version-preflight плутав звичайні CAD-словники вимірювань із геопросторовими. Односторінковий CAD-кресленик завантажувався нормально, а потім SaveToFile повертав 0 з LastErrorCode 602 і вимагав 1.7 ExtensionLevel 3. Виправлені правила трактують прямолінійні словники /Measure (/Subtype /RL) як звичайний PDF 1.6 і лишають extension-гейт для справжніх геопросторових маркерів
Файл прийшов через приймання корпусу: одна сторінка, одна група optional content, два прямолінійні вимірювальні viewport-и — той тип виводу, який пише архітектурний CAD-пакет, щоб переглядач міг зчитувати відстані з плану поверху. Нічого екзотичного — і саме тому відмова мала значення. Preflight, який блокує валідний файл, гірший за повільний, бо викликач отримує солідний на вигляд діагноз, що вказує на фічу, якої в документі немає. Виправлення складалося з двох частин: перечитування специфікації за одним правилом і усвідомлення, що правило не могло розрізнити два типи словників на тому рівні, на який дивилося
Як працює version-preflight збереження в PDFlibPas?
Гейт збереження PrepareAndCheckSaveVersion звіряє кожен непрямий об'єкт із PDFFeatureRules і падає на першому правилі, яке і збігається, і вимагає більшого, ніж дозволяє ціль. Ціль — це версія документа (або версія, закрита LockSaveVersion) плюс рівень розширення Adobe, оголошений під /Extensions /ADBE. Кожен запис TPDFFeatureRule несе MinVersion, MinExtensionLevel, MatchKind на кшталт fmkDictKey чи fmkDictSubtype, рядок Match, зрозумілу людині назву Feature і опційний callback. AddRule реєструє звичайне правило версії; AddExtensionRule завжди закріплює MinVersion на 17 і додає рівень розширення зверху, тож extension-правило може бути задоволене лише PDF 1.7 плюс правильним записом /Extensions. Коли гейт спрацьовує, потрібна версія і назва фічі зберігаються для викликача, і ключі 311, 312 і 313 GetInformation їх віддають
var
Pdf: TPDFlib;
begin
Pdf := TPDFlib.Create;
try
if Pdf.LoadFromFile('floor-plan.pdf', '') <> 1 then
raise Exception.Create('load failed');
if Pdf.SaveToFile('floor-plan-out.pdf') <> 1 then
if Pdf.LastErrorCode = PDFLIB_ERROR_VERSION_COMPLIANCE then
// 311: потрібна версія, 312: фіча, що її спричинила,
// 313: версія, на якій закрита ціль збереження ('' коли відкрито)
Writeln('Needs ', Pdf.GetInformation(311),
' for ', Pdf.GetInformation(312),
', locked at [', Pdf.GetInformation(313), ']');
finally
Pdf.Free;
end;
end;
Чому звичайний CAD-кресленик падав із помилкою 602?
У таблиці правил сиділо AddExtensionRule(3, fmkDictKey, 'Measure', '/Measure geospatial dictionary', Nil), яке спрацьовувало на будь-якому словнику, що просто мав ключ /Measure, а такий ключ є в кожного вимірювального viewport-а. Масив /VP сторінки тримає словники viewport-ів, кожен viewport вказує на свій словник вимірювань через /Measure, і збіг за присутністю ключа там і зупинявся, не дивлячись, чим словник вимірювань насправді був. Скан фіч під час завантаження міг підняти номер версії документа до 1.7, але він ніколи не пише оголошення /Extensions від імені вхідного файлу, тож гейт збереження бачив PDF 1.7 на рівні розширення 0 і звітував 1.7 ExtensionLevel 3. Ця відмова вигадувати оголошення розширення — навмисна: бібліотека не тихо підвищує вхідний файл, щоб замазати правило, яке неправильне
Специфікація щодо прямолінійного випадку однозначна. Словники Measure з'явилися в PDF 1.6, і ISO 32000-1 §12.9 дає /Subtype усталене значення RL — прямолінійну систему координат, описану власним набором записів: масштаб, формати чисел X і Y, відстань і площа. Геопросторові вимірювання — пізніше доповнення з Adobe Extension Level 3 поверх PDF 1.7, розпізнаване за /Subtype /GEO і несе масиви географічних точок, словники систем координат і одиниці показу — структури, які розібрано в читанні viewport-ів GeoPDF і масивів GPTS та LPTS у Delphi. Обидва словники висять на тому самому ключі /Measure, тож будь-яке правило, що зупиняється на ключі, не може бути правильним для обох. Розрізнювальна інформація лежить рівнем нижче — у самому словнику вимірювань
Що виправлений набір правил досі вимагає?
Виправлення видаляє безумовне правило за ключем і лишає гейти, що описують справжні вимоги версій. Сторінка з /VP чи /UserUnit досі потребує PDF 1.6 через CB_PagePDF16Entries, ключ /PtData досі потребує рівня розширення 3, а CB_GeospatialDictionary вирішує, чи словник вимірювань геопросторовий, за його вмістом, а не за ключем, яким він дістався
// Видалено: кожен словник із ключем /Measure рахувався геопросторовим
// AddExtensionRule(3, fmkDictKey, 'Measure', '/Measure geospatial dictionary', Nil);
AddRule(16, fmkCustom, '', 'Page PDF 1.6 entry /UserUnit /VP', CB_PagePDF16Entries);
AddExtensionRule(3, fmkDictKey, 'PtData', '/PtData geospatial dictionary', Nil);
AddExtensionRule(3, fmkCustom, '', 'geospatial measure dictionary', CB_GeospatialDictionary);
function CB_GeospatialDictionary(Obj: TPDFObject; const Ctx: TPDFRuleContext): Boolean;
var
Dict: TPDFDictionary;
begin
Result := False;
if not (Obj is TPDFDictionary) then
Exit;
Dict := TPDFDictionary(Obj);
Result := (Dict.StringValue('Subtype') = 'GEO') or
(Dict.FindIndexByKeyName('GCS') >= 0) or (Dict.FindIndexByKeyName('DCS') >= 0) or
(Dict.FindIndexByKeyName('GPTS') >= 0) or (Dict.FindIndexByKeyName('LPTS') >= 0) or
(Dict.FindIndexByKeyName('PDU') >= 0);
end;
Спільні Delphi- та FPC-регреси закріплюють ту межу з обох боків. Viewport, чий словник вимірювань пропускає /Subtype, і той, що виписує /RL, обидва проходять на PDF 1.6, та сама сторінка досі відкидається на PDF 1.5, і детекція фіч більше не звітує для неї розширення. Додавання масиву /GPTS перекидає вердикт назад на 1.7 ExtensionLevel 3, який проходить щойно рівень розширення оголошено, а голий словник /Subtype /GEO без нього відмовляється. Callback свідомо консервативний: прямолінійний словник, що теж несе зайвий ключ /GCS чи /PDU, трактується як геопросторовий, бо в RL-моделі ті ключі сенсу не мають
LockSaveVersion — місце, де ця зміна стає видимою викликачам. TPDFlib.LockSaveVersion приймає '1.0' до '1.7', повертає 0 на всьому іншому, закріплює версію документа і не дає викликам на боці запису тихо її підняти, але гейт збереження все одно звіряється із закритим значенням. З виправленими правилами CAD-файл, закритий на 1.6, зберігається чисто. Справжній GeoPDF, закритий на 1.6, досі отримує 602 — це правильна відповідь, — а геопросторові авторські виклики на кшталт SetMeasureDictCoordinateSystem самі оголошують рівень розширення 3, коли ви будуєте такий вміст через API
if Pdf.LockSaveVersion('1.6') <> 1 then
raise Exception.Create('unsupported version string');
if Pdf.SaveToFile('floor-plan-16.pdf') <> 1 then
begin
if Pdf.LastErrorCode = PDFLIB_ERROR_VERSION_COMPLIANCE then
// Справжній вміст вище 1.6, наприклад GEO-словник вимірювань
raise Exception.CreateFmt('Locked at 1.6 but %s needs %s',
[string(Pdf.GetInformation(312)), string(Pdf.GetInformation(311))]);
end;
Pdf.UnlockSaveVersion;
Чому скан правил версій був повільнішим, ніж міг бути?
Скан копіював кожен TPDFFeatureRule у локальний запис перед тестуванням, а оскільки запис тримає два поля AnsiString, кожна копія коригувала два лічильники посилань і вивільняла попередні значення. Preflight відвідує кожен вузол кожного дерева об'єктів, включно зі скалярами, тож та ціна множила кількість об'єктів на кількість правил, і правила, які взагалі не стосувалися цільової версії, спершу копіювалися, а потім пропускалися. Оскільки PDFFeatureRules заповнюється один раз при ініціалізації юніта і трактується як read-only, v3.539.17 передає записи таблиці прямо в MatchSingleRule і RuleExceedsTarget, чиї параметри const Rule беруть посилання, не торкаючись рядків
// Раніше: керована копія запису на правило, на відвіданий об'єкт
Rule := PDFFeatureRules[X];
if not RuleExceedsTarget(Rule, TargetVersion, TargetExtensionLevel) then
Continue;
// Тепер: const-параметри читають незмінний запис таблиці на місці
if not RuleExceedsTarget(PDFFeatureRules[X], TargetVersion, TargetExtensionLevel) then
Continue;
if MatchSingleRule(Obj, Ctx, PDFFeatureRules[X]) then
begin
RequiredVersion := RequiredVersionString(PDFFeatureRules[X]);
FeatureName := PDFFeatureRules[X].Feature;
Result := False;
Exit;
end;
Виміряний ефект вузький, і цитувати його треба саме так. Бенчмарк звіряє масив із 20 000 числових об'єктів із ціллю PDF 1.4 по десять разів на раунд; збірка FPC Win64 з -O2 дала медіану п'яти раундів, що впала з 0.711 с до 0.203 с, а прогін двох збірок у зворотному порядку — 0.459 с проти 0.150 с. Це приблизно трикратний виграш лише на шляху збігів правил. Реальне збереження також платить за відкладену детекцію фіч, декодування об'єктів і серіалізацію, тож відношення не переноситься на весь час збереження. Порядок правил, callback'и, пороги версій і діагностика першого збою не змінилися, і жодне правило не кешувалося між збереженнями і не пропускалося заради цього
Що перевіряти, коли завантажений PDF провалює version-preflight?
Читайте ключі 311 і 312, перш ніж торкатися версії. Якщо фіча називає геопросторовий словник, а файл лише малює прямолінійні вимірювання, — це був той самий false positive, і актуальна збірка зберігає файл без змін. Якщо фіча справжня, або оголосіть розширення, або закрийтеся на версію, яка чесно містить цей вміст; підняти версію лише щоб заткнути гейт — значить сховати питання, чи зможуть споживачі нижче по течії прочитати те, що ви поставляєте. Той самий принцип обмежених, підкріплених доказами перевірок веде author-mode-preflight PDF/E-1 для інженерних документів, де CAD-кресленики зустрічаються зі стандартом відповідності, а не з номером версії
Перевірки відповідності версіям, словники вимірювань і геопросторові словники та закриття версії збереження — усе це частина PDF Library for Delphi, тулкіта PDFlibPas для розробників на Delphi, C++Builder і Lazarus