يبلغك فحص ما قبل الحفظ بأن الملف نظيف من PDF/UA. ثم يفتح veraPDF الملف نفسه ويضع علامة على Figure بلا نص بديل تحت المادة 7.3. كلا الأداتين على حق، والفجوة بينهما هي جوهر المشكلة في التحقق من الإتاحة عبر فحص البايتات. الفحص على مستوى البايت يؤكد أن الملف يقول إنه معنّن: فهو يعثر على /StructTreeRoot، و/MarkInfo /Marked true، وpdfuaid:part في حزمة XMP، وعنوان المستند، واللغة. هذه كلها علامات صيغة، وهي ضرورية. لكنها لا تخبرك شيئًا عن كون الشكل الفعلي في الصفحة الرابعة يحمل وصفًا يمكن لقارئ الشاشة أن ينطقه. الجواب يعيش في شجرة الوسوم، وللحصول عليه عليك أن تمشي الشجرة.
PDFium Component هي مكتبة PDF أصلية من نوع VCL لـ Delphi وC++Builder، وValidatePdfUa يقوم بكلا التمريرين. التمرير على مستوى البايت يتعامل مع علامات الصيغة. وفوقه توجد مرحلة شجرة بنية تحمل الشجرة المعنونة الحية، وتمشي عبر كل عنصر، وتفحص مجموعة صغيرة من قواعد المحتوى عالية الثقة حيث تعني الصفة المفقودة عيبًا حقيقيًا في الإتاحة لا مجرد تفضيل أسلوبي. هذه المقالة عن التمريرة الثانية: ما الذي تفحصه، ولماذا منطق القواعد دالة خالصة بلا DLL تحتها، وأين تتوقف عمدًا.
لماذا لا يستطيع الفحص بالبايت رؤية Alt مفقود
ISO 14289-1 (PDF/UA-1) هي طبقة من المتطلبات فوق ISO 32000. بعض هذه المتطلبات بنيوي ومرئي في الملف الخام: يجب أن يعلن الكتالوج شجرة بنية، ويجب أن تضبط تفضيلات العارض DisplayDocTitle. ويمكن لماسح رموز يزيل أجسام التدفقات ويطابق رموز الأسماء بحدود فصل أن يتحقق من كل ذلك، ويفعل فحص PDFium ValidatePdfUaCompliance ذلك بالضبط بالنسبة إلى مواد مثل 7.1 و7.18 و7.21.
لكن "كل Figure لديه نص بديل" ليست خاصية في صياغة الملف. إنها خاصية في البنية المنطقية لشجرة العناصر الموسومة التي تربط المحتوى بالمعنى. يمكن أن يوجد مدخل Alt الخاص بـ Figure داخل قاموس عنصر البنية، أو يُقدَّم عبر span /ActualText، أو يأتي من نوع مخصص معاد تعيينه عبر role map. لا يمكنك العثور عليه بثقة عبر البحث عن /Alt في البايتات، لأن تلك السلسلة تظهر في سياقات غير مرتبطة، وقد تكون مضغوطة داخل object stream، ولا تخبرك بأي عنصر بنيوي تنتمي إليه. الطريقة الصادقة للإجابة عن السؤال هي أن تسأل شجرة البنية نفسها، عنصرًا عنصرًا، على السطح نفسه الذي يقيمه veraPDF وPAC. هذا هو الخط الذي بُنيت عليه فحوص Tier-1 في PDFium: فحص بايتات للصيغة، وتجول في الشجرة للمحتوى.قراءة شجرة الوسوم الحية
المادة الخام هي
(وتُعرض أيضًا على أنها خاصية TPdf.GetStructureElements)، وهي تعيد StructureElements، مصفوفة مسطحة من سجلات TPdfStructureElements بترتيب المستند. كل سجل هو إسقاط عنصر بنيوي واحد عبر دوال الوصول في PDFium، مع الحقول التي تحتاجها قواعد الإتاحة فعلًا:TPdfStructureElementالحقل
type
TPdfStructureElement = record
Level: Integer; // depth in the tag tree
ParentIndex: Integer; // index of parent element, or -1
TypeName: WString; // standard /S name: Figure, Formula, Note...
Title: WString; // /T
AlternateText: WString; // /Alt (FPDF_StructElement_GetAltText)
ActualText: WString; // /ActualText
Expansion: WString; // /E
ID: WString; // /ID (FPDF_StructElement_GetID)
Language: WString; // /Lang
MarkedContentIDs: TPdfIntegerArray;
// ... child bookkeeping fields
end;
هو الذي يرتكز عليه المدقق. يأتي من TypeName، التي تعيد نوع البنية القياسي للعنصر، أي FPDF_StructElement_GetTypeالاسم — بعد أن يحل PDFium خريطة الأدوار./Sيأتي من AlternateText، وFPDF_StructElement_GetAltText من ActualText، وFPDF_StructElement_GetActualText من ID.FPDF_StructElement_GetIDوبما أن المصفوفة مسطحة ومرتبة، يمكن للمدقق أن يفهم المستند كله مرة واحدة بدلًا من العودية، وهو أمر مهم للقاعدة الوحيدة العالمية لا لكل عنصر منفرد.
المدقق دالة خالصة، وهذا مقصود
منطق القواعد لا يعيش داخل الدالة التي تتحدث إلى DLL. إنها دالة مستقلة وعامة وخالصة:
function ValidatePdfUaStructureElements(
const Elements: TPdfStructureElements): TPdfUaValidationIssues;
تأخذ مصفوفة مسطحة من العناصر وتعيد مجموعة من المشكلات. لا تستدعي أي دالة PDFium، ولا تفتح أي مستند، ولا تلمس أي حالة عامة. هذا الفصل مقصود، ويفيد مرتين. أولًا من ناحية القابلية للاختبار: يمكنك بناء مصفوفة TPdfStructureElements اصطناعية في اختبار وحدات، Figure بلا Alt، وFormula نصه القابل للوصول موجود فقط في ActualText، وNoteان يشتركان في ID واحد، ثم تفحص مجموعة النتائج من دون pdfium.dll وجود DLL أصلًا. يختبر منطق القواعد دون اتصال، بينما يُختبر تجوال DLL منفصلًا بواسطة فحص حيّ لمستند مباشر يتخطاه الاختبار عندما تكون المكتبة مفقودة.
ثانيًا من ناحية وضوح المسؤولية. TPdf.ValidatePdfUa تتولى الجزء الفوضوي، وهو تحميل كل صفحة، وجلب عناصرها، وتجميعها، ثم تسلم مصفوفة نظيفة إلى المدقق الخالص. "احصل على البيانات" (DLL، الآثار الجانبية، العمر) و"احكم على القواعد" (خالص، حتمي) لا يختلطان أبدًا. وعندما تحتاج القاعدة إلى تغيير، فأنت تغيّر دالة بلا I/O فيها.
ما الذي تفحصه القواعد الثلاث فعليًا
مرحلة شجرة البنية ترفع ثلاث قيم مشكلة، وتُضاف إلى نهاية TPdfUaValidationIssues حتى يبقى التعداد مستقرًا في ABI للمستدعين الحاليين: pvuaiFigureMissingAlt, pvuaiFormulaMissingAlt, وpvuaiNoteMissingId. والجسم صغير بما يكفي لفهمه كاملًا:
for I := 0 to High(Elements) do
begin
T := string(Elements[I].TypeName);
if T = 'Figure' then
begin
// §7.3 — a Figure needs an alternate representation:
// an Alt entry OR ActualText. Flag only when BOTH are empty.
if (Elements[I].AlternateText = '') and (Elements[I].ActualText = '') then
Include(Result, pvuaiFigureMissingAlt);
end
else if T = 'Formula' then
begin
// §7.7 — same rule as Figure: Alt OR ActualText.
if (Elements[I].AlternateText = '') and (Elements[I].ActualText = '') then
Include(Result, pvuaiFormulaMissingAlt);
end
else if T = 'Note' then
begin
// §7.9 — every Note must have a unique ID.
NoteId := string(Elements[I].ID);
if NoteId = '' then
Include(Result, pvuaiNoteMissingId)
else
for J := 0 to I - 1 do
if (string(Elements[J].TypeName) = 'Note') and
(string(Elements[J].ID) = NoteId) then
begin
Include(Result, pvuaiNoteMissingId);
Break;
end;
end;
end;
المادة 7.3 تحكم الأشكال: يجب أن يوفّر عنصر Figure نصًا بديلًا. كان الإصدار الأول من هذا الفحص ينظر فقط إلى Alt، مما جعله أشد من المدققين المرجعيين. PDF/UA يقبل figure يكون النص القابل للوصول فيه معروضًا عبر ActualText بدلًا من ذلك، لأن replacement text تمثيل بديل صالح، لذا لا تُعلِّم القاعدة Figure إلا عندما يكون both Alt وActualText فارغين. المادة 7.7 تخص Formula، وبعد التصحيح نفسه تستخدم الاختبار ذاته Alt-or-ActualText؛ كانت عينة من corpus الامتثال تعطي Formula نصه القابل للوصول عبر ActualText فقط تُرفض خطأً إلى أن تمت مواءمة فرع Formula مع فرع Figure.
المادة 7.9 مختلفة نوعًا. يجب أن يملك Note /ID، ويجب أن يكون ذلك ID فريدًا عبر المستند كله. غياب ID هو فشل على مستوى العنصر الواحد. أما duplicate ID فهو علاقة بين عنصرين، ولهذا فالمصفوفة المسطحة مهمة: فلكل Note يفحص المدقق إلى الخلف العناصر التي رآها سابقًا ويُعلّم أي تصادم مع أي Note أسبق يحمل ID نفسه. الكلفة هي O(n²) الواضحة بحسب عدد Note، وهي غير مهمة لأي مستند حقيقي، وتُبقي الدالة حلقة واحدة مقروءة من دون فهرس إضافي يحتاج إلى مزامنة.
التجميع عبر الصفحات حتى تكون الفريدة عالمية
يعرض PDFium عناصر البنية لكل صفحة، لا لكل مستند، لذا فإن التنسيق في ValidatePdfUa يجب أن يجمعها قبل أن تعمل القواعد. وهو يمر على كل صفحة باستخدام FPDF_LoadPage/ GetStructureElementsForPage/ FPDF_ClosePage، مستقلًا عن أي صفحة تكون المكوّن فاتحها حاليًا، ثم يضيف عناصر كل صفحة إلى مصفوفة واحدة. بعدها فقط يستدعي المدقق الخالص:
// inside TPdf.ValidatePdfUa, after the byte-level pass
if (FDocument <> nil) and
(not (pvuaiMissingStructTreeRoot in Result.Issues)) then
begin
AllElems := nil;
PageTotal := FPDF_GetPageCount(FDocument);
for I := 0 to PageTotal - 1 do
begin
Page := FPDF_LoadPage(FDocument, I);
if Page = nil then Continue;
try
PageElems := GetStructureElementsForPage(Page);
finally
FPDF_ClosePage(Page);
end;
// append PageElems into AllElems ...
end;
Result.Issues := Result.Issues + ValidatePdfUaStructureElements(AllElems);
end;
التجميع هو ما يجعل فحص الفريدة في 7.9 صحيحًا. يمكن لـ Noteين في صفحتين مختلفتين أن يشتركا في ID واحد؛ ولو تحققت صفحة بصفحة فلن ترى التصادم أبدًا، لأن مجموعة عناصر كل صفحة تبدو متسقة داخليًا. بناء مصفوفة واحدة على مستوى المستند هو الطريق الوحيد لجعل التكرار مرئيًا. والضابط في المقدمة جدير بالملاحظة أيضًا: جولة الشجرة لا تعمل إلا عندما تكون التمريرة على مستوى البايت قد لا أبلغت عن pvuaiMissingStructTreeRoot. المستند غير الموسوم لا يملك شجرة تمشي عليها وقد جرى بالفعل تعليمه بسبب غياب جذر البنية، لذا تُتخطى عمليات التحميل لكل صفحة بالكامل. التمريرة العميقة لا تكلف شيئًا على المستندات التي لا تستفيد منها.
محافظ بطبعه: يفوّت بهدوء، ولا يصرخ ذئبًا
أهم خاصية في هذا المدقق هي ما يرفض القيام به. لا يطابق إلا أسماء النوع القياسية /S التي FPDF_StructElement_GetType تعيدها مباشرة، Figure, Formula, وNote. المستند الذي يعرّف نوعًا مخصصًا ويعيد تعيينه إلى Figure سيُبلّغ، اعتمادًا على كيفية حل PDFium للنوع، باسمه هو. وعندما يحدث ذلك لا يتعرف إليه المدقق ويبقى صامتًا. ذلك سلبية كاذبة، وهذا هو السلوك المقصود. قاعدة التصميم هي أن الحد من التقارير بدلًا من إنتاج سلبية كاذبة، لأن أداة فحص ترفع ذئبًا كاذبًا على الملفات المطابقة تعلّم مستخدميها تجاهلها، والمدقق المتجاهل أسوأ من لا شيء. تعيش الصور الزخرفية في artifact stream، لا في شجرة البنية، لذلك لا تظهر أبدًا بوصفها Figures من الأصل؛ ولن تتلقى شكوى "missing Alt" على قاعدة خلفية وُسمت بشكل صحيح على أنها artifact.
ولهذا أيضًا يُحصر النطاق في ثلاث قواعد. فتعشيق مستويات العناوين (المادة 7.4)، ونطاق رؤوس الجداول (7.5)، واكتشاف حلقات role-map (7.1) كلها متطلبات PDF/UA مشروعة، لكن التحقق منها جيدًا يحتاج تحليلًا حقيقيًا للرسم البياني والسمات، أما التحقق الساذج فينتج بالضبط الإيجابيات الكاذبة التي يحظرها التصميم. يسمح PDF/UA بأنماط عناوين مثل H1 وH2 وH3 وH3، وهو ما سترفضه قاعدة "يجب أن تزداد دائمًا بشكل صارم" خطأً. تُترك تلك الفحوص لأدوات امتثال مخصصة. ومجموعة Tier-1 هي الجزء الذي تكون فيه الصفة المفقودة واضحة لا لبس فيها.
الحد الفاصل، بصياغة صريحة
ثمة حدان يستحقان المعرفة قبل أن تربط هذا ببوابة إصدار. أولًا، المدقق لا يكون أفضل من قدرة PDFium على القراءة من عنصر البنية. بعض ملفات corpus الامتثال التي يمررها المدقق المرجعي تستخدم آلية نص بديل لا يعرضها PDFium، لذلك FPDF_StructElement_GetAltText تعود فارغة رغم أن الملف متوافق فعلًا. عندها يعلّم المدقق الخالص غياب Alt على بيانات ناقصة على نحو "صحيح"، وهي سلبية كاذبة مصدرها تغطية accessor في DLL، لا منطق القاعدة. تخفيف القاعدة لاستيعاب تلك الحالات سيعميها أيضًا عن الأعطال الحقيقية التي وُضعت لالتقاطها، لذلك توثَّق كقيد معروف في PDFium بدل أن تُغطى بالطلاء.
ثانيًا، هذا فحص مسبق لا شهادة اعتماد. يلتقط Tier-1 أخطاء المحتوى عالية الثقة التي لا يستطيع فحص البايتات وحده التقاطها بنيويًا، ويفعل ذلك من دون إنذارات كاذبة. لكن الامتثال الكامل لـ PDF/UA، بما في ذلك دلالات العناوين وبنية الجداول وصحة ترتيب القراءة، ما يزال من اختصاص مدقق كامل وفي النهاية مراجِع بشري. استخدم ValidatePdfUa لإسقاط العيوب الواضحة بسرعة وبكلفة منخفضة داخل خطك، ثم دع veraPDF أو PAC يقول الكلمة الأخيرة. وتقوم نفس جولة شجرة البنية على بناء قارئ PDF يمكن الوصول إليه في Delphi، حيث تقود شجرة الوسوم ترتيب القراءة والنص المنطوق، كما تكمل العمل على مستوى البيانات الوصفية في مراجعة تعليقات PDF من Delphi.
تأتي واجهات برمجة تطبيقات شجرة البنية وValidatePdfUa المدقق المعروض هنا مع PDFium Component لـ Delphi وC++Builder (VCL) وLazarus/FPC (LCL). وتربط صفحة المنتج المرجع الكامل لواجهات البرمجة، بما في ذلك سجل TPdfStructureElement والتعداد الخاص بالمشكلات الكامنة خلف هذه الفحوص.