مقال تقني

أعداد PDF محايدة اللغة لتطبيقات Delphi ذات الفاصلة العشرية

يكتب PDFlibPas، مكتبة losLab PDF Developer Library لـ Delphi، كل عدد يضعه في تدفق محتوى بفاصل عشري نقطة ودون أُس، مهما قالت الإعدادات الإقليمية لويندوز. ومنذ v3.539.26 تنسق AddPageMatrix و ScalePage و DeskewPage و RedactRegion ومخرجات النص إلى مسار وإعادة التلوين معاملاتها عبر PLDoubleToStrConst، ومنذ v3.539.33 يستخدم المحللون الذين يقرؤون تلك الأعداد من جديد PLTryStrToFloatInvariant بدل لغة النظام. وعلى جهاز ألماني أو فرنسي أو برازيلي ينتج الكود نفسه البايتات نفسها كما على جهاز أمريكي، وهو السلوك الوحيد الذي تحتمله صيغة ملف

لماذا يفسد locale الفاصلة العشرية ملف PDF دون رفع خطأ؟

يُفسد locale الفاصلة العشرية ملف PDF بصمت لأن الفاصلة ليست محرف عدد في صيغة PDF، فيُقرأ الضرر بوصفه رموزاً صالحة بمعنى خاطئ. قبل الإصلاح لم تكن PLFloatToStr سوى استدعاء FloatToStr مجرد، و FloatToStr تتبع FormatSettings.DecimalSeparator. وبفاصل فاصلة كتبت AddPageMatrix(0.5, 0.5, 0, 0) سطر 0,5 0 0 0,5 0 0 cm. وتسمح ISO 32000-1 §7.3.3 في العدد بأرقام وفترة واحدة وإشارة افتتاحية ولا شيء غير ذلك، فيقرأ محلل المحتوى ذلك السطر بوصفه العدد 0 يليه رمزاً مجهولاً ,5، وينتهي معامل cm بمعاملات خاطئة. لا شيء يُرفع ولا يُسجَّل. الصفحة ترسم ببساطة بمصفوفة تحويل انحرفت، والعمل رجوعاً من رسم في غير موضعه إلى إعداد لغوي عصرٌ بؤس

العيب الثاني يختبئ خلف الأول. تستخدم FloatToStr الصيغة ffGeneral التي تنتقل إلى ترميز الأُس متى هبط المقدار تحت 1E-4، فخرج إزاحة صغيرة على صورة 1E-5. وتنص §7.3.3 نفسها على أن PDF لا يدعم صورة الأُس، أي أن جهازاً بلغة أمريكية أيضاً كان يمكن أن يكتب معاملاً غير صالح لو صغرت القيمة كفاية. واختبارات الانحدار لهذا الإصدار تثبت صورتي الفشل كلتيهما: تقلب الفاصل إلى فاصلة، وتستدعي الـ API، وتمسح المحتوى الناتج بحثاً عن أي رمز يحوي فاصلة أو أُساً

uses
  System.SysUtils, PDFlibrary;

var
  Lib: TPDFlib;
  OldSeparator: Char;
begin
  Lib := TPDFlib.Create;
  try
    Lib.SetPageDimensions(300, 200);
    Lib.DrawBox(10, 10, 20, 20, 1);
    OldSeparator := FormatSettings.DecimalSeparator;
    try
      FormatSettings.DecimalSeparator := ',';   // محاكاة سطح مكتب ألماني
      Lib.AddPageMatrix(0.5, 0.25, 1E-5, 12.75);
      // v3.539.26 وما بعده يكتب: 0.5 0 0 0.25 0.00001 12.75 cm
      // الإصدارات الأقدم كتبت:  0,5 0 0 0,25 1E-5 12,75 cm
      Writeln(Lib.GetPageContentToString);
    finally
      FormatSettings.DecimalSeparator := OldSeparator;
    end;
  finally
    Lib.Free;
  end;
end;
كتبت PDFlibPas ‏AddPageMatrix على سطح مكتب فاصلة عشرية سطر 0,5 0 0 0,25 1E-5 12,75 cm قبل الإصلاح، وهو ما يقرؤه محلل PDF بوصفه العدد 0 مع رموز مجهولة، فيبقى cm بمعاملات خاطئة والصفحة محولة بصمت، بينما يكتب PLDoubleToStrConst عشريات نقطة صالحة
الفاصلة ليست محرف عدد في صيغة PDF، فيُقرأ الضرر رموزاً صالحة بمعنى خاطئ — وأُسّات ffGeneral مثل 1E-5 كانت غير صالحة على كل اللغات لا الفاصلة منها فقط

نوعان من الأعداد، عائلتان من المساعدات

الإصلاح في PDFlibPas فصل صارم: الأعداد المعروضة على الناس يجوز أن تتبع اللغة، والأعداد المكتوبة للآلة لا تتبعها أبداً. تبقى PLFloatToStr و PLStrToFloat في PDFlibExtra.pas للنص المواجه للمستخدم، وإعلانهما يحمل الآن تعليقاً يقول ذلك بالضبط. وكل ما ينتهي صيغة PDF يمر عبر PLDoubleToStrConst بعدد منازل عشرية ثابت يُختار حسب العمل: ست للمصفوفات، وأربع للإحداثيات وضبطات TJ، وثلاث للألوان ومستطيلات FDF. وتدقيق v3.539.26 لمس مواضع استدعاء أكثر مما اقترحه تقرير العيب الأصلي:

  • AddPageMatrix و ScalePage و DeskewPage، التي تسجل كل منها cm في مقدم محتوى الصفحة القائم
  • بانو عناصر الصفحة التي تطلق إعادات ضبط Tm وتقدمات TJ وتحويلات cm
  • مصفوفات مواضع المحارف ونقاط المخطط التفصيلي في محول النص إلى مسار
  • صندوق التعبئة الأسود الذي يسجله RedactRegion في المقدم، وقيم /Rect في تصدير FDF، والمعاملات التي تكتبها إعادة التلوين

PLDoubleToStrConst منسّقة مكتوبة يدوياً لا غلاف حول FloatToStrF، وثلاث من خصائصها مهمة هنا. تكتب دائماً فترة وتقلع الأصفار اللاحقة، فتبقى 0.5 على صورة 0.5 لا 0.500000. ولا تكتب أُساً لمدخل منتهٍ أبداً. والقيمة غير الصفرية الأصغر من الدقة المطلوبة تحتفظ بأرقامها المعنوية بدل الانهيار إلى صفر، فتعيد PLDoubleToStrConst(1E-9, 6) القيمة 0.000000001؛ ولا تصبح القيم 0 إلا تحت 5E-16 تقريباً. وسبب وجود تلك القاعدة الأخيرة أن تقريب معامل تحجيم صغير إلى صفر يحول مصفوفة صالحة إلى مفردة، وهو عيب أسوأ من العيب الذي يُصلَح

يقسم PDFlibPas تنسيق الأعداد في قسمين: تبقى PLFloatToStr و PLStrToFloat مرتبطتين باللغة للنص المواجه للمستخدم، بينما تنسق PLDoubleToStrConst و PLTryStrToFloatInvariant كل ما يصبح صيغة PDF بنقطة ودون أُس وبدقة ثابتة للعمل ست أو أربع أو ثلاث منازل
المنسّقة المستقلة عن اللغة مكتوبة يدوياً عن قصد: تقلع الأصفار اللاحقة، ولا تكتب أُساً أبداً، وتبقي الأرقام المعنوية للقيم الصغيرة، لأن تقريب معامل تحجيم إلى صفر سيجعل مصفوفة صالحة مفردة
uses
  PDFlibExtra;

procedure CheckNumberHelpers;
var
  V: Double;
begin
  // مخرجات الآلة: عشريات نقطة، دون أُس، بلا أصفار لاحقة
  Assert(PLDoubleToStrConst(1E-5, 6) = '0.00001');
  Assert(PLDoubleToStrConst(-0.5, 6) = '-0.5');
  Assert(PLDoubleToStrConst(0.000012346, 4) = '0.00001235');  // يبقي 4 أرقام معنوية
  Assert(PLDoubleToStrConst(12345.25, 4) = '12345.25');

  // مدخلات الآلة: فشل هادئ بدل EConvertError
  Assert(PLTryStrToFloatInvariant('0.5', V) and (V = 0.5));
  Assert(not PLTryStrToFloatInvariant('0,5', V));   // أعداد المحتوى لا تستخدم فاصلة أبداً
end;

لماذا جهة التحليل أخطر من جهة الكتابة؟

جهة التحليل أخطر لأن محللاً مرتبطاً باللغة لا ينتج عدداً خاطئاً بل يرمي. تستدعي PLStrToFloat ‏StrToFloat التي ترفع EConvertError حين لا يطابق النص فاصل النظام. وعلى نظام فاصلة عشرية كان ذلك يعني أن RecolorPage تتوقف لحظة تقابل معامل 0.5 g عادياً، فكل صفحة واقعية تفشل لا الغريبة منها فقط. ورفضت RenderPageRegionToFile صيغة المقص الموثقة الخاصة بها "10.5,20.5,50.5,40.5"، وخصائص أطوال SVG وألوان تصدير SVG وقوائم رؤوس التعليقات وقيم صلابة output intent إما رُفضت أو استُبدلت بصمت بالافتراضات. ومكتبة تعمل على جهاز المطور بلا عيب وتفشل عند أول عميل في ميونخ هي بالضبط نوع الكود الذي، مثل حالات مقالة كود Delphi الذي يعمل بالمصادفة، لا يبدو صحيحاً إلا بسبب المكان الذي اختُبر فيه

صنفت v3.539.33 كل استدعاء StrToFloat و TryStrToFloat حسب مصدر مدخله. معاملات تدفق المحتوى وخصائص SVG وسلاسل ألوان الرسام وقوائم المقص والرؤوس المفصولة بفواصل لها صيغة نقطة ثابتة، فتمر الآن عبر PLTryStrToFloatInvariant التي تقلم النص، وتحلله بـ PLInvariantFormatSettings، وتعيد False على مدخل فارغ أو مشوه أو غير منتهٍ بدل أن ترفع. والقائمة المفصولة بفواصل لا تترك مجالاً للتسوية، لأن الفاصلة لا يمكن أن تكون محدداً للقائمة وعلامة عشرية معاً. وأصلحت التمريرة نفسها كتابة خارج الحدود أيضاً: كانت RenderPageRegionToFile تخزن قيمة مقص خامسة بعد مخزنها من أربعة عناصر. ولمسار إعادة التلوين الموصوف في دليل تحويل PDF إلى فضاء لون واحد فإن النتيجة العملية أن RecolorPage و RecolorDocument لم تعدا يتوقفان على نظام فاصلة عشرية. وقيم القواعد التي يكتبها المستدعي في CheckDocumentPolicy هي حالة التحليل الوحيدة التي تستخدم المساعدة المتساهلة بدلاً منها، وللسبب الذي يشرحه القسم التالي

ماذا يحدث إن أصلحت طرفاً واحداً من رحلة ذهاب وإياب؟

إصلاح طرف واحد من رحلة لغوية ذهاب وإياب يكسر كوداً كان يعمل، ولهذا نقل تغيير خصائص البنية في v3.539.32 الكاتب والقارئ معاً. تعمل أغلفة SetStructElem* تمرير الأعداد سلاسل: تنسق SetStructElemBBox أربع قيم في سلسلة واحدة، وتخزنها عبر AddTagAttribute، ثم يحلل كاتب /A تلك السلسلة لاحقاً ليقرر هل تصبح عدداً أم مصفوفة أم اسماً. وكلا الطرفين كان يستخدم لغة النظام، فكانت الرحلة على نظام فاصلة عشرية متسقة مع نفسها. وظهر العيب فقط حين اتبع مستدٍع التوثيق ومرر "0.5" إلى AddTagAttribute: لم يستطع القارئ تحليلها فأطلق اسم PDF ‏/0.5. ولموضع PDF/VCR المؤقت المشكلة المعكوسة، لأن المكتبة كانت تولد GTS_BBox بنقطة ثم تتحقق منها باللغة قبل الحفظ

تغيير الكاتب وحده إلى نقطة كان أسوأ من لا شيء، لأن كل قيمة SetStructElem* كانت ستفشل عند القارئ المرتبط باللغة وتتدهور إلى اسم. لذا يستخدم الكتّاب الآن PLDoubleToStrConst(v, 6)، والقارئ يستخدم PLTryStrToFloatLenient الجديدة، التي تجرب صورة النقطة أولاً وترجع إلى لغة النظام. فالمستدعي بلغة الفاصلة الذي مرر "1,25" في الماضي ما زال يحصل على العدد 1.25. والمقايضة متعمدة وموثقة: على نظام ألماني كانت "1.500" تصبح اسماً لأن StrToFloat ترفض فواصل الآلاف، وهي الآن تقرأ 1.5، بينما لم تعد السلسلتان الحرفيتان NAN و INF تُقبلان عددين

تعمل PDFlibPas ‏SetStructElemBBox وأخواتها تمرير الأعداد سلاسل عبر AddTagAttribute، ويحلل كاتب /A تلك السلاسل من جديد، فنقل v3.539.32 الطرفين معاً: تكتب PLDoubleToStrConst بنقطة وتقرأ PLTryStrToFloatLenient النقطة أولاً مع رجوع إلى اللغة، فتبقى 1,25 بلغة الفاصلة تقرأ 1.25
إصلاح الكاتب وحده كان سيتدهور بكل خصائص العناصر المهيكلة إلى اسم PDF، ولهذا تتحرك رحلة الذهاب والإياب بطرفيها معاً أو لا تتحرك
uses
  System.SysUtils, PDFlibrary;

var
  Lib: TPDFlib;
begin
  FormatSettings.DecimalSeparator := ',';   // مستدعٍ بنظام فاصلة عشرية
  Lib := TPDFlib.Create;
  try
    Lib.BeginTag('Figure', 'Sales chart', '');
    Lib.SetStructElemBBox(10.5, 20.25, 200.5, 100.75); // /BBox [ 10.5 20.25 200.5 100.75 ]
    Lib.AddTagAttribute('Layout', 'SpaceAfter', '0.5');   // ‏/SpaceAfter 0.5، وكانت /0.5
    Lib.AddTagAttribute('Layout', 'StartIndent', '1,25'); // ما زالت /StartIndent 1.25
    Lib.DrawText(20, 20, 'figure');
    Lib.EndTag;
    Lib.SaveToFile('tagged.pdf');
  finally
    Lib.Free;
  end;
end;

أين يُوقف NaN واللانهاية

ترفض AddPageMatrix و ScalePage و RedactRegion الآن وسائط NaN واللانهاية من البداية وتعيد 0، لأن لا عدد PDF يستطيع تمثيلها. وكانت ScalePage ترفض أصلاً العوامل صفرية أو أقل، لكن NaN تجتاز اختبار <= 0، فعامل NaN كان يسافر حتى المنسّقة كل الطريق. وفي v3.539.26 كانت تلك المنسّقة لا تزال تستدعي Round على NaN، الذي يرفع EInvalidOp على Win32 حيث وحدة x87 لا تحجب العمليات غير الصالحة؛ وجعلت v3.539.31 ‏PLDoubleToStrConst تكتب 0 لـ NaN سطر دفاع أخير، لكن الصفر في مصفوفة تحويل مفرد، فالفحص على مستوى الـ API يبقى الإصلاح الحقيقي. ويبقى حدّان في مكانهما عمداً. سلاسل حالة الملفات التعريفية تُكتب وتُقرأ باللغة داخل عملية واحدة ولا تغادرها أبداً، فتُركت وشأنها. واختبار ينقّ 1E-5 عبر مسار عناصر الصفحة يجب أن يقرأ المحتوى قبل إعادة كتابة الطبقة، لأن إعادة إطلاق المعاملات بدقة المستند تحول تلك القيمة مشروعاً إلى 0

إن كان تطبيقك يشحن إلى عملاء خارج عالم النقطة العشرية فالعادة الأسلم هي التي تستخدمها حزمة اختبارات PDFlibPas الآن: شغّل مسارات إنتاج PDF مرة واحدة مع FormatSettings.DecimalSeparator مضبوطة فاصلة وامسح المخرج بحثاً عن فواصل وأُسّات. و مقالة الحفاظ على دقة العشريات المحللة تغطي النصف الآخر من القصة نفسها، كيف تحتفظ الأعداد المقروءة من ملف قائم بنصها الدقيق عند الحفظ. والتحميلات والمرجع الكامل للـ API وبناء التجربة على صفحة منتج PDFlibPas Delphi PDF library