یک PDF که به مرز تولید میرسد، چه صف چاپ باشد، چه بایگانی، چه درگاه بارگذاری مشتری، باید پیش از آنکه چیزی آن را render کند ممیزی شود. ممکن است فایل یک Launch action داشته باشد که برای اجرای برنامهای خارجی سیمکشی شده است، تصاویری داشته باشد که برای چاپ بیش از حد کمکیفیتاند، یک encryption dictionary حمل کند که همان کار چاپی را که فایل برایش ارسال شده ممنوع میکند، یا یک برچسب PDF/A داشته باشد که واقعاً شرایطش را برآورده نمیکند. بازرسی سند در برابر چنین قواعدی پیش از ورود به workflow را preflighting میگویند، و PDFium C API هرآنچه Delphi برای پیادهسازی مستقیم این بررسیها نیاز دارد در اختیارش میگذارد، بدون اینکه حتی یک صفحه render شود
این مقاله خودِ بررسیها را میسازد: چهار کلاس ممیزی، که هرکدام یک روال کوچک هستند و یافتهها را به یک فهرست نتیجه مشترک اضافه میکنند. عناصر تعاملی، سنجههای منبع، وضعیت امنیتی و نشانگرهای استاندارد، همگی با کد عملی پوشش داده میشوند، از جمله حساب عددی آنها. اگر چیزی که لازم دارید machinery پیرامون این بررسیها است، مثل حلقهزدن روی پوشهها در حالت batch، فایلهای گزارش JSON و HTML، و ایزولهسازی per-file، خود PDFium Component یک موتور preflight آماده عرضه میکند و مقاله CLI دستهای preflight همین plumbing را توضیح میدهد. هر دو عمداً یک واژگان exit code مشترک دارند، بنابراین ممیزیای که اینجا مینویسید مستقیم زیر همان driver دستهای جا میافتد
رکورد یافتهها و قرارداد exit code
هر بررسی در یک نوع رکورد flat مینویسد، چون گزینه جایگزین، یعنی اینکه هر بررسی خودش متن آزاد چاپ کند، بعداً قابل شمارش، فیلترکردن یا thresholdگذاری نیست. چهار فیلد کافی است
uses
System.SysUtils, System.Math, System.IOUtils,
System.Generics.Collections, pdfium_lib;
type
TFindingSeverity = (fsInfo, fsWarning, fsError);
TPreflightFinding = record
Severity: TFindingSeverity;
Code: string; // stable machine key, e.g. 'ACT-LAUNCH'
Page: Integer; // 1-based; 0 means document level
Message: string; // for humans; free to reword between releases
end;
TFindings = TList<TPreflightFinding>;
procedure Add(Findings: TFindings; Severity: TFindingSeverity;
const Code: string; Page: Integer; const Msg: string);
var
F: TPreflightFinding;
begin
F.Severity := Severity;
F.Code := Code;
F.Page := Page;
F.Message := Msg;
Findings.Add(F);
end;
ابزارهای پاییندست روی Code کلید میزنند، نه روی متن Message که آزاد است بین نسخهها عوض شود. exit code فرایند هم همان قرارداد سهمقداری مقاله batch را دنبال میکند: 0 یعنی فایل هیچ یافتهای تولید نکرد، 1 یعنی یافته وجود دارد، و 2 یعنی خود ممیزی نتوانست اجرا شود چون فایل parse نشد یا password میخواهد. جدا نگهداشتن code 2 مهم است. یک پوشه پر از اسکن خراب نشانه خرابی اسکنر بالادستی است، نه فروپاشی ناگهانی انطباق، و قاطیکردن این دو کسی را دنبال مسئله اشتباه میفرستد
عناصر تعاملی: اسکریپتها، Launch targetها و پیوندهای خارجی
PDFium هر actionی را که پیدا میکند با یک نوع عددی دستهبندی میکند، و ثابتهای آمده از fpdf_doc.h را باید دقیق میخکوب کرد، چون مقدارهای اشتباهکپیشده یک اسکنر را بیصدا کور میکنند. enum واقعی این است: PDFACTION_UNSUPPORTED = 0، PDFACTION_GOTO = 1، PDFACTION_REMOTEGOTO = 2، PDFACTION_URI = 3، PDFACTION_LAUNCH = 4، و PDFACTION_EMBEDDEDGOTO = 5. دقت کنید چه چیزی غایب است: هیچ عضو JavaScriptای در این enum نیست. scriptهای سطح سند link action نیستند و هرگز از مسیر FPDFAction_GetType ظاهر نمیشوند؛ آنها با خانواده جداگانهای از فراخوانیها enumerate میشوند. ممیزیای که action typeها را با یک ثابت خیالی JavaScript مقایسه میکند، کامپایل میشود، اجرا میشود، و برای همیشه هیچچیز پیدا نمیکند
const
PDFACTION_GOTO = 1; // in-document jump: harmless
PDFACTION_REMOTEGOTO = 2; // jump into another local file
PDFACTION_URI = 3; // opens an external URL
PDFACTION_LAUNCH = 4; // starts an external program
PDFACTION_EMBEDDEDGOTO = 5; // jump into an embedded file
function ActionTarget(Doc: FPDF_DOCUMENT; Action: FPDF_ACTION;
AType: ULONG): string;
var
Buf: array[0..2047] of AnsiChar;
begin
FillChar(Buf, SizeOf(Buf), 0);
if AType = PDFACTION_URI then
FPDFAction_GetURIPath(Doc, Action, @Buf, SizeOf(Buf))
else
FPDFAction_GetFilePath(Action, @Buf, SizeOf(Buf));
Result := string(UTF8String(PAnsiChar(@Buf)));
end;
procedure AuditPageActions(Doc: FPDF_DOCUMENT; Page: FPDF_PAGE;
PageNo: Integer; Findings: TFindings);
var
StartPos: Integer;
Link: FPDF_LINK;
Action: FPDF_ACTION;
AType: ULONG;
begin
StartPos := 0;
while FPDFLink_Enumerate(Page, @StartPos, @Link) <> 0 do
begin
Action := FPDFLink_GetAction(Link);
if Action = nil then
Continue; // destination-only link, nothing to flag
AType := FPDFAction_GetType(Action);
case AType of
PDFACTION_LAUNCH:
Add(Findings, fsError, 'ACT-LAUNCH', PageNo,
'Launch action targets "' + ActionTarget(Doc, Action, AType) + '"');
PDFACTION_URI:
Add(Findings, fsWarning, 'ACT-URI', PageNo,
'link opens ' + ActionTarget(Doc, Action, AType));
PDFACTION_REMOTEGOTO, PDFACTION_EMBEDDEDGOTO:
Add(Findings, fsWarning, 'ACT-XFILE', PageNo,
'cross-file destination "' + ActionTarget(Doc, Action, AType) + '"');
end; // PDFACTION_GOTO stays silent by design
end;
end;
procedure AuditDocumentBehaviors(Doc: FPDF_DOCUMENT; Findings: TFindings);
var
N: Integer;
begin
N := FPDFDoc_GetJavaScriptActionCount(Doc);
if N > 0 then
Add(Findings, fsError, 'JS-DOC', 0,
Format('%d document-level JavaScript action(s) run on open', [N]));
N := FPDFDoc_GetAttachmentCount(Doc);
if N > 0 then
Add(Findings, fsWarning, 'ATT-EMB', 0,
Format('%d embedded file attachment(s)', [N]));
end;
تقسیم severity بیانگر policy است. Launch action یک error است، چون اجرای یک برنامه دلخواه خطرناکترین کاری است که یک click در PDF میتواند انجام دهد و هیچ invoiceای به آن نیاز ندارد. URIهای خارجی warning هستند: در اسناد مشروع رایجاند، اما بازبین باید مقصد را بدون کلیککردن ببیند، چون متن قابلدیدن لینک و مقصد واقعی آن لزوماً یکی نیست. پرشهای درونسندی از نوع GoTo ساختار هستند، نه رفتار، و عمداً اصلاً وارد گزارش نمیشوند؛ preflightای که برای هر ورودی table of contents فریاد خطر بکشد، خیلی سریع همه را به نادیدهگرفتن خودش عادت میدهد. برای خواندن بدنه اسکریپتهای پشت شمارش JavaScript، و برای سطوح signature MDP و تشخیص XFA، مقاله ممیزی ریسک امنیتی همین سطح را از راه wrapper شیگرای component دنبال میکند
سنجههای منبع: effective image DPI
یک تصویر داخل PDF بهخودیخود هیچ DPIای ندارد. چیزی که دارد پیکسل است، و صفحه آن پیکسلها را درون یک مستطیل با واحد point قرار میدهد، جایی که هر 72 point برابر با یک inch است. رزولوشن فقط بهصورت نسبت این دو معنا پیدا میکند، و به همین دلیل همان عکس 600 در 400 هم میتواند بهعنوان thumbnail بسیار تیز باشد و هم وقتی تمام صفحه شود کاملاً تار. بنابراین ممیزی برای هر تصویر به هر دو عدد نیاز دارد: ابعاد پیکسلی منبع از metadata تصویر، و مستطیل قرارگیری از bounds شیء
procedure AuditPageImages(Page: FPDF_PAGE; PageNo: Integer;
Findings: TFindings);
var
I, ObjCount: Integer;
Obj: FPDF_PAGEOBJECT;
Meta: FPDF_IMAGEOBJ_METADATA;
L, B, R, T: Single;
WidthPt, HeightPt, DpiX, DpiY, EffDpi: Double;
begin
ObjCount := FPDFPage_CountObjects(Page);
for I := 0 to ObjCount - 1 do
begin
Obj := FPDFPage_GetObject(Page, I);
if FPDFPageObj_GetType(Obj) <> FPDF_PAGEOBJ_IMAGE then
Continue;
if FPDFImageObj_GetImageMetadata(Obj, Page, @Meta) = 0 then
Continue;
if FPDFPageObj_GetBounds(Obj, @L, @B, @R, @T) = 0 then
Continue;
WidthPt := R - L; // placed size on the page, in points
HeightPt := T - B;
if (WidthPt <= 0) or (HeightPt <= 0) or
(Meta.Width = 0) or (Meta.Height = 0) then
Continue;
// 72 points = 1 inch, so placed inches = points / 72, and
// effective DPI = source pixels / placed inches.
DpiX := Meta.Width / (WidthPt / 72.0);
DpiY := Meta.Height / (HeightPt / 72.0);
EffDpi := Min(DpiX, DpiY); // the worse axis decides print quality
if EffDpi < 150.0 then
Add(Findings, fsWarning, 'IMG-LOWRES', PageNo,
Format('image %dx%d px placed at %.1fx%.1f pt = %.0f DPI effective',
[Meta.Width, Meta.Height, WidthPt, HeightPt, EffDpi]))
else if EffDpi > 600.0 then
Add(Findings, fsInfo, 'IMG-BLOAT', PageNo,
Format('image is %.0f DPI at placed size; resampling would ' +
'shrink the file with no visible loss', [EffDpi]));
end;
end;
این thresholdها policy هستند، نه physics. مقدار 150 DPI کف تقریبیای است که پایینتر از آن چاپ اداری بهشکل دیدنی pixelate میشود، 300 هدف رایج چاپ تجاری است، و هر چیزی بالاتر از 600 کیفیت دیداری قابلتوجهی اضافه نمیکند و فقط حجم فایل را باد میکند، به همین دلیل بهعنوان informational bloat گزارش میشود نه defect. یک caveat صادقانه هم وجود دارد: FPDFPageObj_GetBounds فقط جعبه axis-aligned را برمیگرداند، بنابراین اگر تصویر با rotation قرار گرفته باشد، عدد محاسبهشده چگالی واقعی را دستکم میگیرد. ساختار FPDF_IMAGEOBJ_METADATA همچنین فیلدهای horizontal_dpi و vertical_dpi را هم دارد که PDFium آنها را از transform matrix کامل استخراج میکند، و مقایسه آن دو نتیجه راه ارزانی برای تشخیص placementهای چرخیده است. همین حساب point-to-pixel در جهت معکوس، یعنی render کردن، در مقاله خروجی JPEG پوشش داده شده است
وضعیت امنیتی: encryption و permission bitها
رمزنگاری PDF دو password با دو کارکرد متفاوت تعریف میکند. user password درِ decryption را باز میکند: بدون آن فایل اصلاً باز نمیشود و FPDF_LoadDocument مقدار nil برمیگرداند، در حالی که FPDF_GetLastError مقدار FPDF_ERR_PASSWORD را گزارش میکند. owner password مجوزها را دروازهبانی میکند: فایلی که فقط owner password دارد بدون credential هم باز میشود، اما بیتهای محدودکنندهای حمل میکند که reader سازگار باید آنها را رعایت کند. بنابراین تلاش برای بارگذاری خودِ فایل نخستین probe امنیتی است، و همین تمایز exit code را تعیین میکند — فایلِ دارای user password عملاً قابل ممیزی نیست و code 2 میگیرد، در حالی که فایلِ owner-password-only بهطور عادی ممیزی میشود و فقط finding جمع میکند
const
FPDF_ERR_PASSWORD = 4;
function AuditSecurity(const FileName: string;
Findings: TFindings): FPDF_DOCUMENT;
var
Perms: ULONG;
Revision: Integer;
begin
Result := FPDF_LoadDocument(PAnsiChar(AnsiString(FileName)), nil);
if Result = nil then
begin
if FPDF_GetLastError() = FPDF_ERR_PASSWORD then
Add(Findings, fsError, 'SEC-USERPW', 0,
'user (open) password required; audit cannot proceed')
else
Add(Findings, fsError, 'DOC-BROKEN', 0, 'file failed to parse');
Exit;
end;
Revision := FPDF_GetSecurityHandlerRevision(Result);
if Revision >= 0 then // -1 means the file is not encrypted
begin
// Opened with an empty password yet encrypted: owner-password-only.
// Anyone may read it, but the permission bits restrict what a
// conforming reader lets them do. Unencrypted files report all
// bits set, which is why the revision gate comes first.
Perms := FPDF_GetDocPermissions(Result);
Add(Findings, fsInfo, 'SEC-ENC', 0,
Format('encrypted, security handler revision %d', [Revision]));
if (Perms and 4) = 0 then // bit 3: print
Add(Findings, fsWarning, 'SEC-NOPRINT', 0,
'printing is not permitted');
if (Perms and 16) = 0 then // bit 5: copy / extract content
Add(Findings, fsInfo, 'SEC-NOCOPY', 0,
'content extraction is not permitted');
if (Perms and 2048) = 0 then // bit 12: high-resolution print
Add(Findings, fsWarning, 'SEC-LOWPRINT', 0,
'only low-resolution printing is permitted');
end;
end;
این maskها از Table 22 در ISO 32000-1 میآیند که بیتها را از 1 شمارهگذاری میکند: bit 3 از مقدار /P mask برابر 4 است، bit 5 برابر 16، و bit 12 برابر 2048. اینکه هر finding چقدر مهم است یک تصمیم routing است. یک مرکز چاپ باید فایل دارای SEC-NOPRINT را همان لحظه intake پس بزند، جایی که submitter پیام روشن میگیرد، نه اینکه سه ساعت مانده به deadline در RIP این موضوع کشف شود. یک archive باید خود SEC-ENC را blocker بداند، چون encryption و نگهداری بلندمدت با هم نمیسازند — نکتهای که بررسی استاندارد در بخش بعدی آن را بهصورت رسمیتر بیان میکند
نشانگرهای استاندارد: خواندن یک ادعای PDF/A
یک فایل تطابق PDF/A را از راه بسته metadata مبتنی بر XMP خودش اعلام میکند؛ با ویژگی pdfaid:part برای شماره بخش 1 تا 4 و pdfaid:conformance برای حرف سطح، مثل b برای وفاداری دیداری یا a برای tagging ساختاری کامل. PDFium C API هیچ accessorای برای XMP ندارد؛ FPDF_GetMetaText فقط Info dictionary را میخواند، و شناسایی PDF/A آنجا نگهداری نمیشود. راه فرار را خود استاندارد فراهم میکند: ISO 19005 الزام میکند stream مربوط به XMP metadata بدون فشردهسازی ذخیره شود تا ابزارها بدون داشتن یک parser کامل PDF بتوانند آن را پیدا کنند. بنابراین یک byte scan خام روشی مشروع برای تشخیص claim است — و فایلی که claimش را در stream فشرده پنهان کند از قبل همان استانداردی را که ادعا میکند نقض کرده است
function PdfAClaim(const FileName: string): string;
var
Bytes: TBytes;
S: RawByteString;
P, Limit: Integer;
begin
Result := ''; // empty = no PDF/A claim present
Bytes := TFile.ReadAllBytes(FileName);
if Length(Bytes) = 0 then
Exit;
SetString(S, PAnsiChar(@Bytes[0]), Length(Bytes));
P := Pos('pdfaid:part', S); // XMP identification schema
if P = 0 then
Exit;
// Handles both <pdfaid:part>2</pdfaid:part> and pdfaid:part="2":
// take the first digit after the property name.
Limit := Min(P + 32, Length(S));
Inc(P, Length('pdfaid:part'));
while (P <= Limit) and not (S[P] in ['1'..'4']) do
Inc(P);
if P <= Limit then
Result := 'PDF/A-' + Char(S[P]);
end;
findingای که این روال تولید میکند عمداً informational است، چون claim فقط یک declaration است، نه یک ویژگی ذاتی فایل. ورودی XMP یک خط XML است که هر producerای، حتی producer خراب، میتواند بنویسد؛ conformance یعنی اینکه فایل واقعاً صدها قاعده مربوط به فونتهای تعبیهشده، رنگ مستقل از دستگاه و قابلیتهای ممنوع را رعایت کند. تشخیص claim فقط به شما میگوید کدام فایلها را باید برای validation واقعی route کنید و نه بیشتر. موتور preflight داخلی component این validation را برای profileهای PDF/A، PDF/UA و PDF/X انجام میدهد، و مقاله CLI دستهای نشان میدهد چگونه آن را درون یک pipeline همراه با reportهایی که auditor بعداً بتواند باز کند، سیمکشی کنید
یک اجرا روی فایل مسئلهدار
driver همه بررسیها را به هم میدوزد: اول security، چون تعیین میکند ممیزی اصلاً اجرا میشود یا نه، بعد behaviorهای سطح سند و claim استاندارد، و بعد یک حلقه صفحهای برای actionها و imageها
function AuditFile(const FileName: string; Findings: TFindings): Integer;
var
Doc: FPDF_DOCUMENT;
Page: FPDF_PAGE;
I: Integer;
Claim: string;
begin
Doc := AuditSecurity(FileName, Findings);
if Doc = nil then
Exit(2); // audit failure, not a verdict
try
AuditDocumentBehaviors(Doc, Findings);
Claim := PdfAClaim(FileName);
if Claim <> '' then
Add(Findings, fsInfo, 'STD-PDFA', 0,
Claim + ' conformance claimed (declaration only, not validated)');
for I := 0 to FPDF_GetPageCount(Doc) - 1 do
begin
Page := FPDF_LoadPage(Doc, I);
if Page = nil then
begin
Add(Findings, fsError, 'PAGE-BROKEN', I + 1, 'page failed to parse');
Continue;
end;
try
AuditPageActions(Doc, Page, I + 1, Findings);
AuditPageImages(Page, I + 1, Findings);
finally
FPDF_ClosePage(Page);
end;
end;
finally
FPDF_CloseDocument(Doc);
end;
if Findings.Count > 0 then
Result := 1
else
Result := 0;
end;
روی یک brochure که از یک آژانس بیرونی برگشته، خروجی چیزی شبیه این است
> preflight_audit brochure_final.pdf
brochure_final.pdf: 5 finding(s)
[ERROR] ACT-LAUNCH page 3 Launch action targets "..\tools\setup.exe"
[ERROR] JS-DOC doc 2 document-level JavaScript action(s) run on open
[WARNING] IMG-LOWRES page 7 image 412x287 px placed at 396.0x275.8 pt = 75 DPI effective
[WARNING] SEC-NOPRINT doc printing is not permitted
[INFO] STD-PDFA doc PDF/A-2 conformance claimed (declaration only, not validated)
exit code 1
هر خط بهتنهایی actionable است، اما verdict واقعی در ترکیب آنهاست. این فایل در حالی ادعای PDF/A-2 میکند که هم encryption dictionary دارد و هم JavaScript زنده، و PDF/A هر دو را صریحاً ممنوع میکند — بنابراین پیش از آنکه هر validator عمیقتری اجرا شود، دروغبودن claim از قبل ثابت شده است. این همان نوع تناقضی است که یک فهرست flat از findingها آشکار میکند و یک pass/fail بولی آن را پنهان میسازد
این ممیزی چه چیزهایی را نمیتواند به شما بگوید
صداقت درباره scope همان چیزی است که یک ابزار preflight را قابل اعتماد نگه میدارد. همه چیزهایی که در بالا آمد آنچه را میخواند که فایل درباره خودش اعلام میکند: PDFium ساختار را parse میکند و این ممیزی آن را inventory میکند. این ممیزی validation واقعی PDF/A انجام نمیدهد — نه بررسی پوشش گلیف در برابر فونتهای embedشده، نه تحلیل فضای رنگ در برابر output intentها، و نه هیچکدام از قواعد clause-levelای که claim را از conformance جدا میکنند؛ برای آن به یک validator اختصاصی مانند موتور preflight component یا veraPDF نیاز دارید. permission bitها declarationهایی هستند که readerهای سازگار رعایتشان میکنند، نه دیوارهای رمزنگارانه، بنابراین SEC-NOPRINT نیت را توصیف میکند نه enforcement را. اسکن actionها annotationهای پیوندی و اسکریپتهای سطح سند را پوشش میدهد؛ scriptهایی که در event dictionaryهای fieldهای فرم پنهان شدهاند، به APIهای فرم هم نیاز دارند. و اگر ممیزی را با بررسی امضا گسترش دهید، آن هم intent اعلامشده را گزارش میکند نه رمزنگاریِ verifyشده را — اعتبارسنجی زنجیره گواهی یک کار جداگانه است. ممیزی preflight مصاحبه intake است، نه دادگاه: وظیفهاش این است که تصمیم routing را آگاهانه، سریع و تکرارپذیر کند
نکته: APIهای سند، صفحه، annotation و image object که در سراسر این ممیزی استفاده شدند، همراه با یک wrapper سطحبالای Delphi و یک موتور کامل preflight برای اعتبارسنجی استانداردها، همگی با PDFium Component عرضه میشوند