مقال تقني

الحفاظ على دقة الأعداد العشرية المحللة عند الحفظ في Delphi

يحتفظ PDFlibPas، مكتبة losLab PDF Developer Library، بالنص العشري الدقيق الذي حلله لكل عدد حقيقي في المستند ويعيد كتابة ذلك النص حرفياً كلما كانت القيمة لم تعدَّل قط. منذ v3.539.19 يتحكم إعداد SetPrecision بالأعداد التي تنشئها المكتبة أو تحررها وحدها، فلن يقرّب حفظ عادي وتحميل /Gamma لـ CalRGB من 2.22221 إلى 2.2222 فيزيح ألوان صفحة لم يلمسها أحد. التغيير صغير في الكود وكبير فيما يقوله عن المحللات: القيمة التي تفككها والنص الحرفي الذي تصدره شيئان مختلفان، وجولة ذهاب وإياب عبر Double ليست تحويلة محافظة على الهوية

لماذا زاحم حفظ لم يغيّر شيئاً ألوان الصفحة؟

لأن وسائط فضاء اللون كان يعاد تنسيقها، لا الصورة. الملف الذي كشف هذا مستند مكتبي من 35 صفحة في مدونة الانحدار المحلية، بصورة ترويسة معاد استخدامها في كل صفحة. حمّله واحفظه مباشرة أنتجت تدفقات صور مطابقة بايتاً ببايت للمدخل، وبلّغت مقارنة بصمات التدفقات أن المستند لم يتغير. خالفت المقارنة المصوّرة: كل واحدة من الصفحات الـ 35 أظهرت فروق بكسل في الترويسة، ولا في مكان آخر

ترسم صورة الترويسة عبر فضاء ألوان CalRGB، الذي يعرّفه ISO 32000-1 §8.6.5.3 بـ /WhitePoint ومصفوفة /Gamma اختيارية من ثلاثة عناصر ومصفوفة /Matrix اختيارية من تسعة عناصر. تلك المصفوفات كائنات عددية عادية في قاموس فضاء اللون. خزّنت TPDFNumeric كل واحدة منها كـ Double ولا شيء غير ذلك، وصاغت TPDFNumeric.Output ذلك الـ Double عبر PDFPrecNum الذي يأتي افتراضياً بأربعة مواضع عشرية. فذهبت /Gamma من 2.22221 إلى 2.2222، وذهب إدخال مصفوفة من 0.71519 إلى 0.7152، وأنتج العارض بأمانة ألواناً مختلفة قليلاً من معايرة مختلفة قليلاً. بايتات الصورة كانت بريئة؛ الأعداد حولها لم تكن كذلك. والجزء المقلق هو مدى خفاء ذلك. مقارنة بايتات التدفق المفكوك لا تراه، لأن الأعداد تعيش في قاموس لا في تدفق. ومقارنة حمولات المرفقات لا تراه. وحتى فرق المراجعات الموصوف في مقالة مستويات التعديل يبصم جسم كائن منطبَع، فتُبصم كلتا المراجعتين إلى القيمة نفسها ويبلّغ الفرق عنهما كمتطابقتين. لم يمسكه إلا العرض، ولهذا تصرّ أساس مدونة المستندات كل صفحة بدل الاكتفاء بالفحوص البنيوية

أين أفقد PDFlibPas دقة CalRGB عند حفظ بلا عمل في Delphi: تعيش /Gamma المحللة 2.22221 وإدخال مصفوفة 0.71519 في TPDFNumeric كـ Double، وتصوغهما Output عبر PLDoubleToStr مع PDFPrecNum عند أربعة مواضع عشرية، وتبلّغ كل فحوص بنيوية أن المستند لم يتغير، وتظهر المقارنة المصوّرة وحدها إزاحة كل صور الترويسات الـ 35
بايتات الصورة كانت بريئة: أعادت TPDFNumeric تنسيق أعداد المعايرة حولها عبر PDFPrecNum، فبلغت بصمات التدفقات وفرق البصمة معاً أن المراجعتين متطابقتان بينما أنتج العارض ألواناً مختلفة قليلاً على كل صفحة

القيمة التي حللتها ليست الحرفية التي ينبغي أن تكتب

العدد الحقيقي في PDF سلسلة عشرية، و ISO 32000-1 §7.3.3 صريح في أنها سلسلة عشرية فحسب: لا تدوين جذري ولا صيغة أسّية. ثم يسرد الملحق C الدقة المتوقع أن تحترمها التنفيذات، نحو خمسة أرقام عشرية معنوية في الجزء الكسري. ودقة إخراج افتراضية من أربعة تحت ذلك أصلاً، ويزداد الأمر سوءاً قرب الصفر: تحجّم PLDoubleToStr القيمة وتقرّبها إلى عدد صحيح وتصدر 0 حين يكون الناتج صفراً، فإدخال مصفوفة مثل -0.000012345 لا يفقد رقماً، بل يختفي كلياً

رفع الافتراضي لا يفعل شيئاً سوى تحريك الهاوية. الإصلاح هو التوقف عن التظاهر بأن Double هو العدد. عندما يتعرف المميّز في TPDFStructure.Decode على حقيقي قياسي، أي أن الرمز يحوي نقطة عشرية ولا يحمل مؤشر أس، يخزّن النص المصدر في حقل FOriginalText الجديد إلى جانب القيمة المحوّلة. ثم تفضّل Output ذلك النص ولا تعود إلى التنسيق إلا إذا لم يوجد ما يُفضَّل

كيف يحفظ PDFlibPas النص العشري المحلل في Delphi: يبقي المميّز في TPDFStructure.Decode النص الحرفي المصدر في FOriginalText لأي رمز فيه نقطة عشرية وبلا أس، وتكتب Output ذلك النص حرفياً بدل استدعاء PLDoubleToStr، ويمسح SetTo إياه لأن الرقم المعدَّل رقم جديد
القيمة التي تفككها والنص الحرفي الذي تصدره شيئان مختلفان: تفضيل النص المحلل يبقي 2.22221 دقيقاً، بينما تتبع الأعداد المنشأة والمعدَّلة بالمكتبة PDFPrecNum ولا يصل الإعداد إلى مدخل لم يُمسّ
// Lib/PDFlibStruct.pas — الإصلاح كله في جهة الإخراج
Function TPDFNumeric.Output: AnsiString;
Begin
  If FOriginalText<> '' Then
    Result:= FOriginalText
  Else
    Result:= PLDoubleToStr(FValue, Owner.PDFPrecNum);
End;

Procedure TPDFNumeric.SetTo(Const Value: Double);
Begin
  FOriginalText:= '';   // الرقم المعدَّل رقم جديد
  FValue:= Value;
  FChanged:= True;
End;

حدّان مقصودان. الأعداد الصحيحة لا تُحفظ، لأن تنسيق الأعداد الصحيحة بلا خسارة أصلاً. وصيغ الأس مثل 6.02E23 تتسامح معها في المدخل من أجل منتجين معيبين لكنها لا تُحفظ في المخرج، لأن إعادة كتابتها تُخلّد صياغة يمنعها §7.3.3؛ فهي تمر بالمنسّق مثل أي عدد ولّدته المكتبة. كما يطبق المميّز إصلاحه الأدنى المعتاد قبل تخزين النص، فيبقى الحرفي النقطي الابتدائي مثل .5 كـ 0.5 والحرفي النقطي الختامي مثل 5. كـ 5.0. وكلاهما العدد نفسه عند كل قارئ ويقبله أوسع بكثير

ماذا يضمن SetPrecision بعد v3.539.19؟

يتحكم TPDFlib.SetPrecision الآن في المواضع العشرية للأعداد التي تنتجها المكتبة نفسها: القيم المسحوبة عبر الرسّام، والأعداد المنشأة من Double كما عبر NewNumeric، وأي قيمة محللة عُدّلت لاحقاً بـ SetTo. ولاحظ أن النص المفكوك عبر واجهة الكائنات، مثل حرفي ممرر إلى SetObjectFromString، يمر بالمميّز نفسه ويُحفظ بالطريقة نفسها. العدد العشري المحلل الذي لم يُعدَّل يبقي دقة مدخله أياً كان الإعداد، وتغيير الإعداد بعد التحميل لا يلمسه بأثر رجعي. حُدّث إدخال مرجع SetPrecision في الإصدار نفسه ليقول هذا بالضبط، لأن الصياغة القديمة توحي بأن الإعداد ينطبق على كل عدد في الملف

والمسح يجري في SetTo لا يُشتق من علم Changed، وهذا التمييز مهم. يعيد خط أنابيب الحفظ تصفير Changed على الكائنات بعد كتابتها، ففحص من النمط "أصدر النص الأصلي إلا إذا تغير" سيبدأ بإصدار نص قديم لقيمة عُدّلت ثم حُفظت ثم عُدّلت ثانية في نفس الجلسة. ربط النص الأصلي بالإسناد نفسه يجعل اختلاف الاثنين مستحيلاً. ويثبّت اختبار الانحدار كل سلوك من هذه السلوكيات بالقيم من الملف الأصلي

uses
  PDFlibStruct;

var
  Structure: TPDFStructure;
  Values: TPDFArray;
  Number: TPDFNumeric;
begin
  Structure := TPDFStructure.Create;
  try
    Structure.PDFPrecNum := 4;
    Values := TPDFArray(Structure.Decode('[2.22221 0.71519 -0.000012345 1 0.12567]'));
    // مدخل بلا تحرير ينجو حرفياً، بما في ذلك القيمة التي
    // كان التنسيق بأربعة مواضع سينهار إلى 0
    Assert(Values.Output = '[ 2.22221 0.71519 -0.000012345 1 0.12567 ]');

    // تعديل يرمي النص الأصلي ويتبع PDFPrecNum
    Number := TPDFNumeric(Values.Item[0]);
    Number.SetTo(0.123456);
    Assert(Number.Output = '0.1235');
    Assert(Structure.NewNumeric(0.123456).Output = '0.1235');

    // خفض الدقة لاحقاً لا يبلغ المدخل غير المعدَّل
    Structure.PDFPrecNum := 2;
    Assert(TPDFNumeric(Values.Item[1]).Output = '0.71519');
  finally
    Structure.Free;
  end;
end;

لماذا ما زال نموذج المحتوى يطبع الأعداد؟

لأن TPDFContentProgram يَعِد بمعاملات عددية قياسية، وتلك الوعدة أثمن من النص الحرفي داخل تدفق محتوى. نموذج المحتوى القابل للتحرير، وهو نفسه الذي بُني عليه متتبع حالة الرسوميات، موجود حتى ينتج NormalizeContentStreams والمُحسِّن و Emit مخرجاً مستقراً قابلاً للمقارنة من مدخل عشوائي. لو حمل معامل محلل نصه الأصلي معه إلى النموذج، لصدر تتابع معاملات مثل 0.50000 0 0 RG بشكل مختلف عن 0.5 0 0 RG، وانجرفت كل مقارنة هابطة مع عادات تنسيق المنتج

لهذا يجرد النموذج النص الأصلي عند مدخليه اثنين. تجري NormalizeContentNumbers على كل معامل عندما يدفعه المحلل وثانية داخل SetOperand حين يُفك مصدر قادم من المستدعي، وتتكرر داخل المصفوفات والقواميس حتى تغطى أنماط الشرط ومصفوفات TJ وقواميس خصائص المحتوى الموسوم. استدعاء SetTo(AsDouble) على كل عدد يكفي، لأنه بالضبط العملية التي تمسح النص. أما بيانات الصور المضمّنة الخام فتُترك على حالها، كما كانت دائماً

لماذا ما زال نموذج المحتوى في PDFlibPas يطبع الأعداد: تجري NormalizeContentNumbers حيث يدفع المحلل كل معامل وثانية داخل SetOperand، وتتكرر داخل المصفوفات والقواميس فتغطى أنماط الشرط ومصفوفات TJ وقواميس خصائص المحتوى الموسوم، ويمسح SetTo بـ AsDouble النص الأصلي فيصدر 0.50000 و 0.5 بالمثل
المعاملات العددية القياسية هي وعدة نموذج المحتوى: تُترك بيانات الصور المضمّنة الخام على حالها، وتُبقي الأعداد القاموسية غير الملموسة خارج تدفقات المحتوى الضمانة الحرفية، فما زال زوج LoadFromFile و SaveToFile العادي يحفظها
// Lib/PDFlibContentModel.pas — نموذج المحتوى يفي بعقده
Procedure NormalizeContentNumbers(Obj: TPDFObject);
Var
  K: Integer;
Begin
  If Obj is TPDFNumeric Then
    TPDFNumeric(Obj).SetTo(TPDFNumeric(Obj).AsDouble)
  Else If Obj is TPDFArray Then
    For K:= 0 To TPDFArray(Obj).Count- 1 Do
      NormalizeContentNumbers(TPDFArray(Obj).Item[K])
  Else If Obj is TPDFDictionary Then
    For K:= 0 To TPDFDictionary(Obj).Count- 1 Do
      NormalizeContentNumbers(TPDFDictionary(Obj).Entry[K].Value);
End;

القاعدة العملية للمستدعين بسيطة إذن. LoadFromFile عادية يتبعها SaveToFile تُبقي تدفقات المحتوى غير الملموسة والأعداد القاموسية غير الملموسة كما كانت. والصفحة التي تمر عبر NormalizeContentStreams، أو أي تحرير عبر نموذج المحتوى، تخرج قياسية بالتصميم، وبقية المستند ما زال محفوظاً. هذان طلبان مختلفان، وهما الآن يفعلان شيئين مختلفين

ماذا يكلف، وأين تتوقف الضمانة؟

يحمل كل TPDFNumeric الآن مرجع AnsiString إضافياً واحداً، ويُبقي كل عدد عشري محلل نصه المصدر حياً على مدى عمر الكائن. في مستند بملايين الأعداد الحقيقية تلك ذاكرة حقيقية، ويجب إدراجها في أي قياس للمستندات الكبيرة لا الرف عنها بيدين. والضمانة محدودة النطاق بمستند العدد نفسه: نسخ الكائنات بين المستندات أو إعادة بناء القيم عبر واجهة الكائنات ينتج أعداداً جديدة تتبع دقة الإخراج مثل أي عدد جديد. ويستحق أن نكون دقيقين فيما يدّعيه الإصدار وما لا يدّعيه. حفظ مستند غير ملموس وتحميله يحفظ الآن أعداد المعايرة التي يستهلكها العارض فعلاً، وهي الخاصية التي يفحصها أساس المدونة. ولا يدّعي مخرجاً متطابقاً بايتاً ببايت، فذلك يعتمد أيضاً على ترقيم الكائنات وضغط التدفقات ومعرف المقطع الختامي المذكور في مقالة معرف PDF الحتمي. ولا يجعل فرق البصمة يرى فروق التقريب في ملفات أنتجها برمجيا آخرون، لأنها ما زالت تبصم الجسم المنطبَع. والدرس يتعمم إلى ما وراء CalRGB بكثير: حين يحتفظ محلل بالقيمة المحوّلة وحدها يصير كل حفظ تحريراً، والطريق الوحيد للانتباه هو النظر إلى النتيجة المصوّرة. معالجة الأعداد ودلالات SetPrecision موثقة على صفحة منتج losLab PDF Developer Library