يخزن HotPDF Delphi Component كل عدد PDF في THPDFNumericObject.Value المعرف بوصفه Single، و Single لا يستطيع الاحتفاظ بكل عدد صحيح من 32 بتاً. أعلام حقول AcroForm (/Ff) قناع من 32 بتاً، فأي قناع ببت عالٍ مفعّل كان يُقرَّب عند التحميل: عادت $8000FFFF بوصفها $80010000. ومنذ v2.989.5 تحلل قراءات أعلام الحقول رمز العدد الصحيح الأصلي تحليلاً دقيقاً وتنسخ بتاته إلى الـ API الحالي من نوع Integer
يستحق الخطأ نظرة مقربة حتى لو لم تمس نماذج PDF قط. لم ينهار شيء ولم يُرفع استثناء، ونجحت معظم ملفات الاختبار، لأن معظم الأقنعة الحقيقية صغيرة. ولم يظهر الفشل إلا حين اجتمع بت عالٍ مع الأعلام الشائعة الدنيا، وحينها لم يفقده بتاً واحداً بل أعد كتابة عدة بتات دفعة واحدة. تخزين قناع بتات في نوع عشوائي فخ ديلفي كلاسيكي، وهذا شكله في كود إنتاج
لماذا يفقد Single بتات أعلام حقول AcroForm؟
للـ Single في ديلفي دالة significand من 24 بتاً، فيمثل كل عدد صحيح حتى 2^24 (16777216) بدقة ولا يمثل شيئاً بعدها دون فجوات. بين 2^31 و 2^32 تتتباعد قيم Single المتتالية بمقدار 256. وقناع أعلام حقل ببت 32 مفعّل يسكن ذلك المدى تماماً، فبتاته الثمانية الدنيا، وغالباً أكثر، تُقرَّب إلى أقرب مضاعف للـ 256
يعرف ISO 32000-1 §7.3.3 كائنات الأعداد الصحيحة والحقيقية نوعين منفصلين من الأعداد، ويسرد الملحق C حدود تنفيذ مختلفة لكل منهما، فلا التزام على عدد PDF الصحيح أن يتسع في float من 32 بتاً. ومدخل أعلام الحقل معرف في الجدول 221 كعدد صحيح، ويقرأ بتاً ببت لا بوصفه كمية. تقريب كمية بكسر ضئيل غير ضار؛ أما تقريب قناع فينتقل أثره إلى البتات المجاورة. ويمكنك إعادة إنتاج الأثر كاملاً في أسطر قليلة من ديلفي الصافي:
var
S: Single;
Mask: Int64;
begin
S := $8000FFFF; // 2147549183، لا يمكن تمثيلها كـ Single
Mask := Round(S); // 2147549184
Writeln(IntToHex(Mask, 8)); // 80010000: مسحت البتات 1-16 وفعّل البت 17
S := $80003FFF;
Mask := Round(S);
Writeln(IntToHex(Mask, 8)); // 80004000: مسحت البتات 1-14 وفعّل البت 15
end;
يتعمم الدرس خارج PDF. Single أو Double أو Variant يحمل عدداً عشوائياً أو عدد JSON يُفك ترميزه إلى float، كلها وعاء خاطئ لحقل بتات، حتى عندما تكون القيم التي تصادف اختبارها صغيرة. يدفع Double المدى الدقيق إلى 2^53، فيخفي المشكلة عن الأقنعة ذات 32 بتاً لكنه يترك النوع يعبر عن قصد خاطئ مع ذلك. استخدم Cardinal أو Integer أو Int64 وحوّل من النص المصدر لا من float جرى تقريبه سلفاً
أي أعلام الحقول تنقلب: Radio و Pushbutton و Password و Multiline
القناعان أعلاه ليسا اعتباطيين؛ يريان كيف تغير خطوة تقريب واحدة نوع الحقل الذي تراه العارضة. ترقم PDF بتات الأعلام من 1، فالبت n قيمته 2^(n-1). يعرف الجدول 226 في ISO 32000-1 §12.7.3.1 البتات من 1 إلى 3 لكل حقل، ويعرف الجدول 228 أعلام الأزرار، ويعرف الجدول 230 أعلام حقول النص
$8000FFFFإلى$80010000: يتصفّر البت 16 (Radio، $8000، الجدول 228) وينفعّل البت 17 (Pushbutton، $10000)، فيتحول زر اختيار إلى زر دفع- يمسح التقريب نفسه البتات من 1 إلى 3، فتختفي ReadOnly و Required و NoExport من الجدول 226 مع Radio
$80003FFFإلى$80004000: يتصفّر معاً البت 13 (Multiline، $1000) والبت 14 (Password، $2000) من الجدول 230، فيتحول صندوق كلمة مرور مقنع إلى حقل نص عادي من سطر واحد- دون البت 32 يبدأ الضرر عند 2^24:
16777219 (Comb مع ReadOnly و Required) يقرَّب إلى16777220، فيسقط ReadOnly و Required ويضيف NoExport
الحالة الأخيرة كانت مهمة لـ HotPDF خارج تحرير النماذج. يبني ExportLoadedPagesStructured نموذج JSON و XML للنماذج من القناع الفعلي ويستخدم NoExport في قرار كتم القيم، فيمكن لقناع مقرب أن يخفي قيمة عادية أو يبلّغ عن العضوين readOnly و required خطأً. وتلقت مسار التصدير المهيكل القراءة الدقيقة نفسها في v2.989.8
كيف يقرأ HotPDF رمز العدد الصحيح الدقيق ذا 32 بتاً
يصلح HotPDF Delphi Component جانب القراءة دون تغيير نموذج الكائنات العددية. كان المحلل يتذكر نص الرمز الأصلي بجوار قيمة Single أصلاً، فيحلل قارئ أعلام الحقول ذلك الرمز بوصفه عدداً صحيحاً، ويجمعه في Int64 محدود، وينسخ البتات الـ 32 الناتجة إلى Integer الذي تعيده الـ API العامة. تبقى الـ API العددية العامة والمسار البديل القديم لغير الصحيحات كما هي، ولا يسلك المسار الدقيق سوى مستهلكي أعلام الحقول
على المحلل أن يقبل الصيغتين اللتين تكتب بهما الملفات الحقيقية قناعاً ببت عالٍ. يبث بعض الكتّاب القيمة غير الموقعة 2147549183، وهي فوق حد الملحق C للأعداد الصحيحة عند 2^31 - 1 لكنها شائعة عملياً؛ ويبث آخرون القيمة الموقعة -2147418113. وكلاهما يصف البتات نفسها. إحدى نسخ المحلل المحدود تبدو هكذا:
function TryParseFlagMask(const Token: AnsiString; out Mask: Cardinal): Boolean;
var
P, Digit: Integer;
Negative: Boolean;
Number, Limit: Int64;
begin
Result := False;
Mask := 0;
if Token = '' then Exit;
P := 1;
Negative := Token[1] = '-';
if Token[1] in ['+', '-'] then Inc(P);
if P > Length(Token) then Exit;
// رموز موقعة حتى -2^31 ورموز غير موقعة حتى 2^32 - 1
if Negative then Limit := 2147483648 else Limit := 4294967295;
Number := 0;
while P <= Length(Token) do
begin
if not (Token[P] in ['0'..'9']) then Exit; // الحقيقيات تسلك المسار البديل القديم
Digit := Ord(Token[P]) - Ord('0');
if Number > (Limit - Digit) div 10 then Exit; // محدود بلا فيض
Number := Number * 10 + Digit;
Inc(P);
end;
if Negative and (Number <> 0) then Number := 4294967296 - Number;
Mask := Cardinal(Number);
Result := True;
end;
تفصيلان مقصودان. يفحص الحد قبل كل عملية ضرب، فيفشل رمز من 400 خانة فشلاً نظيفاً بدل فيض Int64. ويرفض رمز مثل 3.0، فيُعاد بذلك المستدعي إلى Round(Value)؛ فالمسار الدقيق يحسن الأقنعة الصحيحة دون اختراع دلالات جديدة للمشوهة منها
من كود التطبيق لا يتغير شيء في التواقيع. تعيد THotPDF.GetLoadedFormFieldFlags(FieldIndex: Integer): Integer القناع الفعلي، سائرة سلسلة /Parent بالطريقة نفسها الموصوفة في كيفية حل HotPDF لقيم حقول AcroForm الموروثة وإعادة تعيينها، وتكتب THotPDF.SetLoadedFormFieldFlags(FieldIndex, AFlags: Integer) قناعاً محلياً كاملاً. القناع ذو البت 32 مفعّل هو ببساطة Integer سالب، فانقل البتات بدل تحويل القيمة:
const
FF_RADIO = $00008000; // البت 16، ISO 32000-1 Table 228
FF_PUSHBUTTON = $00010000; // البت 17، Table 228
var
Pdf: THotPDF;
I, Native: Integer;
Bits: Cardinal;
begin
Pdf := THotPDF.Create(nil);
try
if Pdf.LoadFromFile('survey.pdf') <= 0 then Exit;
for I := 0 to Pdf.GetFormFieldCount - 1 do
begin
Native := Pdf.GetLoadedFormFieldFlags(I);
Move(Native, Bits, SizeOf(Bits)); // البتات 32 نفسها بلا تحويل
if (Bits and FF_RADIO) <> 0 then
Writeln(Pdf.GetFormFieldName(I), ': radio, mask $', IntToHex(Bits, 8));
end;
Bits := $80000002; // البت العالي مع Required
Move(Bits, Native, SizeOf(Native)); // Native = -2147483646
Pdf.SetLoadedFormFieldFlags(0, Native);
Pdf.SaveLoadedDocument('survey-out.pdf');
finally
Pdf.Free;
end;
end;
قراءة قناع موروث تترك قاموس الابن لم يمسّه؛ والكتابة تستبدل القناع كله بمدخل محلي على الحقل المحدد. وتبدأ الاختصاران SetLoadedFormFieldReadOnly و SetLoadedFormFieldRequired من القناع الفعلي الموروث، ويعكسان بتاً واحداً، ويحفظان كل بت آخر دقيقاً. ولملء القيم لا الأعلام انظر ضبط قيم حقول النماذج في ملف PDF محمّل بواسطة HotPDF
لماذا يهم ترتيب Value و RememberSourceToken؟
الترتيب مهم لأن THPDFNumericObject لا يثق برمزه المتذكر إلا ما دامت قيمة Single غير متغيرة. يخزن RememberSourceToken الرمز مع لقطة من Value الحالية، ويعيد جالب SourceToken سلسلة فارغة بمجرد أن تختلف Value عن تلك اللقطة. وقاعدة الإبطال تلك هي ما يمنع عدداً محرراً من الحفظ بنصه الأصلي القديم
تتحول القاعدة إلى فخ في جانب الكتابة. تذكّر الرمز أولاً وأسند القيمة ثانياً، تحمل اللقطة القيمة القديمة، ويبلّغ الجالب بعدم وجود رمز، ويعود المسلسل إلى تنسيق Single المقرب. وكل كاتب أعلام داخل المكتبة، بمن فيهم الاختصارات ولوالِد الحقول الأصلية، يتبع التسلسل نفسه:
// صحيح: تطابق اللقطة قيمة Value فينجو الرمز الدقيق عند الحفظ
Item := THPDFNumericObject.Create(nil);
Item.Value := AFlags;
Item.RememberSourceToken(AnsiString(IntToStr(AFlags)));
// خطأ: تُؤخذ اللقطة قبل تغير Value، فيصبح SourceToken
// لاغياً ويُكتب Single المقرب بدلاً منه
Item := THPDFNumericObject.Create(nil);
Item.RememberSourceToken(AnsiString(IntToStr(AFlags)));
Item.Value := AFlags;
لأن SaveNumericObject يكتب الرمز المتذكر كلما وجد رمز صالح، فالحصول على الترتيب الصحيح هو ما يجعل ملفاً محفوظاً ومفتوحاً مجدداً يحمل العدد الصحيح الأصلي لا تقريباً مقرباً منه
رمز null الذي جعل حقلين يتقاسمان اسماً واحداً
أصلح الإصدار نفسه عيباً ثانياً في المحلل اكتشف أثناء اختبار تلك الأقنعة: قيمة null في قاموس مقروء من التدفق لم تكن تُستهلك أبداً. يلقي HotPDF نظرة على الرمز التالي ليصنفه، ويفحص ذلك موضع التدفق عند بداية الرمز. وفرع otNull في قارئ القواميس الانسيابي كان ينشئ الكائن الفارغ لكنه لا يتقدم عبر البايتات الأربع للـ null
لم تكن النتيجة تعليقاً بل قراءة خاطئة صامتة. مسح البحث عن المفتاح التالي إلى الأمام من المكان الخطأ، وتجاوز نهاية القاموس الحالي، وقرأ مفاتيح من الكائن غير المباشر التالي، الذي طغى عندئذ على الحقل الحالي. وفي قائمة الحقول كان العرَض حقلان مختلفان يبلّغان عن الاسم نفسه والقيمة نفسها. وكان محللا المصفوفات والنطاقات يستهلكان null صحيحاً أصلاً، فالإصلاح يقدم التدفق أربعة بايتات في ذلك الفرع الواحد بدل إعادة كتابة المحلل العام. المؤشر الذي لا يتحرك هو من عائلة الخطأ نفسها في تعلقات مؤشر محلل PDF التي علقت HotPDF على مدخلات مشوهة، وإن بعُرض مختلف
وضح الإصلاح الميراث أيضاً. يُعامل /Ff null محلي، أو مرجع يحل إلى null، معاملة الغائب ويتابع البحث إلى الأب، بينما الصفر الصريح 0 قناع حقيقي يطغى على الأب
ما يثبته توجيه {$R-} وما لا يثبته
كانت الفرضية الأولى لخطأ الأعلام خطأ فحص مدى عند إسناد قناع كبير إلى Integer. وكانت خاطئة، والسبب تفصيل ديلفي يستحق المعرفة: توجيه {$R-} محلي وضع قبل ParseSimpleDictionary في HPDFDoc.pas بلا استعادة مقابلة، فأُصرّفت كل دالة بعده في الوحدة وفحص المدى مطفأ. لا يعيد ديلفي ضبط توجيه مفتاح في نهاية دالة، ويرث ملف تضمين حالة التوجيه عند النقطة التي يُضمَّن عندها
لذا فإن -$R+ أو -Cr من سطر الأوامر لا يثبت سوى الإعداد الابتدائي للوحدة. لا يقول شيئاً عن دالة معينة في الأسفل. ولتعرف ما إذا كان فحص المدى فعالاً لروتين معين، اعثر على أقرب توجيه فوقه، ثم أكد السلوك بقيمة حدية حقيقية. لم يرفع مستهلكا الأعلام المحليان الأقدمان استثناء المدى الذي تنبأت به الفرضية أبداً، فذلك الاستثناء الغائب لم يمكن الاستشهاد به دليل إصلاح. والعيب الحقيقي كان تقريب Single عبر بتات أعلام معروفة
القراءة الدقيقة للرمز صادقة في نطاقها أيضاً. لا تغير حالة التوجيه على مستوى الوحدة، ولا تسند معنى للبتات المحجوزة، ولا تدّعي أن كل تركيبة بتات حقل صالح. تضمن أن بتات الملف الـ 32 هي نفسها البتات الـ 32 التي يراها كودك
FAQ
لماذا لا تغيّر THPDFNumericObject.Value إلى Double أو Int64؟
تغيير نوع حقل عام يكسر كل مستدعي يقرؤه أو يسنده، وأعداد PDF حقيقية بقدر ما هي صحيحة. الإبقاء على Single وقراءة أعلام الحقول من الرمز الأصلي يصلح الخطأ للأقنعة البتية دون تغيير أي تعريف حالي أو اصطلاح استدعاء
هل تقبل SetLoadedFormFieldFlags أقنعة ببت 32 مفعّل؟
نعم. مرر القناع بوصفه Integer موقعة؛ فـ $80000002 و -2147483646 البتات الـ 32 نفسها. ويعيد GetLoadedFormFieldFlags القيمة نفسها بعد الحفظ وإعادة التحميل، بما في ذلك Required
أي إصدار من HotPDF يقرأ أعلام حقول AcroForm بدقة؟
جاءت قراءات أعلام الحقول الدقيقة وإصلاح رمز null في HotPDF v2.989.5، وتبنى التصدير المهيكل إلى JSON و XML القناع الدقيق في v2.989.8. تقرب الإصدارات الأقدم أي قناع فوق 2^24 عبر Single
أعلام حقول AcroForm الدقيقة وبحث الحقول الموروثة وتحرير النماذج المحملة كلها من HotPDF Component لـ Delphi و C++Builder، مكتبة PDF أصلية لإنشاء مستندات PDF وتحميلها وتحريرها