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 آنها را در دسترس میگذارند
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 اندازهگیری
مجموعهٔ قواعد اصلاحشده همچنان چه چیزهایی را اعمال میکند؟
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 شان بدون دست زدن به رشتهها ارجاع میگیرند
// قبل: یک کپی رکورد مدیریتشده بهازای هر قاعده و هر 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 هستند