قم بإنشاء تقرير، وقم بتضمين خط TrueType، وسيتم فتح الإخراج بشكل صحيح في كل عارض تجربه. الصور الرمزية (glyphs) صحيحة، والنص قابل للتحديد، والملف صالح. الشيء الوحيد الخطأ هو الحجم. يحمل المستند الذي استخدم بضع عشرات من الأحرف اللاتينية خط 350 كيلوبايت بالكامل. يحمل المستند الذي طبع فقرة باللغة الصينية خط CJK بحجم 14 ميجابايت بدلاً من شريحة نصف ميجابايت التي يجب أن يحتاجها. لم يتم رفع أي استثناء، ولم يتم تسجيل أي تحذير، واجتاز الملف التحقق من الصحة. هذا ما تبدو عليه خطوة الإنهاء (finalization) غير المرتبة من الخارج: لا شيء يفشل، والدليل الوحيد هو رقم كبير جداً
عاش الخطأ الذي أنتجه في HotPDF لخط إصدار واحد وتم إصلاحه منذ ذلك الحين. يجدر كتابته ليس كإشعار عيب ولكن كدرس، لأن شكل الخطأ عام. أي محرك مستندات يحتوي على مرحلة إنهاء تقوم بتحويل (mutates) الكائنات قبل كتابتها مباشرة، ويعتمد صحة تلك المرحلة كلياً على ترتيب خطواتها بالنسبة للتسلسل (serialization). احصل على خطوة واحدة على الجانب الخطأ من الكتابة ولن تفعل شيئاً، بهدوء
ما يفترض أن تفعله مجموعة الخطوط الفرعية (font subsetting)
الخط الفرعي (subset font) هو جزء من ملف TrueType الذي يستخدمه المستند بالفعل. يصف ISO 32000-1 §9.9 كيف يركب برنامج خط مضمن (embedded font program) في تيار (stream) يشار إليه بواسطة واصف الخط (font descriptor)، وبالنسبة لبرنامج TrueType يكون هذا التيار هو /FontFile2 مع /Length1 الذي يعطي عدد البايتات غير المضغوطة. تعيد المجموعة الفرعية كتابة جدولي glyf و loca بحيث يحتويان فقط على الصور الرمزية التي يشير إليها المستند، وتعيد ترقيم معرفات الصور الرمزية، وتسبق اسم /BaseFont بعلامة (tag) من ستة أحرف مثل ABCDEF+ لتمييز الخط كمجموعة فرعية، تماماً كما تتطلب المواصفات. وجه لاتيني يتم تقسيمه إلى عشرة أو خمسة عشر كيلوبايت هو الفرق بين ملف PDF خفيف الحجم وملف يشحن محرفاً (typeface) بأكمله من أجل عنوان واحد
النقطة التي يحدث فيها هذا مهمة. مجموعة الخطوط الفرعية ليست تحويلاً (transform) تقوم بتطبيقه على البايتات الموجودة بالفعل على القرص. تقوم بتحرير رسم بياني للكائنات (object graph) في الذاكرة: إنها تقلص محتوى تيار /FontFile2، وتصلح /Length1، وتعيد كتابة سلسلة /BaseFont. يجب أن يكون كل ذلك في مكانه عندما يمشي المتسلسل (serializer) في الرسم البياني ويصدر البايتات. إذا هبطت التعديلات بعد كتابة البايتات، فإنها تقوم بتحديث الكائنات التي لن يقرأها أحد أبداً
العَرَض (symptom)، ولماذا لم يشتكِ شيء
كان السلوك المبلغ عنه هو خطوط كاملة في الإخراج مع عدم وجود تشخيص (diagnostic). وجد المستخدم الذي قام بتسجيل خط Unicode TrueType وأنتج مستنداً عادياً أن كائن الخط المضمن كان بنفس طول الملف المصدري .ttf، وأن اسم /BaseFont لم يحمل أي بادئة مجموعة فرعية من ستة أحرف. لم يتقلص الإخراج أبداً بين عمليات التشغيل التي استخدمت عشر صور رمزية وعمليات التشغيل التي استخدمت عشرة آلاف
غياب أي خطأ هو الجزء الذي يجعل هذه الفئة من الأخطاء باهظة الثمن. لا يزال روتين المجموعة الفرعية (subsetting routine) الذي يعمل في الوقت الخطأ يعمل. إنه يمشي على استخدام نقاط الكود (codepoint) المتراكمة، ويبني مجموعة فرعية صحيحة تماماً، ويطبقها على الرسم البياني للكائنات في الذاكرة. داخلياً يتم إنجاز العمل ويعود الاستدعاء بشكل نظيف. الشيء الوحيد الخطأ هو أن الرسم البياني للكائنات الذي تم تحريره لم يعد هو الشيء الذي يتم كتابته، لأن الكاتب (writer) قد انتهى بالفعل. من وجهة نظر المتصل (caller)، تم إنتاج المستند وحفظه دون وقوع حادث، وهو الانطباع الدقيق الذي يعطيه الفشل الصامت (silent failure)
السبب الجذري كان ترتيب الإنهاء (finalization order)
في HotPDF يحدث العمل الختامي داخل EndDoc. خطوة المجموعة الفرعية هي روتين داخلي يسمى BuildAndApplyUnicodeFontSubset. يقرأ مجموعة نقاط الكود المستخدمة لكل مستند، المحفوظة في صورة نقطية (bitmap) يملأها مسار إرسال النص مع ظهور الصور الرمزية، ويعين كل نقطة كود مستخدمة من خلال جدول نقطة كود إلى صورة رمزية (codepoint-to-glyph table) المخزن مؤقتاً إلى معرف صورة رمزية حقيقي، ويعيد كتابة برنامج الخط حول ذلك الإغلاق (closure). عند تسجيل خط Unicode TrueType، يقوم مسار الإرسال (emit path) بتعيين بت (bit) في مجموعة نقاط الكود المستخدمة لكل حرف يرسمه، لذلك بحلول الوقت الذي يغلق فيه المستند، يعرف المحرك بالضبط ما هي الصور الرمزية التي يجب أن تحتفظ بها المجموعة الفرعية
كان العيب هو أنه تم استدعاء BuildAndApplyUnicodeFontSubset بعد أن قام SaveToStream أو SaveToFile بالفعل بتسلسل المستند. تم حساب جميع تعديلات أداة المجموعة الفرعية (subsetter) على /FontFile2، و /Length1 المصحح، وبادئة /BaseFont المكونة من ستة أحرف، مقابل رسم بياني للكائنات تم تحويله بالفعل إلى بايتات. كان الإصلاح عبارة عن إعادة ترتيب بسطر واحد: انقل استدعاء المجموعة الفرعية قبل التسلسل، بحيث يصدر الكاتب الخط المقسم فرعياً بدلاً من الخط الأصلي. يشغل التسلسل المصحح أداة المجموعة الفرعية أولاً ويتسلسل بعد ذلك
var
Pdf: THotPDF;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.RegisterUnicodeTTF('C:\Fonts\NotoSansSC-Regular.ttf');
Pdf.BeginDoc;
Pdf.CurrentPage.SetFont('Noto Sans SC', [], 12);
Pdf.CurrentPage.TextOut(72, 760, 0, '报表标题 Report Heading');
Pdf.EndDoc; // subsetting runs here, before the write
Pdf.SaveToFile('Report.pdf');
finally
Pdf.Free;
end;
end;
مع تصحيح الترتيب، لا شيء يتغير حول الكود المتصل. تكون المجموعة الفرعية قيد التشغيل افتراضياً بمجرد تسجيل خط Unicode TrueType. تقوم بتسجيل الخط، والبدء في المستند، والرسم، وإنهائه، ويتم بناء المجموعة الفرعية من الصور الرمزية التي استخدمتها قبل أن تغادر البايتات الذاكرة
لماذا تمثل خطوة واحدة في غير مكانها فئة كاملة
السبب في أن هذا يستحق أن يكون درساً وليس حاشية سفلية (footnote) هو أن EndDoc يصدر قائمة بخطوات الإغلاق، وكل واحدة منها حساسة لموضعها بالنسبة للكتابة. مجموعة الخطوط الفرعية هي إحداها. يتطلب إخراج PDF/A تيار /CIDSet يعدد بالضبط معرفات الصور الرمزية الموجودة في المجموعة الفرعية، وهو قيد يفرضه ISO 19005 بحيث يمكن للمدقق (validator) تأكيد أن البرنامج المضمن يطابق ما يطالب به واصف الخط؛ يتم إصدار هذا التيار في نفس نافذة الإنهاء ويعتمد على بناء المجموعة الفرعية أولاً. يتطلب PDF/UA-1، وفقاً لـ ISO 14289-1 §7.18.3، أن تعلن كل صفحة تحمل تعليقاً توضيحياً (annotation) عن /Tabs بقيمة /S، ويقوم روتين داخلي يسمى EnsurePDFUATabsOnAnnotatedPages بختم هذا المفتاح خلال نفس المرحلة. تعمل فحوصات نية الإخراج (Output-intent checks) هناك أيضاً
أسقط نفس خطأ الترتيب الذي عطل المجموعة الفرعية أيضاً مفتاح ترتيب علامة التبويب (tab-order) لـ PDF/UA على الصفحات المشروحة (annotated pages)، لأن تلك الخطوة جلست على نفس الجانب الخطأ من الكتابة. أبلغ veraPDF و PAC عن فقدان /Tabs /S كانتهاك لنقطة تفتيش بروتوكول Matterhorn 21-001. لذلك، لم يؤد استدعاء واحد في غير محله إلى تضخيم حجم الملف فحسب؛ بل إنه كسر بصمت أحد متطلبات مطابقة إمكانية الوصول في نفس الوقت، مع نفس الافتقار إلى أي خطأ. هذا هو خطر مرحلة الإنهاء: تتشارك خطواتها في شرط مسبق، ويمكن لخطأ ترتيب واحد أن يأخذ العديد منها مرة واحدة بينما لا يزال كل استدعاء يعود بنجاح
كيف يتم التقاط فشل الإرسال (emit failure) الصامت فعلياً
لا يتم التقاط الخطأ الذي لا يثير أي استثناء عن طريق تشغيل البرنامج. يتم التقاطه من خلال فحص الإخراج ومقارنته بما كان يجب أن ينتجه الإدخال. بالنسبة لمجموعة الخطوط الفرعية، تكون الشيكات ملموسة (concrete). قارن حجم ملف الإخراج بتوقع تقريبي: لا ينبغي أن يكون المستند الذي لمس عدداً قليلاً من الصور الرمزية بحجم محرف (typeface) كامل. افتح كائن الخط المضمن واقرأ طوله بالبايت؛ فإن /FontFile2 المقسم فرعياً لوجه لاتيني هو جزء صغير من الملف المصدر. اقرأ اسم /BaseFont وتأكد من وجود البادئة المكونة من ستة أحرف، لأن غيابها هو إشارة مباشرة إلى أنه لم يتم تطبيق أي مجموعة فرعية
var
Pdf: THotPDF;
Output: TMemoryStream;
begin
Output := TMemoryStream.Create;
try
Pdf := THotPDF.Create(nil);
try
Pdf.RegisterUnicodeTTF('C:\Fonts\DejaVuSans.ttf');
Pdf.BeginDoc;
Pdf.CurrentPage.SetFont('DejaVu Sans', [], 11);
Pdf.CurrentPage.TextOut(72, 760, 0, 'Subset me');
Pdf.EndDoc;
Pdf.SaveToStream(Output);
finally
Pdf.Free;
end;
// A few glyphs from a ~700 KB face must not yield a multi-hundred-KB stream.
if Output.Size > 100 * 1024 then
raise Exception.Create('Font subset did not shrink the output');
finally
Output.Free;
end;
end;
بالنسبة لإخراج PDF/A، يكون الفحص أكثر حدة، لأن المدقق يقوم بالعمل نيابة عنك. اضبط مستوى المطابقة وقم بتشغيل النتيجة عبر veraPDF: يتم الإبلاغ عن /CIDSet المفقود، أو المجموعة الفرعية التي لا تتطابق مع الواصف، كبند فاشل بدلاً من تركه لتلاحظه بالعين. مفاتيح المطابقة التي تدفع عمل الإنهاء هذا هي خصائص (properties) في المستند. يأخذ PDFACompliance سلسلة مثل '2B' لـ PDF/A-2 Level B، و PDFUACompliance هو قيمة منطقية (boolean) تعمل على تشغيل متطلبات PDF ذي العلامات (tagged-PDF) وترتيب علامات التبويب (tab-order)
Pdf := THotPDF.Create(nil);
try
Pdf.PDFACompliance := '2B'; // PDF/A-2 Level B, drives /CIDSet emission
Pdf.PDFUACompliance := True; // stamps /Tabs /S on annotated pages
Pdf.RegisterUnicodeTTF('C:\Fonts\NotoSansSC-Regular.ttf');
Pdf.BeginDoc;
Pdf.CurrentPage.SetFont('Noto Sans SC', [], 12);
Pdf.CurrentPage.TextOut(72, 760, 0, '合规报告');
Pdf.EndDoc;
Pdf.SaveToFile('Report_PDFA.pdf');
finally
Pdf.Free;
end;
الدرس الهندسي
تخرج قاعدتان من هذا. الأولى هي أن أي خطوة إنهاء تقوم بتحويل الكائنات يجب أن يتم تشغيلها قبل إجراء تسلسل (serialized) لتلك الكائنات، ويجب قراءة المرحلة الختامية لمحرك المستندات كخط أنابيب (pipeline) مرتب حيث يكون التسلسل هو الإجراء الأخير، وليس إجراءً واحداً من بين العديد من الإجراءات. والثانية هي التي كلفت معظم الوقت هنا: بالنسبة لخطوة الإرسال، فإن غياب الخطأ ليس دليلاً على النجاح. الروتين الذي يبني المجموعة الفرعية الصحيحة ويطبقها على الرسم البياني الخاطئ والمكتوب بالفعل لا يبلغ عن أي خطأ، لأنه من وجهة نظره الخاصة لم يكن هناك شيء خاطئ. يجب أن ينظر التحقق (Verification) إلى القطعة الأثرية (artifact)، وليس رمز الإرجاع (return code). تحقق من حجم الإخراج، واقرأ طول بايت الخط المضمن وبادئة /BaseFont الخاصة به، ودع veraPDF يحكم على إخراج PDF/A حيث يحول فقدان /CIDSet النقص الصامت إلى فشل مسمى
تمت تغطية جانب المنتج (producer side) لمعالجة الخطوط، وكيف يتم تسجيل الوجوه (faces) وتضمينها لإخراج التقرير، في مقالنا حول الخطوط والصور في إخراج التقرير. تمت تغطية جانب التحقق من الصحة (validation side)، حيث يتم فحص خطوات الإنهاء هذه مقابل المعايير، في المعاينة الشاملة (walkthrough) على التحقق من صحة PDF/A و PDF/UA. كلاهما يقترن بعمل المجموعة الفرعية والمطابقة الموصوف هنا، والذي يتم شحنه كجزء من مكون HotPDF لـ Delphi و C++Builder جنباً إلى جنب مع واجهات برمجة التطبيقات (APIs) للتحميل والتحرير والتشفير والتوقيع التي تمت تغطيتها في مكان آخر في هذه المدونة