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

PDF version preflight в Delphi: правила Measure RL срещу GEO

PDFlibPas (PDF Library for Delphi) проверява всеки обект срещу таблица с PDF version правила, преди да запише файл, а до скоро този PDF version preflight бъркаше обикновените CAD measurement речници с геопространствените. Едностранен CAD чертеж се зареждаше чисто, после SaveToFile връщаше 0 с LastErrorCode 602 и искаше 1.7 ExtensionLevel 3. Поправените правила третират rectilinear /Measure речници (/Subtype /RL) като обикновен PDF 1.6 и запазват extension вратата за истински геопространствени маркери

Файлът дойде чрез corpus приемане: една страница, една optional-content група, два rectilinear measurement viewport-а — точно такъв изход пише архитектурен CAD пакет, за да може viewer да прочете разстоянията направо от подовия план. Нямаше нищо екзотично в него, което е точно причината отказът да има значение. Preflight, който блокира валиден файл, е по-зле от бавен, защото извикващият получава диагноза с авторитетен вид, сочеща функция, която документът изобщо не съдържа. Поправката имаше две части: прочитането на спецификацията зад едно правило и прозрението, че правилото не можеше да разграничи два типа речници на нивото, на което гледаше

Как работи version preflight-ът при запис в PDFlibPas?

Save вратата, PrepareAndCheckSaveVersion, сравнява всеки indirect обект със PDFFeatureRules и се проваля на първото правило, което и съвпада, и иска повече от това, което целта позволява. Целта е версията на документа (или версията, закована от LockSaveVersion), плюс Adobe extension level-ът, обявен под /Extensions /ADBE. Всеки запис TPDFFeatureRule носи MinVersion, MinExtensionLevel, MatchKind като fmkDictKey или fmkDictSubtype, Match низ, четливо за човек име Feature и опционален callback. AddRule регистрира обикновено version правило; AddExtensionRule винаги закова MinVersion на 17 и добавя extension level отгоре, така че extension правило може да бъде задоволено единствено от PDF 1.7 плюс правилния /Extensions запис. Когато вратата се задейства, нужната версия и името на feature-а се пазят за извикващия, а ключовете 311, 312 и 313 на GetInformation ги излагат

Save-time version preflight в PDFlibPas: PrepareAndCheckSaveVersion сравнява всеки обект с PDFFeatureRules записи, носещи MinVersion, extension level и match kind, AddExtensionRule закова изискването на PDF 1.7 плюс extension level, а първото съвпадение, което целта не може да задоволи, спира записа с грешка 602
Ключовете 311, 312 и 313 на GetInformation превръщат отказа в диагноза, докладвайки нужната версия, feature-ът, я задействал, и закованата цел, така че извикващият да оправи файла или правилото, вместо да гадае
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: feature-ът, я задействал,
        // 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, а всеки measurement viewport има такъв. /VP масивът на страницата държи viewport речници, всеки viewport сочи своя measure речник през /Measure, а съвпадението по присъствие на ключ спираше дотам, без да погледне какъв всъщност е measure речникът. Feature scan-ът при зареждане можеше после да вдигне номера на версията на документа до 1.7, но той никога не пише /Extensions декларация от името на входен файл, така че save вратата виждаше PDF 1.7 на extension level 0 и докладваше 1.7 ExtensionLevel 3. Този отказ да измисли extension декларация е нарочен: библиотеката не промоира тихо входен файл, за да замаже правило, което е грешно

Спецификацията е недвусмислена за rectilinear случая. Measure речниците идват с PDF 1.6, а ISO 32000-1 §12.9 дава на /Subtype подразбиране RL — rectilinear координатна система, описана от собственото си множество записи: мащабно съотношение, формати на числата за X и Y, разстояние и площ. Геопространственото измерване е по-късното добавяне от Adobe Extension Level 3 върху PDF 1.7, разпознаваемо по /Subtype /GEO и носещо geographic point масиви, coordinate system речници и display единици — структурите, обиколени в четенето на GeoPDF viewport-и, GPTS и LPTS масиви в Delphi. И двата речника висят на същия ключ /Measure, така че правило, спиращо на ключа, не може да е вярно и за двата. Различаващата информация седи едно ниво надолу, в самия measure речник

Един /Measure ключ, два речника в PDFlibPas: rectilinear измерването, със /RL или пропуснат subtype, има нужда само от PDF 1.6, докато геопространствен речник иска 1.7 ExtensionLevel 3, затова CB_GeospatialDictionary решава по съдържание, където старото правило по ключ не можеше да ги различи
Preflight, който блокира валиден файл, е по-зле от бавен, защото извикващият получава авторитетна диагноза за функция, която документът никога не е съдържал — затова различаващата проверка слезе едно ниво надолу

Какво поправеният набор правила и нататък налага?

Поправката изтрива безусловното правило по ключ и оставя вратите, описващи истински version изисквания. Страница с /VP или /UserUnit и нататък иска PDF 1.6 чрез CB_PagePDF16Entries, ключ /PtData и нататък иска extension level 3, а CB_GeospatialDictionary решава дали measure речник е геопространствен по съдържанието му, а не по ключа, през който е стигнат

// Премахнато: всеки речник с ключ /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, чиито measure речник пропуска /Subtype, и такъв, който изписва изрично /RL, минават и двата на PDF 1.6, същата страница и нататък се отхвърля на PDF 1.5, а feature разпознаването вече не докладва extension за нея. Добавяне на /GPTS масив връща присъдата обратно на 1.7 ExtensionLevel 3, което минава щом extension level-ът е обявен, а гол /Subtype /GEO речник се отказва без него. Callback-ът е консервативен по дизайн: rectilinear речник, носещ и излишен ключ /GCS или /PDU, се третира като геопространствен, защото тези ключи нямат смисъл в RL модела

LockSaveVersion е мястото, където тази промяна става видима за извикващите. TPDFlib.LockSaveVersion приема '1.0' до '1.7', връща 0 за всичко друго, закова версията на документа и спира writer-ските извиквания да я вдигат тихо, но save вратата и нататък се изпълнява срещу закованата стойност. С поправените правила CAD файл, закован на 1.6, се записва чисто. Истински GeoPDF, закован на 1.6, и нататък получава 602 — правилният отговор — а геопространствените authoring извиквания като SetMeasureDictCoordinateSystem сами обявяват extension level 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 measure речник
    raise Exception.CreateFmt('Locked at 1.6 but %s needs %s',
      [string(Pdf.GetInformation(312)), string(Pdf.GetInformation(311))]);
end;
Pdf.UnlockSaveVersion;

Защо сканирането на version правилата беше по-бавно, отколкото трябва?

Сканът копираше всеки TPDFFeatureRule в локален запис, преди да го тества, а понеже записът държи две AnsiString полета, всяко копие нагласяше две reference броилки и освобождаваше предишните стойности. Preflight-ът обикаля всеки възел на всяко object дърво, вкл. скаларите, така че тази цена се умножаваше по броя обекти и по броя правила, а правила, изобщо неприложими за целевата версия, първо се копираха и после се прескачаха. Понеже PDFFeatureRules се пълни веднъж при unit инициализацията и се третира като read-only, v3.539.17 подава записите от таблицата направо на MatchSingleRule и RuleExceedsTarget, чиито параметри const Rule взимат референция, без да пипат низовете

Ускорение на скана на правила в PDFlibPas: preflight-ът копираше всяка TPDFFeatureRule запис преди тестване, нагласяйки AnsiString reference броилки за всеки посетен обект, докато const параметрите вече четат read-only таблицата на място, сваляйки медианата на round за rule matching от 0.711 s на 0.203 s
Печалбата е истинска, но тясна: пълен запис плаща и за отложено feature разпознаване, декодиране на обекти и сериализация, така че измереното съотношение принадлежи на пътя за rule matching, не на цялото време за запис
// Преди: копие на managed запис за всяко правило, за всеки посетен обект
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;

Измереният ефект е тесен и трябва да се цитира точно така. Benchmark-ът проверява масив от 20 000 числови обекта срещу PDF 1.4 цел десет пъти на round; компилиран с FPC Win64 на -O2, медианата от пет rounds падна от 0.711 s на 0.203 s, а пускането на двата build-а в обратен ред даде 0.459 s срещу 0.150 s. Това е приблизително 3x печалба само на пътя за rule matching. Истински запис плаща и за отложено feature разпознаване, декодиране на обекти и сериализация, така че съотношението не се пренася върху цялото време за запис. Редът на правилата, callback-ите, version праговете и диагностиката при първи провал са непроменени, а нито едно правило не е кеширано между записите или прескачано, за да се стигне дотам

Какво да проверите, когато зареден PDF падне на version preflight-а?

Прочетете ключовете 311 и 312, преди да пипнете версията. Ако feature-ът назовава геопространствен речник, а файлът рисува само rectilinear измервания, това е бил този false positive и текущ build записва файла непроменен. Ако feature-ът е истински, или обявете extension-а, или заковете на версия, която честно съдържа съдържанието; вдигането на версията само за да замълчи вратата крие въпроса дали потребителите надолу по веригата могат да прочетат това, което доставяте. Същият принцип на ограничени, подкрепени с доказателства проверки движе PDF/E-1 author-mode preflight-а за инженерни документи, където CAD чертежи се срещат със стандарт за конформност, а не с номер на версия

Проверките за version съответствие, measurement и геопространствените речници и заковането на версия при запис са част от PDF Library for Delphi — PDFlibPas toolkit-ът за Delphi, C++Builder и Lazarus разработчици