يخزن مكوّن HotXLS لدلفي صف ODS يحمل table:number-rows-repeated مع ارتفاع صف بوصفه سجل TXLSXRowHeightRun واحدًا — أول صف، آخر صف، ارتفاع واحد — بدل مدخل ارتفاع لكل صف مكرر، ويطوي نمط الخلايا الفارغة الذي ترثه تلك الصفوف في تراكب نمط فتري واحد. هذا هو السبب كله في أن HotXLS 2.382.2 يفتح جدولًا يتكرر ذيله 1,048,530 صفًا فارغًا في 0.02 ثانية حيث تجاوز 2.382.1 المهلة، ولماذا يحفظ الملف نفسه عائدًا إلى ODS بعداد التكرار سليمًا لا كمليون صف حرفي
الملف المقصود عادي. يكتب LibreOffice Calc ورقة بأربعة عشر عمودًا و45 صفًا من البيانات، ثم يصف كل ما دونها بعنصر واحد: <table:table-row table:style-name="ro1" table:number-rows-repeated="1048530"><table:table-cell table:number-columns-repeated="14"/></table:table-row>. يضبط النمط ro1 قيمة style:row-height="0.452cm"، ويحمل كل <table:table-column> قيمة table:default-cell-style-name ترثها كل خلية فارغة في الامتداد. حجم content.xml كله 103 كيلوبايت. لا شيء في الملف يقول «مكلف»؛ التكلفة كانت كلها من نصيبنا
لماذا يتجاوز صف مكرر واحد مهلة استيراد ODS؟
لأن المستورد كان يوسعه. في 2.382.1 دار منتهي الصفوف حلقة SetRowHeight(RowIndex + i, RowHeight) مرة لكل صف مكرر، كاتبًا كل ارتفاع في قائمة سلاسل Name=Value مفهرسة برقم الصف. كل إدراج في تلك القائمة شغّل بحث IndexOfName على كل ما فيها سلفًا، فكلفت مليون ارتفاع مليون مسح خطي — وهو البحث التربيعي في القوائم الذي رُفع ضده HXLS-005. وفي الوقت نفسه جسّد OdsCommitRow كائن خلية لكل عمود ورث نمطًا، في كل صف من الصفوف المكررة، لأن الخلية الفارغة ذات النمط ما تزال تُعد خلية
وكان لجانب الحفظ نسخته من المشكلة. ينتهي ملف LibreOffice بصف ro1 إضافي بعد التكرار الكبير، فجثم أعلى صف منمق في قاع الورقة تمامًا، وكان OdsBuildTableXml يمشي على كل صف حتى إليه مصدرًا عناصر <table:table-row> واحدًا تلو الآخر. حتى مصنف استورد برخصة لكان سيكتب بكلفة. إصلاح الاستيراد دون إصلاح التصدير كان سينقل المهلة لا يزيلها
ما امتداد ارتفاع الصفوف في HotXLS؟
الامتداد أصغر شيء يمكنه وصف «الصفوف من 46 إلى 1,048,575 كلها بارتفاع 12.81 نقطة» دون قول ذلك 1,048,530 مرة. TXLSXRowHeightRun سجل من FirstRow وLastRow وHeight؛ وTXLSXRowHeightRuns مصفوفة ديناميكية منها، ويحتفظ كل TXLSXWorksheet بواحدة في FRowHeightRuns بجوار قائمة الارتفاعات القائمة لكل صف. عند استيراد ODS يتفرع منتهي الصفوف الآن على عداد التكرار: عداد 1 ما يزال يستدعي SetRowHeight، وأي شيء أكبر يستدعي XlsxAssignRowHeightRun مرة واحدة للمدى كله. ويُحد المدى إلى XlsxMaxRow وهي 1,048,576، فعداد تكرار يتجاوز الورقة يُقتطع لا يُرفض
XlsxAssignRowHeightRun هو الكاتب الوحيد للمصفوفة، ويحفظ الامتدادات منفصلة بالبناء. أمام فترة جديدة ينسخ كل امتداد قائم يقع كليًا خارجها، ويشطر أي امتداد يتقاطع معها إلى القطعة قبلها والقطعة بعدها، ثم يلحق الفترة الجديدة حين تكون Present قيمتها true — أو لا يلحق شيئًا حين تكون Present false، وهي كيف يثقب ClearRowHeight ثقبًا بصف واحد. يترتب على ذلك أمران. لا تحوي المصفوفة قط فترات متقاطعة، فيمكن للبحث أن يقف عند أول إصابة. ولا تُبدَّل المصفوفة في مكانها قط؛ تُبنى نسخة جديدة عند كل استدعاء، وهي كلفة لا شيء عند الأحجام المعنية وتنزع صنفًا كاملًا من أخطاء التشارك المرجعي
var
Workbook: TXLSXWorkbook;
Sheet: TXLSXWorksheet;
begin
Workbook := TXLSXWorkbook.Create;
try
// ورقة يتكرر صفها الذيلي 1,048,530 مرة تحت نمط صف واحد
Workbook.OpenODS('conditional-formatting.ods');
Sheet := Workbook.Sheets[1];
// كلتا القراءتين تحلان عبر الامتداد نفسه؛ لم يُوسع شيء
Writeln(Sheet.RowHeight[46]:0:2, ' pt');
Writeln(Sheet.RowHeight[1048575]:0:2, ' pt');
// تجاوز أحادي الصف يظلل الامتداد دون شطره
Sheet.RowHeight[500000] := 36;
// محو صف واحد داخل الامتداد يقطعه قطعتين
Sheet.ClearRowHeight(500001);
Writeln(Sheet.HasRowHeight(500001)); // False
Writeln(Sheet.RowHeight[500002]:0:2, ' pt'); // ما يزال ارتفاع الامتداد
finally
Workbook.Free;
end;
end;
ترتيب البحث هو الجزء الذي يستحق الحفظ. يفحص TXLSXWorksheet.GetRowHeight قائمة الصفوف أولًا ولا يراجع الامتدادات إلا حين لا يكون للصف مدخل صريح، ويفعل HasRowHeight الشيء نفسه. لذلك Sheet.RowHeight[500000] := 36 لا تلمس الامتداد إطلاقًا — تضيف مدخلًا واحدًا إلى قائمة الصفوف، ويفوز ذلك المدخل لأنه يُبحث أولًا. أما ClearRowHeight فهو العكس: ينزع أي مدخل لكل صف ثم يستدعي XlsxAssignRowHeightRun بقيمة Present = False، لأن الصف الممحون يجب أن يُقرأ «بلا ارتفاع» حتى لو غطاه امتداد. وClearRowHeights يفرغ البنيين معًا دفعة واحدة
إلى أين تذهب أنماط الخلايا الفارغة الموروثة؟
إلى تراكب نمط فتري واحد لكل عمود، لا إلى كائنات خلايا. يقرر OdsCommitRow لكل قيمة عمود ما إذا كانت فراغًا مضغوطًا: الصف يتكرر أكثر من مرة، والخلية بلا قيمة وبلا صيغة وبلا نص منسق. للفراغ المضغوط ينشئ خلية حقيقية فقط على أول صف من الامتداد، ويطبق عليها النمط الموروث، ثم يسجل الفهارس الستة للنمط نفسها — الخط والتعبئة والحدود وصيغة الرقم والمحاذاة والحماية — عبر StyleOverlays.Add يغطي الصفوف من الثاني إلى نهاية الامتداد في ذلك العمود. وتتخطى حلقة التجسيد الصفوف بعد الأول كليًا
اختبار الانحدار يجعل الشكل ملموسًا. بعد فتح ورقة يتكرر صفها الثاني 1,048,575 مرة تحت نمط افتراضي عمودي عريض، يُؤكد أن Sheet.Cells.Count دون 10، وأن Sheet.Cells[700000, 1].FontIndex ما يزال يحل إلى الخط العريض — التراكب يمد النمط لحظة لمس ذلك الإحداثي. هذه هي الآلية نفسها التي تمنع عمودًا منسقًا فارغًا من كلفت مليون خلية على جانب XLSX؛ تغطي ملاحظات تخزين خلايا كتل الصفوف وتراكبات الأنماط الفترية كيف تتراكب التراكبات وتُحل. الجديد هنا أن مستورد ODS ينشئها من تلقاء نفسه، من عداد التكرار، بدل انتظار تطبيق ينسق مدى
كيف تكتب SaveAsODS عداد التكرار عائدًا؟
بشطر ذيل الورقة الفارغ فقط حيث يتغير شيء فعلًا. يتتبع OdsBuildTableXml الآن حدّين: contentMaxRow، آخر صف يحمل قيمة أو صيغة أو رابطًا تشعبيًا أو فاصل صف يدوي، وmaxRow الذي يمتد إضافة إلى ذلك عبر خلايا فارغة نمطية فقط، وارتفاعات الصفوف الأحادية، وLastRow لكل امتداد، وحافة كل تراكب السفلى. لم تعد الخلية الفارغة النمطية فقط تُعد محتوى — TXLSXCells.IsStyleOnlyBlank هي ما يستبعدها — فيتوقف الصف المنمق الختامي في ملف LibreOffice عن جرّ حد المحتوى إلى قاع الورقة
فوق contentMaxRow تكتب الصفوف واحدًا تلو الآخر تمامًا كما قبل. تحته يحسب الكاتب nextRow أصغر ما في: FirstRow للامتداد التالي، وLastRow + 1 للامتداد الحالي، ومدخل ارتفاع الصف الأحادي التالي، وحافة التراكب التالية، والخلية المجسدة التالية. ثم يصدر كل شيء من الصف الحالي إلى nextRow - 1 بوصفه <table:table-row> واحدة بقيمة table:number-rows-repeated تساوي الفرق، حاملة <table:table-cell/> واحدة لكل عمود باسم النمط المحلول عبر التراكب حين يغطي تراكب ذلك العمود. ويأتي نمط الصف نفسه من TOdsAutoStylePool.RowStyleFor(AHidden, ABreakBefore, AHeightSpec) الذي يطوي الآن نص الارتفاع — 12.81pt مثلًا — في مفتاح نزع التكرار عنده إلى جانب علمي الإخفاء وفاصل الصفحة، فيتشارك كل صف في الامتداد نمط ro<N> واحدًا بخاصية style:row-height واحدة
var
Workbook, Reopened: TXLSXWorkbook;
Saved: TMemoryStream;
begin
Workbook := TXLSXWorkbook.Create;
Reopened := TXLSXWorkbook.Create;
Saved := TMemoryStream.Create;
try
Workbook.OpenODS('conditional-formatting.ods');
Workbook.Sheets[1].RowHeight[500000] := 36;
Workbook.Sheets[1].ClearRowHeight(500001);
// الذيل الفارغ يُكتب حفنة صفوف مكررة، لا مليونًا
Workbook.SaveAsODS(Saved);
Writeln('ODS size: ', Saved.Size, ' bytes');
Saved.Position := 0;
Reopened.Open(Saved);
// التجاوز والثقب والامتداد كلها تنجو من الجولة
Writeln(Reopened.Sheets[1].RowHeight[500000]:0:2); // 36.00
Writeln(Reopened.Sheets[1].HasRowHeight(500001)); // False
Writeln(Reopened.Sheets[1].RowHeight[500002]:0:2); // ارتفاع الامتداد
finally
Saved.Free;
Reopened.Free;
Workbook.Free;
end;
end;
الاختبار الذي يثبت هذا يؤكد أن التدفق المحفوظ دون 64 كيلوبايت لورقة يمتد فيها امتداد الارتفاع على 1,048,575 صفًا مع تجاوز وثقب مثقوب في وسطه. وحذان صادقان يجاوران ذلك الرقم. أولًا، ورقة عمل فيها أي تحقق بيانات تضبط contentMaxRow إلى maxRow، فتعطل التحققات ضغط الذيل في تلك الورقة وتُكتب صفًا صفًا من جديد. ثانيًا، لا يملك XLSX سمة تكرار — <row> في SpreadsheetML يصف صفًا واحدًا — فيصدّر ورقة مدعومة بامتداد إلى .xlsx الصفوف التي يغطيها الامتداد ويكتب سمة ht على كل منها. النموذج ما يزال مضغوطًا في الذاكرة؛ صيغة الملف هي التي تقرر شكل الملف
ما الذي تدين به الآن كل عملية تعيد ترقيم الصفوف للامتدادات؟
الصيانة. تمثيل جديد لبيانات الصفوف الوصفية لا يكون صحيحًا إلا إذا نقلته معه كل عملية تغير أرقام الصفوف إلى جوار قوائم الصفوف التي يجاورها، والمتجر يلمس كل واحدة من تلك العمليات. تمر InsertRows وDeleteRows عبر XlsxShiftRowHeightRuns الذي يعيد بناء المصفوفة معيلًا الجزء من كل امتداد يقع قبل نقطة التعديل، ملقيًا ما يقع داخل نافذة حذف، ومعيدًا إضافة الباقي منزاحًا بالفارق — فيصير امتداد يعبر نقطة إدراج امتدادين بفجوة، وامتداد يعبر نقطة حذف يتقلص. وTileRangeAxisMetadata يمحو الامتدادات عبر المدى المبلط كله، ثم يعيد تسجيل كل امتداد مصدر مرة لكل نسخة عند إزاحته. وTXLSXWorksheet.CopyFrom وTXLSXSheets.AddCopy تأخذان Copy() من المصفوفة بدل إسنادها، ولهذا يستطيع الاختبار أن يمحو كل الارتفاعات على نسخة ويجد الورقة الأصلية سليمة عند الصف 1,048,576
var
Sheet: TXLSXWorksheet;
begin
Sheet := Workbook.Sheets[1];
Sheet.RowHeight[500000] := 36;
Sheet.ClearRowHeight(500001);
// أدخل صفين عند 500000: ينتقل التجاوز إلى 500002 والثقب إلى 500003
Sheet.InsertRows(500000, 2);
Writeln(Sheet.RowHeight[500002]:0:2); // 36.00
Writeln(Sheet.HasRowHeight(500003)); // False
// احذفهما مجددًا: كل شيء يعود
Sheet.DeleteRows(500000, 2);
Writeln(Sheet.RowHeight[500000]:0:2); // 36.00
// بلّط الصفوف 2..4 مرتين نزولًا في الورقة؛ تتبع ارتفاعات الامتداد كل نسخة
Sheet.TileRangeAxisMetadata(2, 1, 3, 1, 2, 1);
Writeln(Sheet.RowHeight[7]:0:2); // ارتفاع الامتداد
end;
لحدود جانب القراءة الالتزام نفسه. يرفع GetUsedRange حافته السفلى إلى FirstRow وLastRow لكل امتداد، ويمد BuildRowMajorCellOrder صفه الأقصى الشامل للبيانات الوصفية عبر كل امتداد فيما يزال كاتب XLSX يزور صفوف الارتفاع فقط. وإن أضفت يومًا بنية مفهرسة بالصفوف من عندك فوق نموذج كائنات HotXLS، فهذه هي القائمة: إدراج، حذف، تبليط، نسخ، مدى مستخدم، وكل مسلسل. فاتك واحدًا كان الفشل صامتًا — تنحرف الارتفاعات بمقدار عدد الإدخالات ولا شيء يرفع استثناء
ما الذي يبقى لكل صف، وكيف تبدو الأرقام الآن
أعلام الإخفاء ومستويات التخطيط وحالة الطي ما تزال تتوسع. تدار منتهي الصفوف حلقة SetRowHidden وSetRowOutlineLevel مرة لكل صف مكرر، فورقة تخفي ذيلًا بمليون صف، أو تدشنه داخل table:table-row-group، تدفع مدخلًا لكل صف عن كل واحد من تلك السمات. التغيير في 2.382.2 محصور في الشيئين اللذين قاسهما HXLS-005 فعلًا — الارتفاعات وأنماط الفراغ الموروثة — وتنطبق تقنية الامتداد نفسها على البقية إن طالب ملف يومًا. كما أن قارئ ODS لا يفعل شيئًا تجاه style:use-optimal-row-height؛ نمط صف يقول «أمثل» ويعطي ارتفاعًا يُستورد بذلك الارتفاع
مقابل المجموعة، يكمل conditional-formatting.ods الآن دورة الفتح والتأكيد والحفظ وإعادة الفتح وإعادة التأكيد في 0.178 ثانية على Win32 و0.158 ثانية على Win64، بمرحلة الفتح ذاتها عند 0.020 ثانية، داخل ميزانية 60 ثانية كان قد استنفدها سلفًا. تُوصف واجهات مستوى المصنف التي تجري فيها الصيغ في شرح فتح وحفظ ملفات ODS، والمجموعة الأوسع من روافع الملفات الكبيرة في أداء المصنفات الكبيرة؛ وعنصر الصف في ODF نفسه، بسمتي التكرار والنمط، محدد في ODF 1.3 Part 3 §9.1.4
يقرأ HotXLS ويكتب XLS وXLSX وODS من كود دلفي وC++Builder أصلي دون تنصيب Excel أو LibreOffice، ولهذا فإن تكرارًا بمليون صف شيء يجب أن تمثله المكتبة جيدًا لا تسليمه لعملية خارجية — صفحة مكوّن جداول HotXLS لدلفي تسرد الصيغ المدعومة وإصدارات RAD Studio