ترقّم HotXLS كل إحالة خط في BIFF8 بالطريقة التي يعرفها [MS-XLS] §2.5.129 FontIndex: القيم من 0 إلى 3 تبدأ من الصفر، والقيم فوق 4 تبدأ من الواحد، والقيمة 4 لا تظهر قط، فيكون سجل FONT الخامس هو ifnt 5 وأكبر ifnt صالح يساوي عدد سجلات FONT. ومنذ HotXLS 2.384.4 يتبع كاتب XF وقارئ XF ومقاطع سلاسل النص المنسق وترحيل المقاطع بين المصنفات تلك القاعدة كلها، وامتدت في 2.384.5 و2.384.6 إلى مقاطع التعليقات وصناديق النص، بما في ذلك عبر النسخ وإدراج الصفوف
تبدو القاعدة وكأنها خطأ مطبعي حتى تصطدم بها. يفتح أحدهم مصنفًا بثمانية سجلات FONT، فيجد XF يشير إلى الخط 8، ويستنتج أن الكاتب أنتج فهرسًا خارج المدى. هذا الاستدلال بالضبط هو ما شُحن في HotXLS 2.384.1 بوصفه «إصلاحًا»، فحوّل تنفيذًا صحيحًا إلى آخر تهبط فيه كل خطوط مخصصة في ملف يفتحه Excel إلى خانة أبكر من موضعها. المثير للاهتمام ليس الإزاحة بمقدار واحد في ذاتها، بل عدد المواضع في مكتبة BIFF8 التي تحمل الاتفاقية نفسها، وكيف يمكن لربط خط أن ينجو من حفظ أول وينكسر في الثاني. إن كنت قد حاربت سلفًا مشكلات الطول والترميز المغطاة في مقالة فك ترميز cch وfHigh في BIFF8 XLUnicodeString، فهذه العائلة نفسها من العيوب: الملف سليم، والحساب ليس كذلك
ماذا تقول قاعدة FontIndex في [MS-XLS] فعلًا؟
تقول §2.5.129 من [MS-XLS] إن FontIndex الأقل من 4 موقع سجل يبدأ من الصفر، وFontIndex الأعلى من 4 موقع سجل يبدأ من الواحد، والقيمة 4 MUST NOT be used أي لا يجوز استخدامها إطلاقًا. يستخدم نوع FontIndex نفسه في سجلات XF ومقاطع تنسيق SST ومقاطع تنسيق TXO، فسوء قراءة واحدة للقاعدة يفسد الثلاثة جميعًا. والدليل سهل الإعادة بملفات كتبها Excel: نسخة SOLVSAMP.XLS المرافقة لـ Office تحوي 19 سجل FONT وأقصى ifnt في XF يساوي 19، ومصنف بـ 43 سجلًا يتوقف عند 43، وملف حفظه Excel 16 بـ 30 سجل FONT يشير بخلاياه بخط Courier New إلى ifnt 22، أي السجل الثاني والعشرين. ولا يحمل أي منها الرقم 4 قط. وإن احتجت إلى تحليل ربط الفهارس بنفسك في أداة تشخيص، فالتحويل دالتان قصيرتان
// [MS-XLS] 2.5.129 FontIndex: من 0 إلى 3 تبدأ من الصفر، وأكثر من 4 تبدأ من الواحد، و4 غير صالحة
function FontIndexToRecordNo(Ifnt: Word): Integer; // رقم سجل FONT يبدأ من الواحد
begin
if Ifnt < 4 then
Result := Ifnt + 1
else if Ifnt > 4 then
Result := Ifnt
else
Result := -1; // يجب ألا تظهر القيمة 4
end;
function RecordNoToFontIndex(RecordNo: Integer): Word;
begin
if RecordNo <= 4 then
Result := RecordNo - 1
else
Result := RecordNo;
end;
داخل HotXLS تسكن القاعدة نفسها في موضعين متناظرين. تأخذ TXLSFontList.GetSaveIndex الموقع القائم على الواحد للخط في القائمة المحالة وتنقص المواضع من 1 إلى 4 وحدها، فيكتب الموقع 5 بوصفه ifnt 5. وتفعل TXLSReader.ParseXF العكس عند التحميل: أي ifnt بقيمة 5 أو أكثر ينقص إلى خانة قائمة خطوط تبدأ من الصفر، وما دونها يبقى كما هو. وتطبق إعادة خرائط المقاطع الغنية في SST وCountRichRunFontRefs التحويل نفسه ifnt >= 5، وهذه هي الفكرة: اتفاقية واحدة، وكل مستهلك يتبعها
// TXLSFontList.GetSaveIndex (جانب الكاتب)
Result := inherited GetSaveIndex(Index); // موقع محال يبدأ من الواحد
if (Result > 0) and (Result < 5) then
Dec(Result); // من 1 إلى 4 تصير 0 إلى 3، وما فوق 5 بلا تغيير
// TXLSReader.ParseXF (جانب القارئ)
fnti := Data.GetWord(0);
if fnti >= 5 then
Dec(fnti); // ifnt 5 هو الخانة 4 في قائمة الخطوط
لماذا أزاح «إصلاح» يبدأ من الصفر كل خط مخصص بمقدار واحد؟
أزاحت إعادة الكتابة القائمة على الصفر في HotXLS 2.384.1 كل خط مخصص لأنها قرأت فهرسًا يبدأ من الواحد على أنه يبدأ من الصفر، ثم عدلت أربعة مواضع استدعاء لتوافق ذلك السيء القراءة: GetSaveIndex وParseXF وإعادة خرائط مقاطع SST وترحيل المقاطع بين المصنفات في Sheets.AddCopy. ظلت دورات HotXLS الذهاب والإياب تبدو سليمة، لأن الكاتب والقارئ اتفقا معًا. لكن Excel لم يتفق. ملف كتبه 2.384.1 وضع أول خط مخصص عند ifnt 4، وهو ما يعامله Excel بوصفه الخط الافتراضي، وكل خط مخصص لاحق هبط سجلًا واحدًا أبكر من موضعه؛ وفتح ملف Excel سار بالاتجاه المعاكس، فارتبط كل خط بسجل متأخر بمقدار واحد
كانت القرينة التي كان ينبغي أن توقف التغيير نائمة في الشيفرة نفسها. لم يلمس CountRichRunFontRefs ولا إعادة خرائط FONTX وFBI للرسوم البيانية ولا قائمة خطوط محرك الأنماط، وظل الجميع يستخدم تخطي 4، فتناقضت المكتبة مع نفسها لحظة هبوط 2.384.1، ولم يخفِ التناقض إلا المصادفة بأن خطوط النص المنسق كانت تُحال عادة من XF ما. حين تظهر اتفاقية واحدة في سبعة مواضع وتعدل أربعة منها، فاشتبه في تعديلك قبل أن تشتبه في الثلاثة الباقية. أعاد الإصدار 2.384.4 ترقيم المواصفة في المواضع الأربعة كلها، واستُبدل اختبار الانحدار القديم، الذي كان يتحقق من ifnt < FontCount وبذلك كوّد السيء القراءة، باختبارات تحول كل ifnt مكتوب إلى اسم سجل FONT عبر صيغة المواصفة. بقي قيد صادق واحد: الملفات المحفوظة بـ 2.384.1 حتى 2.384.3 بخمسة خطوط أو أكثر تحمل فهارس مزاحة لا يستطيع قارئ أن يميزها عن بيانات صالحة، فالعلاج الوحيد هو إعادة توليدها
لماذا تنكسر مقاطع خطوط التعليقات في الحفظ الثاني فقط؟
انكسرت مقاطع التعليقات وصناديق النص في الحفظ الثاني لأن HotXLS كان يبقي أول N-1 سجل FONT بلا شرط ويُسقط الأخير فقط حين لا تحيل إليه أي XF، بينما كانت مقاطع تنسيق TXO ([MS-XLS] §2.4.329) تعاد كتابة بايتًا ببايت دون إعادة ترقيم. ملفات .xls من صنع Excel تنتهي دائمًا بخط ختامي بلا إحالات (خط DengXian بحجم 9pt على نظام بإعدادات صينية)، فلن يكون الخط الذي تستخدمه مقاطع التعليق وحدها أخيرًا في الحفظ الأول، ولا يتحرك شيء بشكل مرئي. لكن ذلك الحفظ الأول أُسقط فيه الخط الختامي وروّج خط التعليقات الوحيد إلى الموضع الأخير. ثم تخلص الحفظ الثاني منه بوصفه غير محال، فأشار ifnt الخاص بالمقطع إلى ما بعد النهاية، وارتد Excel إلى الخط الافتراضي؛ وإن كان المصنف قد اكتسب خطًا جديدًا في الأثناء، ارتبط المقطع بصمت به بدلًا منه، وهو ما حوّل في الاختبار مقطع صندوق نص منسقًا إلى Arial. الملفات الغنية بالتعليقات مثل الموصوفة في بناء مسار عمل للتعليقات والروابط الفائقة هي بالضبط حيث يعض هذا العيب، لأنها تفتح وتعلق وتحفظ مرارًا
يعامل HotXLS 2.384.5 مقاطع TXO معاملة مقاطع SST. يجوب CountRichRunFontRefs الآن كل TMSOShapeTextBox في كل ورقة عمل، ويحول ifnt الخاص بكل مقطع من ترقيم تخطي 4 إلى خانة، ويعده إحالة، فينجو الخط الخاص بالمقاطع وحدها من مرشح الحفظ. ويذهب جدول الخانة إلى فهرس الحفظ الناتج إلى FontRunRemap لكل رسم، فيعيد TMSOShapeTextBox.Store كتابة فهارس المقاطع على نسخة خاصة من بايتات المقاطع الخام، ويترك TxOLastRun الختامي وشأنه لأنه لا يحمل خطًا. أما عقد الاستخدام من كود التطبيق فبسيط: تستخدم TXLSComment.TextRuns.FontIndex وTXLSTextBox.TextRuns.FontIndex ترقيم الملف مع تخطي 4، تمامًا كما قُرئت؛ فهارس المقاطع تبدأ من الواحد وCharIndex هو إزاحة المحرف الذي يبدأ عنده المقطع. بعد الحفظ قد يختلف الرقم المخزن عمن ضبطته، لكنه ما يزال يشير إلى الخط نفسه
var
Book: IXLSWorkbook;
Note: TXLSComment;
I: Integer;
Ifnt: Word;
begin
Book := TXLSWorkbook.Create;
if Book.Open('review-notes.xls') <> 1 then
raise Exception.Create('Cannot open review-notes.xls');
Note := Book.Sheets[1].Range['C2', 'C2'].Comment;
if Note <> nil then
for I := 1 to Note.TextRuns.Count do
begin
Ifnt := Note.TextRuns.FontIndex[I]; // ترقيم الملف، مع تخطي 4
if Ifnt = 4 then
raise Exception.CreateFmt('Run %d uses invalid ifnt 4', [I]);
Writeln(Format('run %d at char %d: ifnt %d = FONT record #%d',
[I, Note.TextRuns.CharIndex[I], Ifnt, FontIndexToRecordNo(Ifnt)]));
end;
end;
النسخ وإدراج الصفوف وترحيل المقاطع بين المصنفات
منذ HotXLS 2.384.6 يحفظ كل مسار نسخ في المحرك الكلاسيكي مقاطع تنسيق التعليقات، لأن Range.Copy وCopyRange وSheets.AddCopy وإزاحات الخلايا الكامنة وراء Range.Insert وRange.Delete تمر جميعها عبر TXLSRange.CopyCell، وكانت CopyCell تنسخ نص التعليق ومؤلفه فقط. الإزاحة نسخ زمسح، فإدراج صف واحد فوق ملاحظة بمقطعين تركاها بلا مقاطع وبخط واحد. ينقل الإصلاح كل مقطع ويحرك خطه عبر TXLSWorkbook.MigrateRunFontIndex، التي تحول الفهرس القائم على تخطي 4 إلى خانة، وترحل الخط بالقيمة إلى جدول خطوط الوجهة، ثم تعيد التحويل إلى ترقيم الملف؛ وأصبح ترحيل النص المنسق في SST داخل Sheets.AddCopy يستدعي الدالة نفسها بدل أن يحمل نسخته الخاصة من الحساب. جاء معها حالتان حديتان: اللصق في الموضع نفسه حيث المصدر والوجهة هما التعليق ذاته يجب ألا يمسح مقاطعه قبل قراءتها، وصار Sheets.AddCopy يجري تمريرة ثانية للتعليقات المرفقة بخلايا بلا سجل خلية مخزن، وهي التي كان يتخطاها كليًا من قبل. ويتبع جانب جدول الخطوط في النسخ بين المصنفات منطق النقل بالقيمة نفسه المتبع في جانب الصيغ المغطى في النسخ بين المصنفات وإعادة ربط الصيغ. أما على محرك XLSX فكانت مسارات النسخ تستنسخ المقاطع بالقيمة سلفًا؛ والفجوة كانت في جزء التعليقات ذاته، حيث تجاهل القارئ rFont وstrike وu وvertAlign ولم يصدر الكاتب u أو vertAlign قط، فتنجو المقاطع الآن من الحفظ وإعادة الفتح بانسجام
كيف تختبر فهارس الخطوط في ملفات BIFF8؟
اختبر فهارس الخطوط بالحفظ وإعادة الفتح، وعبر أكثر من جيل واحد إن أمكن، وبتحويل كل ifnt إلى سجل FONT بدل التحقق من مدى رقمي. كل عيب في هذه القصة نجح في اختبار داخل الذاكرة: انحدار 2.384.1 عاش في زوج كاتب وقارئ متطابقين، وانحراف TXO احتاج حفظين مع تغيير جدول خطوط بينهما، ومقاطع التعليقات المفقودة على XLSX لم تظهر إلا بعد إعادة فتح. عدّة اختبار مفيدة تفتح عينة من صنع Excel، وتحفظها مرتين عبر HotXLS، وتضيف خطًا أو تزيله بين الحفظين، ثم تفحص مواضع المقاطع مع، على مستوى البايتات، أسماء الخطوط خلف كل ifnt. لا تقارن قيم FontIndex قبل الحفظ وبعده، فإعادة الترقيم أمر مشروع
procedure CheckNoteSurvivesShiftAndSave(const SrcFile, OutFile: string);
var
Book: IXLSWorkbook;
Note: TXLSComment;
RunCount: Integer;
SecondRunAt: Word;
begin
Book := TXLSWorkbook.Create;
Assert(Book.Open(SrcFile) = 1); // من صنع Excel، وC2 تحمل مقطعين
Note := Book.Sheets[1].Range['C2', 'C2'].Comment;
RunCount := Note.TextRuns.Count;
SecondRunAt := Note.TextRuns.CharIndex[2];
Book.Sheets[1].Range['C2', 'C2'].Copy(Book.Sheets[1].Range['E5', 'E5']);
Book.Sheets[1].Range['C1', 'C1'].Insert(xlShiftDown); // تنتقل C2 إلى C3
Assert(Book.SaveAs(OutFile) = 1);
Book := TXLSWorkbook.Create; // إعادة فتح، لا تثق بالذاكرة أبدًا
Assert(Book.Open(OutFile) = 1);
Note := Book.Sheets[1].Range['C3', 'C3'].Comment;
Assert((Note <> nil) and (Note.TextRuns.Count = RunCount));
Assert(Note.TextRuns.CharIndex[2] = SecondRunAt);
Note := Book.Sheets[1].Range['E5', 'E5'].Comment;
Assert((Note <> nil) and (Note.TextRuns.Count = RunCount));
end;
إن كنت تقرأ وتكتب XLS الكلاسيكي من Delphi أو C++Builder وتفضل ألا تتعقب أي مستهلك خط من مستهلكي المكتبة ما يزال يتفق مع §2.5.129 من [MS-XLS]، فإن ترقيم تخطي 4 وإعادة ترقيم المقاطع عند الحفظ وترحيل المقاطع بالقيمة الموصوفة هنا مدمجة كلها في مكوّن HotXLS لجداول Delphi الذي يقرأ ويكتب XLS وXLSX دون Excel أو أتمتة OLE