كل رقم حقيقي يصل إلى ملف PDF يجب كتابته بنقطة، مهما كان ما يعتقده نظام التشغيل عن فاصل الأعداد العشرية. يمرر مكوّن HotPDF لـ Delphi جميعها عبر طبقة تحويل ثابتة واحدة في HPDFTypes، لذا تصدر محطة عمل ألمانية أو فرنسية أو برازيلية 0.5 0 0 0.5 5.5 7.25 cm وليس الصيغة المفصولة بفواصل التي لن يحللها أي قارئ مطابق
يبدو ذلك إصلاحاً من سطرين. لم يكن. كان العيب مموهاً لسنوات بأثر جانبي، وعاد للظهور في ثلاثة أنظمة فرعية لا يفكر أحد فيها ككود تنسيق أرقام، واختفى تحت مترجم واحد وبقي حياً تحت آخر، وآخر طبقة منه لم تكن لها علاقة بفاصل الأعداد العشرية أصلاً
لماذا يفسد إعداد الفاصلة العشرية تدفق محتوى PDF؟
لأن ISO 32000-1 §7.3.3 يعرّف الكائن الرقمي كعلامة اختيارية وأرقام عشرية ونقطة واحدة على الأكثر. الفاصلة ليست جزءاً من تلك القاعدة الإنتاجية، وهي ليست فاصل تعبير بموجب §7.2.2 أيضاً، لذا لا يرى القارئ معاملين حيث قصدت واحداً. يرى رمزاً يفشل في التحليل المعجمي، ويُتخلص من العامل الذي احتواه. 0,5 واحدة في مصفوفة حالة رسوميات تسقط كتلة q ... Q كلها حولها. وللفشل شكل مميز في الميدان: لا يتكرر أبداً على جهاز المطور، ويصل كتذكرة دعم من بلد واحد، ويُقرأ كعيب عرض لا كخطأ صياغة، لأن معظم العارضين يسقطون العامل المشوّه بصمت ويعرضون بقية الصفحة
// عملية مضيفة تعمل بـ DecimalSeparator = ','
Doc.LoadFromFile('invoice.pdf');
Doc.RedactLoadedRect(0, 10.25, 20.5, 40.75, 60.5, 0.5, 0.25, 0.125);
Doc.StitchLoadedPage(0, 1, 5.5, 7.25, 0.5);
// ما كان يحمله /Contents الملحق:
// 0,5 0,25 0,125 rg 10,25 20,5 30,5 40 re f
// q 0,5 0 0 0,5 5,5 7,25 cm /StitchSrc Do Q
//
// ليس لدى ISO 32000-1 7.3.3 قاعدة إنتاج لـ "0,5"، لذا يختفي
// مستطيل التنقيح والصفحة الملحومة معاً دون خطأ
الأثر الجانبي الذي أخفى العيب لسنوات
تكتب HotPDF معظم أرقام مسار الإنشاء عبر _CutFloat، وكانت المراجعات المبكرة لتلك الدالة تفعل شيئاً عدائياً بهدوء: في كل استدعاء واحد كانت تعين '.' لـ FormatSettings.DecimalSeparator العام في العملية المضيفة. ذلك يفسد حالة الإعداد المحلي للتطبيق المستضيف للمكوّن وليس آمناً للخيوط، لكن العاقبة المثيرة هي الثالثة. بمجرد توليد أي محتوى، صار فاصل العملية نقطة، لذا أصبح كل FloatToStr و StrToFloat عارٍ في بقية المكتبة صحيحاً بالصدفة. لذلك نجح ترتيب أنشئ ثم حرّر الذي يستخدمه كل مثال تقريباً، وما كان يخفي هو كل مسار يعمل قبل أول استدعاء _CutFloat: محررات المستند المحمل StitchLoadedPage و RedactLoadedRect، وطوابع النص والصور، والعلامات المائية المركبة، ورسم الأكواد الشريطية عبر Format('%.2f')، ومحلل المظهر الافتراضي /DA، ونظام خطوط Type 1 كله
فشل تحليل Type 1 بأبشع طريقة بين الجميع. FontMatrix و BlueValues وتلميحات الجذوع والأرقام الحقيقية داخل charstrings من Type 2 كلها أرقام PostScript مكتوبة بنقاط، بموجب مواصفة تنسيق خط Adobe Type 1. تحليل 0.001 تحت إعداد الفاصلة يرفع EConvertError، وابتلعه try/except محيط، فبقيت مصفوفة الخط صفراً. انهار بعد ذلك كل محرف من ذلك الخط، وهو ما يبدو مشكلة خط لا مشكلة إعداد محلي ويقودك إلى تصحيح عرض المحارف المضمّنة بعد ظهيرة كاملة
طبقة تحويل واحدة بدل رقع مبعثرة
القاعدة التي تتبعها HotPDF الآن صريحة: أي رقم يُكتب في أو يُقرأ من صياغة PDF أو PostScript أو JSON أو JavaScript يمر عبر تحويل ثابت صريح، ولا يعتمد أي كود في أي مكان على أثر جانبي لـ FormatSettings عام. تملك HPDFTypes التحويلات الأربعة، زائد سجل الإعدادات الذي تتشاركه ومساعد دقة ثابتة واحد لمسار الأكواد الشريطية
// Lib/HPDFTypes.pas - سطح التحويل الثابت الوحيد
function HPDFInvariantFormatSettings: TFormatSettings;
function HPDFFloatToInvariantStr(Value: Extended): string;
function HPDFTryInvariantStrToFloat(const S: string;
out Value: Extended): Boolean;
function HPDFInvariantStrToFloat(const S: string): Extended;
function HPDFInvariantStrToFloatDef(const S: string;
const Default: Extended): Extended;
function HPDFFormatInvariant2(Value: Extended): string;
كانت وحدتان تحملان أصلاً دالة InvariantFormatSettings خاصة بهما، HPDFContentStream و HPDFTextExtraction، واختُزلت كلتاهما إلى إعلان تقديمي من سطر واحد بدل تركهما كتنفيذين متوازيين. نظام Type 1 هو الاستثناء المتعمد: لا يحمل تبعية VCL ولا يشير إلى HPDFTypes، لذا يُبقي نسخة محلية مرآة اسمها HPDFType1Core.Type1StrToFloat بمعانٍ مطابقة، على نظرية أن دالة من عشرة أسطر مكررة التزام أصغر من جر طبقة الرسوميات إلى محلل خطوط. تُرك مكانان آخران على حالهما عمداً. يحلل HPDFHTMLImport أطوال CSS بتبديل النقطة بفاصل الإعداد المحلي ثم يحلل بالإعداد المحلي، وهو الصواب لذلك المدخل، و HPDFType1CB.GlyphCommands مع سلاسل FontBBox واجهة عامة يقرؤها المتصلون بواسطة StrToFloat الخاص بهم، حيث يكون الإنتاج والاستهلاك في نفس الإعداد المحلي متناسقين ذاتياً وكان المخرج الثابت سيكسر كل متصل في منطقة فواصل
ماذا يضمن HPDF_HAS_FORMAT_SETTINGS عبر المترجمات؟
يضمن أن المسار الثابت يترجم فعلاً في كل سلسلة أدوات مدعومة، وهو ما لم يفعله حارس الإصدار الواضح. قدّم Delphi XE2 TFormatSettings.Create والحمل الزائد FloatToStr / TryStrToFloat الذي يأخذ TFormatSettings، لذا فإن الغريزة الأولى هي كتابة {$IFDEF XE2+}, InvariantFormatSettings{$ENDIF} في كل موضع استدعاء. لتلك الغريزة نقطة عمياء: يعرّف HotPDF.inc D2009+ تحت Free Pascal لكنه لا يعرّف XE2+ أبداً، لأن XE2+ يجيب عن سؤال في أرقام إصدارات Delphi، بينما يشحن Free Pascal 3.2.2 تلك الأحمال الزائدة بغض النظر. لذلك تدهور كل حارس من تلك الحراس إلى الفرع المعتمد على الإعداد المحلي في بنيات FPC، بصمت، على المنصات نفسها التي جاءت منها البلاغات الأصلية بالضبط. HPDF_HAS_FORMAT_SETTINGS معرّف في كتلة FPC وكتلة XE2+ في HotPDF.inc، وهو الرمز الوحيد الذي تختبره مساعدات الأرقام
{$IFDEF HPDF_HAS_FORMAT_SETTINGS}
function HPDFInvariantFormatSettings: TFormatSettings;
begin
{$IFDEF FPC}
Result := DefaultFormatSettings; // لا يوجد TFormatSettings.Create في FPC
{$ELSE}
Result := TFormatSettings.Create;
{$ENDIF}
Result.DecimalSeparator := '.';
Result.ThousandSeparator := ',';
end;
{$ENDIF}
فرع مترجم واحد يبقى حياً داخل الدالة، لأن Free Pascal لا يملك TFormatSettings.Create وعليك أن تبدأ من DefaultFormatSettings. فاصل الأرقام فقط هو ما يغذي التحويلات، لذا تبقى حقول التاريخ والعملة بما يقدمه الإعداد المحلي الحالي. أما Delphi ما قبل XE2 فلا أحمال زائدة عنده إطلاقاً، وهناك تتراجع المساعدات إلى التحويل بإعداد العملية ثم إعادة كتابة حرف الفاصل في السلسلة الناتجة، وهو قبيح لكنه لا يلمس الحالة العامة أبداً
الطبقة الثالثة: أسس تسمح بها SVG ولا يسمح بها PDF
إصلاح فاصل الأعداد العشرية ضروري وغير كافٍ. يقر W3C SVG 1.1 §4.2 بالأس في قاعدة إنتاج أرقامه وصياغة XPS تفعل الشيء نفسه، لذا فإن 1E-6 إحداثي صالح تماماً عند الدخول، بينما لا يقر ISO 32000-1 §7.3.3 بأي أس إطلاقاً. قيمة تنجو من التنسيق الثابت ما زال بإمكانها الهبوط في تدفق محتوى كـ 1E-6 وتُرفض، و FloatToStrF بالصيغة الثابتة يمكنها نفسها أن تتراجع إلى صيغة الأس بمجرد أن تخرج القيمة من نطاق أرقام مكتبة التشغيل. لذلك يقوم HPDFSafeSVG.SVGFloat بعمله الخاص بعد التحويل الثابت: يمسح أي شيء تحت 1.0E-7 إلى صفر، ثم إذا نجا E يجرّد الأس وينقل الفاصلة العشرية بيده، حاشياً بالأصفار في أي جهة يتطلبها التحول. يحرس SVGFinite المدخل برفض NaN وأي شيء خارج ±1.0E30 قبل وصوله إلى المُنسِّق، ويتولى SVGReadNumber مع SVGSkipSeparators الاتجاه الوارد حرفاً بحرف بدل الإحالة إلى مكتبة التشغيل
// Lib/HPDFSafeSVG.pas - تدوين عشري لمعاملات PDF
if Abs(Value) < 0.0000001 then
Value := 0;
S := HPDFFloatToInvariantStr(Value);
ExponentAt := Pos('E', UpperCase(S));
if ExponentAt > 0 then
begin
ExponentValue := StrToInt(Copy(S, ExponentAt + 1, MaxInt));
SetLength(S, ExponentAt - 1);
// ... انقل الفاصلة العشرية بمقدار ExponentValue مع الحشو بـ '0'
// على اليسار أو اليمين، ثم أعد ربط الإشارة
end;
هذا هو نفس TSVGBuffer الذي يغذي تصدير صفحات SVG ومعالجة حالة الرسوميات، ونفس الانضباط الرقمي ينطبق على جهة الاستيراد عند تحويل مستندات XPS و OXPS إلى PDF عبر HPDFCreateXPSDocumentHandler وعضو hdfXPS في THPDFHandledDocumentFormat. صيغ الترميز كريمة في صياغة الأرقام. تدفق محتوى PDF ليس كذلك، وحدود التحويل هي المكان الذي يجب أن تُحل فيه ذلك الفرق
اختبارات انحدار تعيد إنتاج الإعداد المحلي بدل افتراضه
عيب الإعداد المحلي الذي يظهر فقط على جهاز شخص آخر يستحق تثبيته باختبارات تضبط الإعداد بنفسها، وتحمل HotPDF اختبارين. يعين LoadedEditsEmitInvariantNumbersUnderCommaLocale فاصلة لـ FormatSettings.DecimalSeparator قبل أول تحرير، ويشغّل RedactLoadedRect و StampLoadedPageText و StitchLoadedPage وطابعاً مركباً، ثم يقرأ بايتات محتوى الصفحة عائداً ويؤكد وجود سلاسل العوامل الدقيقة وخلو المخزن من أي فاصلة في أي مكان. يغطي DocumentCreationEmitsInvariantNumbersAndKeepsHostLocale النصف الآخر والخطيئة الأصلية: يقود مسار الإنشاء تحت نفس إعداد الفاصلة، بما فيه DirectDrawBarcode بتنسيقه الثابت ذي العشرين المنزلين، ثم يؤكد أن تدفق المحتوى بلا فواصل وأن FormatSettings.DecimalSeparator ما زال فاصلة عندما يعود EndDoc
التأكيد الثاني هو الذي كان سيلتقط العيب الأصلي يوم كتابته، لأنه يختبر ما تفعله المكتبة بمضيفها لا ما تكتبه على القرص فقط. الدرس يعمّم وراء PDF: أي صيغة بقواعد معجمية ثابتة، بما فيها PostScript و JSON و JavaScript، تحتاج تحويلاً ثابتاً صريحاً عند الحدود. الوصول إلى إعداد عام للحصول على الإجابة الصحيحة ينجح حتى يستدعي أحدهم كودك بترتيب مختلف، أو على خيط مختلف، أو من مترجم مختلف، وعندها يبدو الفشل أي شيء إلا مشكلة تنسيق
طبقة تنسيق الأرقام الموصوفة هنا تشحن كجزء من مكوّن HotPDF لـ Delphi لـ Delphi و C++Builder، إلى جانب مسارات تحرير المستند المحمل والأكواد الشريطية وتصدير SVG وتحويل XPS التي تعتمد عليها