مقال تقني

تدقيق خطوط PDF/UA في Delphi: العرض وCharSet وCIDSet

تقرير veraPDF يقول إن عرض محرف لا يتفق مع برنامج الخط المضمَّن لا يخبرك شيئا تقريبا عن أي محرف أو لماذا۔ يجيب PDFlibPas عن ذلك السؤال بحل كل رمز حرف عبر cmap المضمَّن إلى فهرس محرف، وتطبيع مقياس البرنامج إلى 1000 وحدة لكل em، والمقارنة هناك

لماذا تختلف عروض المحارف؟

لأن الرقمين قيد المقارنة يعيشان في نظامي إحداثيات مختلفين، ولا شيء في قاموس PDF يخبرك بالتحويل۔ يكتب قاموس الخط /Widths في فضاء المحرف، الذي تثبّته PDF عند جزء من ألف من em (ISO 32000-1 §9.2.4)۔ ويكتب جدول hmtx داخل برنامج TrueType المضمَّن التقدمات بوحدات تصميم الخط، ويقرر جدول head كم منها يصنع em: 2048 لمعظم وجوه TrueType، و1000 للمشتقة من CFF، وأحيانا شيء آخر كليا۔ قارن القيم الخام وسيبدو كل خط 2048-upem في مجموعتك معطلا۔ هذا هو الفخ الذي تنصبه ISO 14289-1 §7.21.5 لكل من يحاول تدقيق العروض بقراءة حقول القاموس

تدقيق عرض المحارف في PDFlibPas ضمن Delphi: مدخل /Widths في فضاء المحرف وتقدم hmtx بوحدات تصميم الخط يُؤتى بهما إلى نظام الإحداثيات نفسه بتحجيم مقياس البرنامج إلى 1000 وحدة لكل em قبل إجراء أي مقارنة
يحجِّم PDFlibPas كل تقدم hmtx إلى جزء من ألف من em قبل مقارنته بعرض القاموس، ويبلغ فقط عما يزيد تباعده عن وحدة واحدة

يطبّع PDFlibPas عند التحميل۔ يخزّن TPDFTrueTypeParser القيمة Advance * 1000 div unitsPerEm في مصفوفة العرض لديه، بحيث تجيب Parser.GetWidth(GID) أصلا بأجزاء الألف من em نفسها التي تستخدمها PDF، وتبقى GetRawWidth متاحة عندما تحتاج إلى وحدات التصميم۔ ما يزال ذلك يترك الشطر الأصعب: الانتقال من رمز حرف إلى فهرس محرف۔ بالنسبة إلى خط TrueType بسيط، يعتمد المسار على علامة Symbolic في FontDescriptor، وهي البت 3 من /Flags

Parser := TPDFTrueTypeParser.Create;
try
  Parser.LoadFromString(FontProgram);
  if Symbolic then
  begin
    // الوجوه الرمزية تُخاطَب عبر cmap البرنامج مباشرة،
    // مع اصطلاح البايت المرتفع (3,0) كاحتياطي
    GID := Parser.GetGlyphIndex(Code);
    if GID = 0 then
      GID := Parser.GetGlyphIndex($F000 + Code);
  end
  else
  begin
    // غير الرمزية: الرمز -> اسم المحرف عبر الترميز، الاسم -> Unicode
    // عبر قائمة Adobe Glyph List، Unicode -> GID عبر cmap البرنامج
    UnicodeValue := GetGlyphUnicode(EncodingNames[Code and $FF]);
    if UnicodeValue = 0 then
      Continue;
    GID := Parser.GetGlyphIndex(UnicodeValue);
  end;
  if (GID > 0) and (GID < Parser.GlyphCount) then
    if Abs(PDFWidth - Parser.GetWidth(GID)) > 1 then
      Inc(MismatchCount);
finally
  Parser.Free;
end;

تفصيلان في ذلك المقتطف لهما وزن۔ التسامح هو وحدة واحدة لا صفر، لأن التطبيع قسمة صحيحة والملف المنتج بشكل مشروع يمكن أن يحط على بُعد وحدة؛ وهذا بالضبط هو نص «ضمن جزء من ألف من em» الذي تبلغ عنه التشخيصة 10036۔ وحارس GID < Parser.GlyphCount ليس زخرفة۔ فـ GetWidth مكتوبة لتكون متسامحة مع مستدعيي العرض، فتقيّد فهرسا خارج النطاق إلى آخر مدخل في hmtx وتتراجع إلى 750 عند غياب الجدول۔ التسامح صحيح للعرض وخاطئ للتدقيق، لذا يرفض التدقيق الفهرس قبل طلب عرض بدلا من الوثوق بالتقييد

CIDFontType2 يضيف مستوى غير مباشر آخر

يسلك PDFlibPas الخطوط المركبة بالطريقة نفسها، مع /CIDToGIDMap مدرجا بين CID والمحرف۔ تصل العروض في مصفوفة /W، التي تمنحها ISO 32000-1 §9.7.4.3 شكلين يتناوبان بحرية في مصفوفة واحدة: CID بداية تتبعها مصفوفة عروض متتالية، أو CID أولى وCID أخيرة وعرض واحد يُطبق عبر السلسلة۔ يحلل التدقيق كلا الشكلين، ثم يسلم كل زوج ناتج إلى المقارنة نفسها، ويبلغ الإجمالي تحت التشخيصة 10037۔ خطوة التخطيط هي حيث تختلف الخطوط المركبة، ولهذا تهم التشخيصة 10021 للخريطة المفقودة قبل أن تقرأ أي عرض إطلاقا — فغياب /CIDToGIDMap أو تشوّهه لا يكتفي بمخالفة §7.21.3.2، بل يجعل سؤال العرض بلا جواب

ثلاثة مسالك يسلكها PDFlibPas من رمز حرف إلى فهرس محرف في Delphi: cmap البرنامج للخطوط TrueType الرمزية، والتفاف عبر الترميز وقائمة Adobe Glyph List لغير الرمزية، وخطوة CMap مع /CIDToGIDMap لـ CIDFontType2
لا يمكن لمقارنة العرض أن تبدأ حتى يُحَل رمز الحرف إلى فهرس محرف، وكل نوع خطوط يبلغ ذلك الفهرس بمسلك مختلف
// /CIDToGIDMap هو الاسم /Identity أو تدفق فهارس محارف
// بترتيب 16 بت كبير النهاية، فهرس لكل CID (ISO 32000-1 القسم 9.7.4.2)
Obj := DerefIndRef(FDoc, CIDFont.FindValueByKeyName('CIDToGIDMap'));
if (Obj is TPDFName) and (TPDFName(Obj).Name = 'Identity') then
begin
  GID := CID;
  Result := True;
end
else if Obj is TPDFStream then
begin
  Data := TPDFStream(Obj).GetDecodedStream;
  P := CID * 2 + 1;                       // سلاسل Pascal ذات أساس 1
  if (P >= 1) and (P + 1 <= Length(Data)) then
  begin
    GID := (Integer(Byte(Data[P])) shl 8) or Integer(Byte(Data[P + 1]));
    Result := True;
  end;
end;

ماذا ينبغي للمدقّق أن يفعل عندما يرفض برنامج الخط فك الترميز؟

لا تقل شيئا۔ فحشود اكتمال /CharSet و/CIDSet التي تتطلبها ISO 14289-1 §7.21.4.2 — التشخيصتان 10038 و10039 — هي الموضع الذي يتحول فيه متحقق مفرط الحماس إلى عبء، لأن تقريرا يقول «CharSet لديك غير مكتمل» لا يمكن لقارئه تمييزه من «مفكك Type 1 لدينا استسلم»۔ لذلك لا يبلغ PDFlibPas عن مدخل مفقود إلا عندما تنجح ثلاثة أشياء كلها: فك ترميز برنامج الخط، وحل تخطيط الرمز إلى المحرف، وفك ترميز المجموعة نفسها۔ يجب أن تُرجع TPDFType1Decoder.LoadPFBFromString True وتُنتج عددا من charstring قبل فحص أي اسم محرف ضد نص /CharSet؛ ومسار /CIDSet يحتاج إلى تضخم التدفق وعودة عدد المحارف موجبا قبل فحص بت واحد۔ أي استثناء في الطريق ينهار إلى «لا نتيجة»، لا إلى عيب

قاعدة الإبلاغ المتحفظة في تدقيق PDF/UA ضمن PDFlibPas: لا يُبلَّغ عن مدخل /CharSet أو /CIDSet مفقود إلا عندما يُفك ترميز برنامج الخط ويُحَل تخطيط الرمز إلى المحرف وتُفك المجموعة نفسها، وأي فشل ينتج الصمت
ثلاثة نجاحات مستقلة مطلوبة قبل إصدار نتيجة مدخل مفقود، بحيث يكلفك المفكك المستسلم نتيجة سلبية كاذبة لا اتهاما كاذبا

هذا انحياز متعمَّد نحو السلبيات الكاذبة، ويستحق أن يُصَرَّح به بوضوح لا أن يُدفن۔ جدول CFF تالف، أو صيغة Type 1 غير مدعومة، أو /CIDSet أقصر من مدى المحارف، كلها تنتج الصمت بدلا من تشخيصة۔ المنطق هو أن تدقيقات PDF/UA تُعاد توجيهها إلى مؤلفين لم يبنوا الأدوات، والاتهام الكاذب يكلف أكثر من الفائت: يحرق المؤلف يوما في إثبات أن ملفا ممتثلا ممتثل، ثم يكف عن الثقة بالتقرير كله۔ وبروتوكول Matterhorn يصنع التمييز نفسه بصورة مختلفة عندما يفصل الفحوص التي يمكن للآلة حسمها عن التي يجب على الإنسان حسمها، ونقطة تفتيش الخطوط لديه (31) هي حيث تسكن هذه۔ إذا احتجت القراءة الأشد صرامة، شغّل PDFlibPas كبوابة سريعة ومحققا مخصصا كرأي ثان — هذا الاقتران هو نفسه الموصوف في جولة فحص PDF/A وPDF/UA المسبق

/Contents الصفحة قائمة لا تدفق

أغلى خطأ منفرد في تدقيق تدفقات المحتوى هو معاملة /Contents كتدفق واحد۔ تتيح ISO 32000-1 §7.7.3.3 للصفحة حمل مصفوفة تدفقات يكون تسلسلها، مع فراغ بياض بين الأجزاء، هو برنامج الصفحة؛ وتقسم المُنتِجات عند نقاط اعتباطية، ويمكن أن تجلس BT في عضو بينما ET المطابقة لها في التالي۔ ومعالج المحتوى يحفظ حالة — عمق تداخل المحتوى المعلَّم، والخط الذي اختاره آخر Tf، وعلم كائن النص — وProcess يعيد ضبط تلك الحالة عند الدخول۔ استدعه مرة لكل عضو مصفوفة وكل تدفق بعد الأول سيبدأ بلا خط حالي، فيُقرأ نص كان مُعلَّما بشكل مثالي كتشويش بلا تعليم ولا خط۔ يُسلسل PDFlibPas أولا ثم يعالج مرة واحدة

function ContentObjectData(FDoc: TSmartPDFDocument; Obj: TPDFObject): AnsiString;
  var
  I: Integer;
begin
  Result := '';
  Obj := DerefIndRef(FDoc, Obj);
  if Obj is TPDFStream then
    Result := TPDFStream(Obj).GetDecodedStream
  else if Obj is TPDFArray then
    for I := 0 to TPDFArray(Obj).Count - 1 do
      Result := Result + ContentObjectData(FDoc, TPDFArray(Obj).Item[I]) + #10;
end;

// نداء Process واحد على التسلسل كاملا، لا نداء لكل عضو
Scanner.Process(ContentObjectData(FDoc, PageDict.FindValueByKeyName('Contents')));

أي كائنات Form XObject تُحسب فعلا غير مهيكلة؟

فقط تلك التي تستدعيها الصفحة فعلا، من موقع نداء خارج المحتوى المعلَّم، ويُظهر محتواها الخاص نصا۔ تفرض التشخيصة 10040 معيار ISO 14289-1 §7.20 بتسجيل ثلاث حقائق مستقلة لكل رقم كائن — فيه نص، واستُدعي، واستُدعي داخل محتوى معلَّم — والإبلاغ فقط عن تقاطع أول اثنين مطروحا منه الثالث۔ كل من الاختصارين خاطئ بشكل ستشحنه أنت: تعليم كل Form حامل للنص في /Resources يعاقب مكتبة قوالب لا يرسم منها أحد، وتعليم كل Form مستدعى يعاقب شعارات متجهة لا تحمل نصا ولا تحتاج تعليما۔ موقع الاستدعاء يُحَل برقم الكائن لا باسم المورد، لأن Form نفسه يُبلغ عادة عبر أسماء مختلفة في صفحات مختلفة۔ والتشخيصة المرافقة 10041 تسلك البرنامج المتسلسل نفسه لـ §7.21.8، حائلة كل مُعامِل يُظهر نصا عبر الخط في النطاق وحاسبة الرموز التي تحط على .notdef، المحظورة بصرف النظر عن نمط عرض النص — بما في ذلك النمط غير المرئي المستخدم خلف الصور الممسوحة۔ أما كيف يجب تغليف الـ Forms الناجية فهو سؤال شجرة بنية، مشمول في مقال بناء بنية PDF المعلَّمة

خطوط بلا FontDescriptor إطلاقا

الخط غير المضمَّن مدخل مشروع لهذا التدقيق، لا حالة خطأ، وكل دالة مساعدة تحت فحص التضمين يجب أن تصمد أمامه۔ عندما يجد PDFlibPas لا /FontDescriptor، أو واصفا بلا FontFile أو FontFile2 أو FontFile3، يسجل التشخيصة 10020 — أو 10022 عندما يكون الاسم أحد الخطوط القياسية الأربعة عشر، التي ترفض §7.21.4 الملاحظة 5 إعفاءها بصراحة — ثم يواصل عبر بقية الملف۔ هذه هي فائدة التقرير كاملة: المؤلف يريد كل نتيجة في مرور واحد، لا نتيجة واحدة لكل تشغيل۔ لذا فإن مرجع الواصف المسلَّم إلى دوال العرض وcmap وCharSet وCIDSet المساعدة يمكن أن يكون Nil، وكل منها يختبره عند الدخول بدلا من افتراض أن فحصا سابقا أجهض التدقيق۔ وإذا كان الإصلاح هو تضمين الناقص، فالآليات في ملاحظة تضمين الخطوط الناقصة في ملف PDF قائم

تشغيل التدقيق

نداء واحد، على ملف لم تنتجه بالضرورة۔ تأخذ TPDFlib.CheckFileCompliance محدِّد اختبار امتثال — 2 لـ PDF/UA-1 وفق ISO 14289-1:2014 — وتُرجع إما صفرا أو مقبض قائمة نصوص تكون مدخلاتها رمزا رقميا ونقطتين ورسالة مقروءة۔ نتائج الخطوط وتدفقات المحتوى المناقشة هنا تحتل من 10020 إلى 10041 في ذلك المدى، مبقاة رقميا منفصلة عن رموز PDF/A من 00xxx كي يبقى السجل المختلط مقروءا۔ تمرير 1 في Options يقصّر الدائرة عند أول نتيجة، وهو ما تريده في بوابة بناء لا في أداة تأليف۔ وبالنسبة إلى مستند لا يزال مفتوحا في الذاكرة، تشغّل GetPDFUADiagnostics الفحص المكافئ دون رحلة ذهاب وعودة عبر القرص

var
  Issues, Count, I: Integer;
begin
  // ComplianceTest = 2 يختار PDF/UA-1؛ Options = 0 يبلغ كل نتيجة
  Issues := PDF.CheckFileCompliance('delivery.pdf', '', 2, 0);
  if Issues = 0 then
    WriteLn('delivery.pdf: PDF/UA-1 conformant')
  else
  begin
    Count := PDF.GetStringListCount(Issues);
    for I := 1 to Count do
      WriteLn('  ', PDF.GetStringListItem(Issues, I));   // مثال: 10037 CIDFontType2 ...
  end;
end;

لا شيء من هذا يحتاج إلى ملف تنفيذي لمحقق خارجي على الجهاز، وهو الفرق بين فحص يعمل مع كل بناء وفحص يعمل عندما يتذكر أحدهم۔ وواجهات الامتثال والتشخيص الموصوفة هنا تُشحن في مكتبة PDFlibPas لـ PDF في Delphi القياسية، التي تحمل صفحة منتجها جدول رموز التشخيص الكامل لـ PDF/UA-1 إلى جانب مجموعات اختبار PDF/A وPDF/X وPDF/E