مقاله فنی

Preflight نسخهٔ PDF در Delphi: قواعد Measure بین RL و GEO

PDFlibPas (همان PDF Library for Delphi) پیش از نوشتن فایل، هر object را با جدولی از قواعد نسخهٔ PDF بررسی می‌کند، و تا همین اواخر همان preflight نسخه، dictionaryهای اندازه‌گیری معمولی CAD را با نوع geospatial اشتباه می‌گرفت. یک نقشهٔ CAD تک‌صفحه‌ای راحت بارگذاری می‌شد، بعد SaveToFile مقدار 0 با LastErrorCode برابر 602 برمی‌گرداند و 1.7 ExtensionLevel 3 می‌خواست. قواعد اصلاح‌شده با dictionaryهای /Measure مستقیم‌الزا (با /Subtype /RL) مثل PDF سادهٔ 1.6 رفتار می‌کنند و گیت extension را برای نشانگرهای geospatial واقعی نگه می‌دارند

فایل از مسیر پذیرش کورپوس آمد: یک صفحه، یک گروه optional-content، دو viewport اندازه‌گیری مستقیم‌الزا؛ همان خروجی که یک پکیج CAD معماری می‌نویسد تا viewer بتواند از روی نقشهٔ طبقه فاصله‌ها را بخواند. هیچ چیزش اگزوتیک نبود، و دقیقاً به همین دلیل امتناعش مهم بود. preflightی که یک فایل معتبر را مسدود می‌کند از preflight کند بدتر است، چون به فراخوانی‌کننده یک diagnostic معتبرنما می‌دهد که به ویژگی‌ای اشاره می‌کند سند اصلاً ندارد. fix دو بخش داشت: خواندن spec پشت یکی از قواعد، و این درک که قاعده در سطحی که نگاه می‌کرد نمی‌توانست دو نوع dictionary را از هم تشخیص بدهد

preflight نسخه در زمان ذخیره در PDFlibPas چطور کار می‌کند؟

گیت ذخیره، یعنی PrepareAndCheckSaveVersion، هر object غیرمستقیم را با PDFFeatureRules مقایسه می‌کند و روی اولین قاعده‌ای که هم تطابق دارد و هم بیش از حد مجاز هدف می‌خواهد شکست می‌خورد. هدف، نسخهٔ سند است (یا نسخه‌ای که LockSaveVersion پین کرده) به‌علاوهٔ سطح extension آدوبی که زیر /Extensions /ADBE اعلان شده. هر رکورد TPDFFeatureRule یک MinVersion و یک MinExtensionLevel و یک MatchKind مثل fmkDictKey یا fmkDictSubtype و یک رشتهٔ Match و یک نام Feature خوانا برای انسان و یک callback اختیاری حمل می‌کند. AddRule یک قاعدهٔ نسخهٔ ساده ثبت می‌کند؛ AddExtensionRule همیشه MinVersion را روی 17 پین می‌کند و یک سطح extension به بالایش اضافه می‌کند، پس یک قاعدهٔ extension فقط با PDF 1.7 به‌علاوهٔ درایهٔ درست /Extensions قابل‌ارضا است. وقتی گیت عمل می‌کند، نسخهٔ لازم و نام ویژگی برای فراخوانی‌کننده نگه داشته می‌شوند و کلیدهای 311 و 312 و 313 در GetInformation آن‌ها را در دسترس می‌گذارند

preflight نسخه در زمان ذخیره در PDFlibPas: PrepareAndCheckSaveVersion هر object را با رکوردهای PDFFeatureRules حامل MinVersion و سطح extension و نوع تطبیق مقایسه می‌کند، AddExtensionRule الزام را روی PDF 1.7 به‌علاوهٔ یک سطح extension پین می‌کند، و اولین تطابقی که هدف نمی‌تواند ارضا کند ذخیره را با خطای 602 متوقف می‌کند
کلیدهای 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) بود که روی هر dictionaryای که صرفاً کلید /Measure داشت عمل می‌کرد، و هر viewport اندازه‌گیری یکی دارد. آرایهٔ /VP صفحه dictionaryهای viewport را نگه می‌دارد، هر viewport از طریق /Measure به dictionary اندازه‌گیری‌اش اشاره می‌کند، و تطابق «حضور کلید» همان‌جا تمام می‌شد بدون آنکه نگاه کند dictionary اندازه‌گیری واقعاً چیست. اسکن ویژگی در زمان بارگذاری می‌توانست شمارهٔ نسخهٔ سند را به 1.7 برساند، اما هرگز از طرف یک فایل ورودی اعلان /Extensions نمی‌نویسد، پس گیت ذخیره PDF 1.7 در سطح extension صفر می‌دید و 1.7 ExtensionLevel 3 گزارش می‌کرد. آن امتناع از جعل کردن یک اعلان extension عمدی است: کتابخانه فایل ورودی را بی‌سروصدا ارتقا نمی‌دهد تا قاعده‌ای غلط را بپوشاند

spec دربارهٔ مورد مستقیم‌الزا بی‌ابهام است. dictionaryهای Measure از PDF 1.6 آمدند و ISO 32000-1 §12.9 به /Subtype پیش‌فرض RL می‌دهد، یک دستگاه مختصات مستقیم‌الزا که با مجموعه درایه‌های خودش توصیف می‌شود: نسبت مقیاس، قالب اعداد X و Y، فاصله و مساحت. اندازه‌گیری geospatial افزودنی دیرتر است از Adobe Extension Level 3 روی PDF 1.7، شناسایی‌شده با /Subtype /GEO و حامل آرایه‌های نقطهٔ جغرافیایی و dictionaryهای دستگاه مختصات و واحدهای نمایش؛ همان ساختارهایی که در خواندن viewportهای GeoPDF و آرایه‌های GPTS و LPTS در Delphi پیموده می‌شوند. هر دو dictionary به همان کلید /Measure آویزانند، پس هر قاعده‌ای که در کلید بایستد نمی‌تواند برای هر دو درست باشد. اطلاعات متمایزکننده یک سطح پایین‌تر است، داخل خود dictionary اندازه‌گیری

یک کلید /Measure، دو dictionary در PDFlibPas: اندازه‌گیری مستقیم‌الزا با /RL یا بدون ذکر subtype فقط PDF 1.6 می‌خواهد، در حالی که یک dictionary جیواسپیشیال به 1.7 ExtensionLevel 3 نیاز دارد، پس CB_GeospatialDictionary با محتوا تصمیم می‌گیرد جایی که قاعدهٔ کلیدی قدیمی نمی‌توانستشان را از هم جدا کند
preflightی که فایل معتبر را مسدود کند از preflight کند بدتر است، چون فراخوانی‌کننده یک diagnostic معتبرنما دربارهٔ ویژگی‌ای می‌گیرد که سند هرگز نداشته، و همین است که بررسی متمایزکننده یک سطح پایین‌تر رفت

مجموعهٔ قواعد اصلاح‌شده همچنان چه چیزهایی را اعمال می‌کند؟

fix قاعدهٔ بی‌قید و شرط کلید را حذف می‌کند و گیت‌هایی را که الزامات نسخهٔ واقعی را توصیف می‌کنند باقی می‌گذارد. صفحه‌ای که /VP یا /UserUnit دارد همچنان از طریق CB_PagePDF16Entries به PDF 1.6 نیاز دارد، کلید /PtData همچنان سطح extension 3 می‌خواهد، و CB_GeospatialDictionary با محتوا تصمیم می‌گیرد که یک dictionary اندازه‌گیری geospatial است یا نه، نه با کلیدی که به آن رسیده

// حذف شد: هر dictionary با کلید /Measure geospatial حساب می‌شد
// 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ی که dictionary اندازه‌گیری‌اش /Subtype را نمی‌آورد و یکی که صریحاً /RL می‌نویسد هر دو در PDF 1.6 قبول می‌شوند، همان صفحه در PDF 1.5 همچنان رد می‌شود و تشخیص ویژگی دیگر برایش extension گزارش نمی‌کند. اضافه کردن آرایهٔ /GPTS حکم را به 1.7 ExtensionLevel 3 برمی‌گرداند که وقتی سطح extension اعلان شده قبول می‌شود، و یک dictionary خالی با /Subtype /GEO بدون آن رد می‌شود. callback عمداً محافظه‌کار است: یک dictionary مستقیم‌الزا که اتفاقاً یک کلید سرگردان /GCS یا /PDU هم دارد، geospatial حساب می‌شود، چون آن کلیدها در مدل RL معنایی ندارند

LockSaveVersion جایی است که این تغییر برای فراخوانی‌کننده‌ها دیدنی می‌شود. TPDFlib.LockSaveVersion مقدارهای '1.0' تا '1.7' را می‌پذیرد، برای هر چیز دیگری 0 برمی‌گرداند، نسخهٔ سند را پین می‌کند و جلوی بالا رفتن بی‌سروصدای آن توسط فراخوانی‌های سمت writer را می‌گیرد، اما گیت ذخیره همچنان با مقدار قفل‌شده می‌سنجد. با قواعد اصلاح‌شده، یک فایل CAD قفل‌شده روی 1.6 تمیز ذخیره می‌شود. یک GeoPDF واقعی قفل‌شده روی 1.6 همچنان 602 می‌گیرد، که جواب درست است، و فراخوانی‌های authoring جیواسپیشیال مثل SetMeasureDictCoordinateSystem وقتی آن محتوا را از طریق API می‌سازی خودشان سطح extension 3 را اعلان می‌کنند

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، مثلاً یک dictionary اندازه‌گیری 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 از هر گره هر درخت object می‌گذرد، از اسکالرها هم، پس آن هزینه در تعداد object ضربدر تعداد قواعد می‌شد، و قواعدی که اصلاً به نسخهٔ هدف مربوط نبودند اول کپی و بعد رد می‌شدند. چون PDFFeatureRules یک بار در initialization واحد پر می‌شود و read-only فرض می‌شود، نسخهٔ v3.539.17 درایه‌های جدول را مستقیم به MatchSingleRule و RuleExceedsTarget می‌دهد که پارامترهای const Rule شان بدون دست زدن به رشته‌ها ارجاع می‌گیرند

شتاب‌گیری اسکن قواعد در PDFlibPas: preflight قبلاً قبل از آزمودن، هر رکورد TPDFFeatureRule را کپی می‌کرد و برای هر object بازدیدشده شمارنده‌های ارجاع AnsiString را تنظیم می‌کرد، در حالی که پارامترهای const حالا جدول read-only را درجا می‌خوانند و میانهٔ هر راند تطبیق قواعد را از 0.711 ثانیه به 0.203 ثانیه می‌برند
این سود واقعی اما باریک است: یک ذخیرهٔ کامل هزینهٔ تشخیص ویژگی مؤجل و decode کردن objectها و سریالایز را هم می‌پردازد، پس نسبت اندازه‌گیری‌شده متعلق به مسیر تطبیق قواعد است نه کل زمان ذخیره
// قبل: یک کپی رکورد مدیریت‌شده به‌ازای هر قاعده و هر object بازدیدشده
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تایی از objectهای عددی را ده بار در هر راند با هدف PDF 1.4 بررسی می‌کند؛ بیلدشده با FPC Win64 در -O2، میانهٔ پنج راند از 0.711 ثانیه به 0.203 ثانیه افت کرد و اجرای دو بیلد با ترتیب معکوس 0.459 ثانیه در برابر 0.150 ثانیه داد. تقریباً یک سود 3 برابری، آن هم فقط روی مسیر تطبیق قواعد. یک ذخیرهٔ واقعی هزینهٔ تشخیص ویژگی مؤجل و decode کردن objectها و سریالایز را هم می‌پردازد، پس آن نسبت به کل زمان ذخیره منتقل نمی‌شود. ترتیب قواعد، callbackها، آستانه‌های نسخه و diagnostic اولین شکست بدون تغییرند و برای رسیدن به اینجا هیچ قاعده‌ای بین ذخیره‌ها کش نشد یا رد نشد

وقتی یک PDF بارگذاری‌شده preflight نسخه را رد می‌کند چه باید بررسی کرد؟

قبل از دست زدن به نسخه، کلیدهای 311 و 312 را بخوان. اگر نام ویژگی به یک dictionary geospatial اشاره می‌کند و فایل فقط اندازه‌گیری مستقیم‌الزا رسم می‌کند، همان مثبت کاذب بود و بیلد فعلی فایل را بدون تغییر ذخیره می‌کند. اگر ویژگی واقعی است، یا extension را اعلان کن یا روی نسخه‌ای قفل کن که واقعاً محتوا را در بر دارد؛ بالا بردن نسخه فقط برای ساکت کردن گیت، مسئلهٔ اینکه مصرف‌کننده‌های پایین‌دست می‌توانند چیزهایی که می‌فرستی بخوانند را پنهان می‌کند. همان اصل بررسی‌های محدود و مستند به شواهد، preflight حالت author برای PDF/E-1 در اسناد مهندسی را پیش می‌برد، جایی که نقشه‌های CAD با یک استاندارد همخوانی روبه‌رو می‌شوند نه با یک شمارهٔ نسخه

بررسی‌های همخوانی نسخه، dictionaryهای اندازه‌گیری و geospatial، و قفل کردن نسخهٔ ذخیره همه بخشی از PDF Library for Delphi، جعبه‌ابزار PDFlibPas برای توسعه‌دهندگان Delphi و C++Builder و Lazarus هستند