مقال تقني

قواعد أعداد PDF مقابل JSON: NaN و Infinity و null في Delphi

يطلق PDF Library for Delphi (PDFlibPas) ‏JSON صالحاً لكل عدد PDF منذ v3.539.31. تعيد GetObjectJSON كتابة الرموز التي قبلها ISO 32000-1 ويرفضها RFC 8259، مثل -.25 و +1.5 و 007.5، إلى -0.25 و 1.5 و 7.5 رقماً برقم؛ ويكتب GetDocumentJSON والتقارير التحليلية null لـ NaN و Infinity؛ وتكتب PLDoubleToStr القيمة 0 لـ NaN بدل أن ترفع EInvalidOp في منتصف تصدير. قبل الإصلاح كانت المكتبة تستطيع إنتاج JSON يرفض قارئها هو تحميله من جديد

لماذا يكسر عدد PDF صالح ملف JSON؟

لأن القاعدتين تختلفتان في أربع تفاصيل صغيرة، ومحلل PDF يحترم النص المصدر سيحمل تلك التفاصيل كما هي إلى المخرج. تسمح ISO 32000-1 §7.3.3 لعدد أن يبدأ بإشارة زائد، وأن يحذف الجزء الصحيح (.5)، وأن ينتهي بنقطة مجردة (4.)، وأن يحمل أصفاراً افتتاحية (007.5). ولا يسمح RFC 8259 §6 بأي من ذلك: ناقص اختياري، وجزء صحيح إما 0 أو يبدأ من 1 إلى 9، ورقم واحد على الأقل بعد أي نقطة عشرية. والمنتجون أحرار في كتابة الصور PDF، وكثير من المولدات والملفات المحررة يدوياً يفعل

جاء التسرب من خاصية دقة متعمدة. منذ v3.539.19 تعيد TPDFNumeric.Output النص الدقيق الذي حلله الـ tokenizer للأعداد الحقيقية، وهذا ما يبقي قيمة لون معايَرة دقيقة عند الحفظ، كما في الحفاظ على دقة العشريات المحللة في PDF. وكان الـ tokenizer يرقّع .5 إلى 0.5 و 4. إلى 4.0 في طريق الدخول أصلاً، والأعداد الصحيحة يعاد تنسيقها من قيمتها، فـ +3 تعود 3. وما ينجو حرفياً هو الباقي: نقطة موقعة افتتاحية (-.25)، وزائد صريح على عدد حقيقي (+1.5)، وأصفار افتتاحية (007.5). وكان كاتب الكائنات القديم يلحق Output مباشرة بعد "value":، و TJSONParser.ParseNumber في قارئ المكتبة نفسه يتوقف عند كل واحدة منها برسالة «Invalid JSON number»، فينجح التصدير ويفشل الاستيراد بخطأ PDFLIB_ERROR_OBJECT_JSON_INVALID (105)

تعيد PDFlibPas ‏GetObjectJSON كتابة رموز أعداد PDF التي يرفضها RFC 8259 رقماً برقم: تصبح -.25 ‏-0.25، ويطيح +1.5 بزائده، وتضيع أصفار 007.5 الافتتاحية، بينما تنجو الأرقام العشرية مثل 1.250000، لأن التنسيق من Double المخزنة يضيف ضجيجاً ثنائياً
كان الكاتب القديم يلحق النص المحلل حرفياً، ويتوقف قارئ المكتبة نفسه برسالة Invalid JSON number، وكسر الخطأ 105 رحلة ذهاب وإياب سماها طرف التصدير نجاحاً
uses
  System.SysUtils, PDFlibrary;

var
  Lib: TPDFlib;
  JSON: AnsiString;
begin
  Lib := TPDFlib.Create;
  try
    if Lib.LoadFromFile('legacy-drawing.pdf', '') = 0 then
      raise Exception.Create('load failed');

    // الكائن 12 مصفوفة مكتوبة [-.25 +1.5 007.5]
    JSON := Lib.GetObjectJSON(12, 0);
    // v3.539.31 وما بعده: تصل القيم -0.25 و 1.5 و 7.5

    // ‏SetObjectJSON لا تقبل خيارات، فمرر 0
    if Lib.SetObjectJSON(12, JSON, 0) = 0 then
      raise Exception.CreateFmt('round trip rejected, error %d',
        [Lib.LastErrorCode]);

    Lib.SaveToFile('legacy-drawing-roundtrip.pdf');
  finally
    Lib.Free;
  end;
end;

كيف يحفظ PDFNumberTextToJSON كل رقم؟

تعيد PDFNumberTextToJSON تهجئة الرمز بدل إعادة حسابه من Double. تقرأ الدالة في PDFlibObjectJSON إشارة اختيارية، وتجمع الأرقام قبل نقطة عشرية واحدة وبعدها، ثم تطبق التعديلات التي يطلبها JSON وحدها: تطيح بالزائد، وتقلع الأصفار الافتتاحية مع الإبقاء على واحد، وتسد 0 حين يكون الجزء الصحيح فارغاً، وتطيح بنقطة ختامية مجردة وتعيد الناقص. والرمز الذي يحوي أي محرف آخر، أو لا أرقام فيه إطلاقاً، يرجع إلى PLJSONNumber(Value, 10) التي تكتب null حين تكون القيمة غير منتهية

تقرأ PDFlibPas ‏PDFNumberTextToJSON الإشارة وتجمع الأرقام حول نقطة عشرية واحدة وتطبق التعديلات التي يطلبها JSON وحدها، بينما يرجع أي محرف آخر أو جري أرقام فارغ إلى PLJSONNumber، التي تكتب null لـ NaN و Infinity بدل عدد
إعادة التهجئة تتفوق على إعادة الحساب: كان الـ tokenizer قد رقّع .5 و 4. في طريق الدخول أصلاً، فيبقي الكاتب كل رقم ناجٍ وتعيد الرحلة إنشاء القيمة نفسها تماماً
  • تصبح -.25 ‏-0.25، وتصبح +.5 ‏0.5
  • تصبح +1.5 ‏1.5
  • تصبح 007.5 ‏7.5، بينما تبقى 0.75 كما هي
  • تصبح 4. ‏4 إذا بلغ كهذا الرمز الكاتب يوماً
  • تبقي 2.22221 و 1.250000 كل رقم عشري، والأصفار اللاحقة مشمولة

التنسيق من Double المخزنة كان سيكون أقصر وخاطئاً، للسبب نفسه الذي أنشأ إصلاح الدقة: دقة المخرج الافتراضية أربع منازل عشرية، وحتى تحويل كامل الدقة قد يضيف ضجيجاً ثنائياً إلى literal عشري. والإبقاء على الأرقام يعني أن SetObjectJSON و ImportObjectJSON، اللتين تسلّمان كل نص عدد JSON إلى الـ tokenizer الخاص بـ PDF، تعيدان إنشاء القيمة نفسها تماماً. والضمان يغطي القيمة لا البايتات: بعد استيراد من جديد يخزن -.25 ويحفظ على صورة -0.25. والتهجئتان متساويتان تحت §7.3.3، لكن مقارنة بايتات ستوسم التغيير، فلا تعامل دورة تصدير واستيراد بوصفها no-op على مستند تغطي تواقيع بايتاته

ماذا يحدث لعدد لا يستطيع JSON تمثيله؟

يكتب GetDocumentJSON الآن null لأي عدد NaN أو لانهائي، لأن RFC 8259 §6 لا صيغة له عنده لأي منهما. واللانهاية أسهل إنتاجاً مما تظن: يجمع tokenizer الخاص بـ PDF الأرقام بضرب متكرر في Double تسقف قرب 1.8 × 10308، فعدد صحيح literal يزيد طوله على 300 رقم قليلاً يصير بصمت +Inf. الملفات الصادقة لا تحوي هكذا literal قط؛ والمضبوكة والمعادية تفعل، ولهذا تعود إلى مجموعة الاختبار نفسها مع حالات تحصين محلل PDF بـ Pascal ضد الملفات الخبيثة. وكان كاتب المستندات القديم ينقّ غير الصحيحة بـ Str(D:0:6)، ومع +Inf يكتب ذلك النص +Inf، الذي لن يحلله أي مستهلك JSON

و null هنا فقدان معلومات عن قصد. على مستهلكي مخرج GetDocumentJSON أن يقبلوا null في أي مكان يمكن أن يظهر فيه عدد، وأن يقرؤوه بمعنى «قيمة كانت موجودة ولا يمكن تمثيلها»، لا بمعنى مفتاح مفقود. وliteral الأصلي غير قابل للاسترجاع من JSON المستند، فالمسار الذي يهتم به يسجل الكائن ويعتبر الملف مشبوهاً بدل أن يستبدل افتراضاً

لماذا كان NaN واحداً يقطع تصدير SVG أو JSON؟

لأن PLDoubleToStr، المنسّقة المستقلة عن اللغة خلف تدفقات المحتوى و SVG و XML و CSV ومعظم JSON في المكتبة، كانت تقيس مدخلها وتستدعي Round، و Round(NaN) ترفع EInvalidOp على أهداف مثل Win32 حيث يترك Delphi استثناء العملية غير الصالحة في x87 دون حجب. والاستثناء انطلق بعد أن كان الكاتب قد أطلق جزءاً من مخرجه، فقياس منحل واحد، أو 0/0 في قياس، أو NaN مررت من مستدعٍ، ترك ملفاً مقطوعاً خلفه. تعيد PLDoubleToStr الآن القيمة 0 لـ NaN، وفرعها الصحيح يُحصر عند ±9.2e18 مثل فرعها العشري، فتصير Infinity أيضاً literal منتهياً

الصفر هو الجواب الصحيح لتدفق محتوى، حيث لا بد أن يحمل خانة عدد عدداً، وهو الجواب الخطأ لتقرير، حيث 0 قياس معقول. وكتابات JSON التي يجب أن تحفظ الفرق تستخدم PLJSONNumber(Value, Decimals) من PDFlibExtra، التي تكتب null لـ NaN أو Infinity وأرقاماً مستقلة عن اللغة سواها. و PLJSONNumber تسند الآن GetSimilarImageDeduplicationReportJSON و GetAnnotationHitsJSON وتقارير الباركود والتجبين والنص المهيكل و PDF/VCR؛ وكان تقرير التجبين يكتب 0 لزاوية غير منتهية ويكتب الآن null

يوقف PDFlibPas ‏NaN و Infinity بثلاث طرق: ترفض AddPageMatrix و ScalePage و RedactRegion الوسائط غير المنتهية من البداية، وتكتب PLDoubleToStr القيمة 0 لخانات تدفق المحتوى، وتكتب PLJSONNumber القيمة null في التقارير حيث كان الصفر سيقرأ قياساً معقولاً، بعد أن كانت Round(NaN) ترفع EInvalidOp في منتصف التصدير
الصفر جواب صحيح لتدفق محتوى وجواب خطأ لتقرير، فيسلّم كتّاب التقارير كل Double إلى PLJSONNumber ويتركون null تقول إن القيمة كانت موجودة ولا يمكن تمثيلها
uses
  SysUtils, PDFlibTypes, PDFlibExtra;

function SkewReportJSON(Page: Integer; Angle, Confidence: Double): string;
var
  B: PLStringBuilder;
begin
  B := PLStringBuilder.Create(128);
  try
    // نسّق كل Double إلى نص أولاً؛ تكتب PLJSONNumber ‏null
    // لـ NaN أو Infinity وتستخدم دائماً نقطة عشرية
    B.Append('{"page":').Append(Page)
     .Append(',"angle":').Append(string(PLJSONNumber(Angle, 4)))
     .Append(',"confidence":').Append(string(PLJSONNumber(Confidence, 4)))
     .Append('}');
    // لا تستخدم أبداً B.Append(Angle): فالتحميل Double يتبع لغة المستخدم
    Result := B.ToString;
  finally
    B.Free;
  end;
end;

أين ما زالت لغة المستخدم تتسلل إلى JSON؟

عبر أي منسّقة تستشير الإعدادات الإقليمية، وتدقيق كامل للمخرج المقروء آلياً وجد واحداً متبقياً بالضبط: ‏maxAcceptedMeanError في GetSimilarImageDeduplicationReportJSON، مكتوبة بـ PLFloatToStr، غلاف رقيق فوق FloatToStr. وعلى سطح مكتب فاصله العشري فاصلة احتوى التقرير "maxAcceptedMeanError":1,5، الذي يقرؤه محلل JSON بوصفه القيمة 1 يليها رمز طائش. والخبرة تبلّغ عن أسوأ خطأ بكسل مقبول من إزالة تكرار الصور الإدراكية، وهي الآن تمر عبر PLJSONNumber(Stats.MaxAcceptedMeanError, 6). ولفخ متبقٍ هو PLStringBuilder: على Delphi هي اسم بسيط لـ System.SysUtils.TStringBuilder، التي تنقّ تحميلتها Append(Double) عبر لغة المستخدم، بينما تستخدم بناءات FPC صنف المكتبة بدلاً منها، فلن يلتقطها اختبار على Free Pascal أو جهاز en-US أبداً

uses
  System.SysUtils, System.JSON, PDFlibrary;

var
  Lib: TPDFlib;
  Report: WideString;
  Parsed: TJSONValue;
begin
  // استنسخ سطح مكتب ألماني أو فرنسي داخل تشغيل الاختبار
  FormatSettings.DecimalSeparator := ',';
  Lib := TPDFlib.Create;
  try
    // استخدم ملف اختبار يحوي فعلاً صوراً شبه مكررة،
    // وإلا كان متوسط الخطأ 0 وبقي العيب مخفياً
    Lib.LoadFromFile('scanned-batch.pdf', '');
    // تشغيل جاف بحدود 2 و 2 و 4: المستند لا يُعدل
    Lib.GetSimilarImageDeduplicationReportJSON(2, 2, 4, Report);
    Parsed := TJSONObject.ParseJSONValue(Report);
    if Parsed = nil then
      raise Exception.Create('report is not valid JSON on a comma locale');
    Parsed.Free;
  finally
    Lib.Free;
  end;
end;

تحتاج حزمة انحدار لمخرج JSON إلى ثلاث ملفات اختبار لتبقى أمينة: صفحة تحمل -.25 و +1.5 و 007.5، وكائن يحمل عدداً صحيحاً من 400 رقم، وأي تقرير يشتغل تحت لغة فاصلة، كل منها متحقق منه بمحلل صارم لا بالعين المجردة. و JSON الكائنات و JSON المستندات والتقارير التحليلية في PDF Library for Delphi تتشارك قواعد الأعداد نفسها عبر Delphi و C++Builder و Free Pascal؛ والقائمة الكاملة للخصائص على صفحة منتج PDF Library for Delphi