مقال تقني

توقيت بصمة المخطط وإزاحات المرتكز في HotXLS لدلفي

لا يعيد مكوّن HotXLS لدلفي إنتاج مخطط Excel غير معدّل بايتًا ببايت إلا إذا توافر شرطان: أن يكون المخطط قد وُصل إليه عبر علاقة رسم ورقة العمل لا عبر اسم جزء مخمّن، وأن تكون بصمة النموذج ذات 64 بت قد أُخذت بعد اكتمال تحليل نموذج المخطط. الإصدار 2.382.0 أصلح الشرط الأول، والإصدار 2.382.3 أصلح الثاني وبدأ في إعادة إظهار إزاحات المرتكز غير الصفرية xdr:colOff وxdr:rowOff التي كان كاتب الرسم يثبّتها صفرًا إجباريًا. كلا العيبين خرج من حالة واحدة في مجموعة الاختبار المحلية، الملف two-charts.xlsx: أولًا رأى تأكيد بنيوي أجزاء مخططات تتحول من اثنين إلى ثلاثة، ثم أظهرت مقارنة بايتات كل xl/charts/chartN.xml أن مخططات لم يلمسها أحد ما تزال يعاد كتابتها — ولم يرفع أي من المشكلتين استثناء ولا أثار Excel شكوى، ولهذا بقيا على قيد الحياة هكذا طويلًا

لماذا عاد مصنف بمخططين بثلاثة أجزاء مخططات؟

لأن المحمّل كان يملك مسار تراجع يخمّن. حين كانت ورقة العمل بلا علاقة رسم في جزء .rels الخاص بها، كان الكود القديم يفترض أن الرسم يقيم عند الاسم الاصطلاحي xl/drawings/drawing{i+1}.xml حيث i موضع الورقة، ويربط ذلك الجزء إن وُجد في الأرشيف. في two-charts.xlsx الورقة الأولى بلا رسم وبلا جزء .rels على الإطلاق، بينما xl/drawings/drawing1.xml موجود فعلًا — وهو يعود للورقة الثانية التي تصل إليه عبر Target="../drawings/drawing1.xml". ورثت الورقة 1 إذن مخططًا لم تشير إليه قط، وحُلّل chart1.xml مرتين، وكتب الحفظ المصنف بثلاثة أجزاء مخططات بدل اثنين

كيف يحل HotXLS رسومات أوراق العمل في عينة two-charts: الورقة Sheet1 بلا علاقة رسم وبلا جزء rels بينما تصل Sheet2 إلى xl/drawings/drawing1.xml عبر ParPartTargets، وكان مسار التراجع قبل 2.382.0 يخمّن ذلك الاسم الاصطلاحي من موضع الورقة فيحلل chart1.xml مرتين ويكتب الحفظ ثلاثة أجزاء مخططات حتى جعل الإصلاح تحميل الرسومات عبر XlsxRtDrawing وحده
لم تشير Sheet1 قط إلى مخطط، فالرسم البياني للعلاقات هو المصدر الآمن الوحيد لهدف الرسم، والاسم الاصطلاحي المخمّن حوّل مصنفًا بمخططين إلى حفظ بثلاثة أجزاء

أزال إصلاح HotXLS في v2.382.0 التخمين كليًا. لم يعد تحميل رسم ورقة العمل يجري إلا عبر ParPartTargets[i].Values[XlsxRtDrawing]، أي الهدف المسجّل لنوع علاقة الرسم على تلك الورقة، والورقة بلا هذه العلاقة لا تحصل على رسم إطلاقًا. هذا هو السلوك الذي يطلبه التنسيق: عنصر <drawing r:id="…"/> في ورقة العمل (ECMA-376 Part 1 §18.3.1.36) هو الوصلة الوحيدة بين الورقة ورسمها، وأسماء الأجزاء في حزمة OPC لا تحمل أي معنى فوق ما يسنده لها الرسم البياني للعلاقات. أرشيفات Excel تكتب بالأسماء الاصطلاحية صدفةً، وهذا ما جعل الاختصار يمر دون شك كل هذه المدة؛ شرح حل علاقات OPC في HotXLS يغطي لماذا يظل تخمين اسم جزء غير آمن أبدًا حتى لو كان التخمين صائبًا في الغالب

// قبل v2.382.0: علاقة الرسم المفقودة كانت تتراجع إلى تخمين
drawingName := ParPartTargets[i].Values[XlsxRtDrawing];
if drawingName = '' then
  drawingName := 'xl/drawings/drawing' + IntToStr(i + 1) + '.xml';
if zip.Exists(drawingName) then
  LoadDrawing(zip, drawingName);   // قد تنتمي إلى ورقة أخرى

// منذ v2.382.0: العلاقة أو لا شيء
drawingName := ParPartTargets[i].Values[XlsxRtDrawing];
if (drawingName <> '') and zip.Exists(drawingName) then
  LoadDrawing(zip, drawingName);

ما الذي تضمنه بصمة المخطط؟

تقرر البصمة، لكل مخطط، ما إذا كان الحفظ يستطيع نسخ الجزء الأصلي أو يجب أن يعيد توليده. عند الاستيراد، مع تفعيل PreserveUnsupportedParts قبل Open، يحفظ HotXLS بايتات UTF-8 الخام لكل جزء مخطط في FRawChartXml، ويبني تسلسل النموذج المطبوع الخاص به عبر BuildChartKnownXml، ويخزن طول ذلك التسلسل في FRawChartModelLength وتجزئته في FRawChartModelHash. التجزئة FNV-1a على وحدات كود UTF-16 لـ XML المولد، بأساس الإزاحة القياسي ذي 64 بت 14695981039346656037 والعدد الأولي 1099511628211. عند الحفظ يعيد XlsxChartRawModelUnchanged بناء XML المعروف ويقارن الطول والتجزئة؛ التطابق يعني أن النموذج المطبوع هو ذاته الذي كان عند الاستيراد، فلا شيء مما كان التطبيق باستطاعته تغييره قد تغير

يلتقط HotXLS بصمة المخطط عند الاستيراد، فيحفظ بايتات UTF-8 الخام في FRawChartXml بينما ينتج BuildChartKnownXml الطول FRawChartModelLength وتجزئة FNV-1a، وعند الحفظ يعيد XlsxChartRawModelUnchanged البناء ويقارن القيمتين، فيعيد التطابق البايتات الأصلية أو ينسخ المدخل المضغوط ويسقط عدم التطابق إلى XlsxMergeChartXml
قيمة البصمة بقدر لحظة أخذها، والالتقاط قبل انتهاء كل اجتيازات الاسترداد يضمن تجزئة لا تطابق النموذج المكتمل مرة أخرى أبدًا
function XlsxChartRawModelUnchanged(Chart: TXLSXChart;
  const KnownXml: WideString): Boolean;
begin
  Result := (Chart <> nil) and (Chart.FRawChartXml <> '') and
    (Length(KnownXml) = Chart.FRawChartModelLength) and
    (XlsxChartModelHash(KnownXml) = Chart.FRawChartModelHash);
end;

function BuildChartXmlFromKnown(Chart: TXLSXChart;
  const KnownXml: WideString): WideString;
begin
  if Chart.FRawChartXml = '' then
    Result := KnownXml                                  // لا شيء محفوظ
  else if XlsxChartRawModelUnchanged(Chart, KnownXml) then
    Result := XlsxDecodeChartUtf8(Chart.FRawChartXml)   // إعادة حرفية
  else
    Result := XlsxMergeChartXml(
      XlsxDecodeChartUtf8(Chart.FRawChartXml), KnownXml); // دمج بنيوي
end;

يخطو كاتب XLSX خطوة أبعد من BuildChartXmlFromKnown. حين يكون النموذج غير متغير وStrictOOXML معطلة، يحاول أولًا نسخ المدخل المضغوط مباشرة من الأرشيف المصدر إلى الناتج تحت اسم الجزء الجديد للمخطط، فلا تُفك بايتات حتى ولا يعاد ضغطها. وفقط إن تعذر ذلك النسخ يسقط إلى مسار الفك أو الدمج. الآلية ذاتها — طول مع تجزئة، إعادة عند التساوي، دمج عند الاختلاف — هي الموصوفة في ملاحظة تحرير مخططات Excel دون فقدان ChartML. موضوع هذه المقالة هو الطريقة التي توقفت عن العمل بها بصمت

لماذا سلك كل مخطط مسار الدمج على أي حال؟

لأن البصمة التُقطت استدعاءً واحدًا قبل الأوان. تحليل المخطط في HotXLS اجتياز SAX على جزء المخطط يليه مجموعة اجتيازات استرداد تسحب من النص الخام تفاصيل لا تمثلها معالجات SAX مباشرة: يقرأ XlsxChartParseSeriesFlags كل كتلة <c:ser> بحثًا عن علم <c:smooth> وقيم srgbClr لتعبئة المؤشر وخطه، ثم يسترد أوضاع تقاطع المحاور وأنماط علامات التدريج الرئيسية والثانوية لمحوري الفئات والقيم. قبل v2.382.3 كان الترتيب في نهاية ParseChartXml: تصنيف مجموعات المحاور، بناء XML المعروف، التقاط الطول والتجزئة، وبعدها فقط تشغيل XlsxChartParseSeriesFlags. فكانت البصمة تصف نموذجًا ما يزال ينقصه أعلام smooth وألوان المؤشرات وعلامات التدريج. وعند الحفظ اشتغل BuildChartKnownXml على النموذج المكتمل الذي أصبح يصدر <c:smooth val="1"/> وألوان المؤشرات المستردة. XML أطول، تجزئة مختلفة، أعادت XlsxChartRawModelUnchanged القيمة False، ومضى المخطط عبر XlsxMergeChartXml. الدمج عملية صحيحة لمخطط حرره أحد، لكنها ليست عملية حافظة للبايتات: فهي تعيد تسلسل الشجرة، وقاعدة الملكية التي تجعل النموذج المطبوع هو الغالب في السلاسل والمحاور ومجموعات الرسم تعني أن العقد المعادة توليدًا تحل محل الأصلية. كانت النتيجة المرئية في تشغيل المجموعة انجراف ألوان السلاسل في مخططات لم يحررها أحد — كل مخطط في كل مصنف محفوظ، في كل حفظ، بلا أي تشخيص في أي مكان

الإصلاح إعادة ترتيب واحدة: صار XlsxChartParseSeriesFlags يعمل قبل بناء XML المعروف، فتصف البصمة النموذج كما سيكون لحظة أول رؤية التطبيق له. والدرس يعم خارج المخططات. بصمة كشف التغيير قيمتها بقدر لحظة أخذها، واللحظة الآمنة هي بعد اكتمال كل اجتياز يستطيع تغيير النموذج. يملك HotXLS موقع التقاط ثانٍ للقيمتين نفسيهما، خط الأساس الذي يعيد تأسيسه على ملف الناتج بعد حفظ ناجح، وكان ذلك الموقع دائمًا يعمل على نموذج محلل بالكامل؛ موقع وقت الاستيراد كان الشاذ الوحيد

إلى أين ذهبت إزاحات المرتكز؟

إلى صفر مكتوب حرفيًا. يثبّت twoCellAnchor في جزء الرسم المخطط بين خليتين، وتحمل كل زاوية فهرس خلية مع إزاحة داخل تلك الخلية: يحمل كل من from (ECMA-376 Part 1 §20.5.2.5) وto (§20.5.2.32) عنصري col وcolOff (§20.5.2.4) وrow وrowOff. الإزاحات بوحدات English Metric Units، أي 914400 للبوصة، ويكتب Excel قيمًا غير صفرية كلما وُضع مخطط أو غيّر حجمه بالفأرة، وهذا حال معظم المخططات. أول مخطط في two-charts.xlsx يبدأ عند الصف 0 بـ rowOff يساوي 19049 وينتهي عند العمود 8 والصف 15 بـ colOff يساوي 247650 وrowOff يساوي 66674 — أي نحو ربع بوصة داخل العمود الأخير. كان محلل الرسم في HotXLS يقرأ تلك القيم الأربع دائمًا — كود الصور استخدمها — لكن كاتب المخطط كان يصدر <xdr:colOff>0</xdr:colOff> و<xdr:rowOff>0</xdr:rowOff> لكل زاوية، فيلصق كل مخطط بشبكة الخلايا عند الحفظ

تشريح زوايا xdr:twoCellAnchor لأول مخطط في عينة HotXLS: تحمل from العمود 0 وrowOff 19049 بينما تحمل to العمود 8 وcolOff 247650 وrowOff 66674 بوحدات EMU بمعدل 914400 للبوصة، وكان الكاتب الذي يصدر إزاحات صفرية يلصق المخططات بالشبكة حتى أعادت FFromColOff وFToColOff وأخواتها إظهار القيم المستوردة
يقيم المرتكز في جزء الرسم لا في جزء المخطط، فهذا الإصلاح مستقل عن إصلاح البصمة، وكان لا بد من شحن كليهما قبل أن تكتمل جولة الحفظ الأمينة للمصنف فعلًا
// منذ v2.382.3 يعيد كاتب المرتكز إظهار إزاحات EMU المستوردة
Result := '<xdr:twoCellAnchor' + EditAsAttr + '><xdr:from><xdr:col>' +
  IntToStr(Chart.FromCol - 1) + '</xdr:col><xdr:colOff>' +
  IntToStr(Chart.FFromColOff) + '</xdr:colOff>' +
  '<xdr:row>' + IntToStr(Chart.FromRow - 1) + '</xdr:row>' +
  '<xdr:rowOff>' + IntToStr(Chart.FFromRowOff) + '</xdr:rowOff></xdr:from>' +
  '<xdr:to><xdr:col>' + IntToStr(Chart.ToCol - 1) + '</xdr:col><xdr:colOff>' +
  IntToStr(Chart.FToColOff) + '</xdr:colOff>' +
  '<xdr:row>' + IntToStr(Chart.ToRow - 1) + '</xdr:row>' +
  '<xdr:rowOff>' + IntToStr(Chart.FToRowOff) + '</xdr:rowOff></xdr:to>' + ...

يحمل TXLSXChart الآن FFromColOff وFFromRowOff وFToColOff وFToRowOff، تُملأ من محلل الرسم وتُنسخ مع بقية حالة المرتكز عند إسناد مخطط. وهي خاصة عمدًا: السطح العام للمرتكز ما يزال إحداثيات الخلايا الأربع FromRow وFromCol وToRow وToCol، والمخطط المنشأ من كود دلفي يقع على حدود الخلايا كما كان. الإزاحات وُجدت لتجعل الجولة أمينة، لا لكشف التموضع داخل الخلية كميزة. ولاحظ أن هذا الإصلاح مستقل عن البصمة: المرتكز يقيم في جزء الرسم لا جزء المخطط، فمخطط أعيد إظهار ChartML الخاص به بإتقان ما كان سيقفز إلى الشبكة بدونه. تحويلات الوحدات وراء قيم EMU تلك مغطاة في ملاحظة هندسة الصور وتحويلات EMU في HotXLS

كيف تثبت أن مخططًا ما يمر بالجولة دون تغيير؟

بمقارنة البايتات، لا بفتح الناتج في Excel. يرمم Excel ويطبّع الكثير عند التحميل لدرجة أن المخطط المنحرف يبدو سليمًا حتى يلاحظ محلل أن لون المؤشر تغير. اختبار المجموعة الذي التقط العيبين يفعل ثلاثة أشياء بعد فتح وحفظ دون أي تعديلات: يمشي على علاقات ورقة العمل والرسم والمخطط ويفشل عند أي مرجع مخطط مكرر أو يتيم أو معلق؛ يقارن بصمة نوع المخطط وصيغ السلاسل وهندسة المرتكز بين الأصل والناتج؛ وبالنسبة إلى two-charts.xlsx يقرأ كل xl/charts/chartN.xml من الأرشيفين ويشترط تطابق البايتات. والكشف نفسه سهل الكتابة في دلفي عبر TZipFile من RTL

uses System.Zip, System.SysUtils;

function ChartPartsIdentical(const Original, Resaved: string): Boolean;
var
  Src, Dst: TZipFile;
  Name: string;
  A, B: TBytes;
begin
  Result := True;
  Src := TZipFile.Create;
  Dst := TZipFile.Create;
  try
    Src.Open(Original, zmRead);
    Dst.Open(Resaved, zmRead);
    for Name in Src.FileNames do
      if Name.StartsWith('xl/charts/chart') and Name.EndsWith('.xml') then
      begin
        Src.Read(Name, A);
        Dst.Read(Name, B);   // تطلق استثناء إذا اختفى الجزء
        if (Length(A) <> Length(B)) or
           ((Length(A) > 0) and not CompareMem(@A[0], @B[0], Length(A))) then
        begin
          Writeln('changed: ', Name);
          Result := False;
        end;
      end;
  finally
    Dst.Free;
    Src.Free;
  end;
end;

ثلاثة شروط تجعل تلك المقارنة ذات معنى، وكل منها يفشل بصمت إن نُسي. يجب أن تكون PreserveUnsupportedParts بقيمة True قبل Open، وإلا لم تلتقط بايتات خام وأعيد بناء كل مخطط من النموذج. يجب أن تكون StrictOOXML بقيمة False، لأن الوضع الصارم يفرض إعادة التوليد عمدًا. وعلى التطبيق ألا يلمس المخطط بين الفتح والحفظ — قراءة الخصائص لا ضرر منها، لكن أي مُعيّن يغيّر النموذج المطبوع يقلب البصمة ويرسل المخطط إلى مسار الدمج، وهو سلوك صحيح لكنه ليس ما صُمم هذا الاختبار لأجله. كما يعاد ترقيم أجزاء المخططات عند الحفظ من عداد على مستوى المصنف بأكمله، فمصنف تغيّر ترتيب أوراقه أو ترتيب مخططاته سيضع بايتات متطابقة تحت اسم chartN.xml مختلف؛ ولهذا السبب يتبع فاحص المجموعة العلاقات لا الأسماء

شُحن كلا الإصلاحين في HotXLS 2.382.0 و2.382.3 وجرى التحقق منهما على Win32 وWin64 مقابل المجموعة المحلية، مع إعادة عرض عينات المخططات المعاد حفظها عبر مجموعة أوفيس مستقلة إلى PDF ومقارنتها صفحة صفحة بالأصول. يقرأ HotXLS مخططات XLSX ويحررها ويكتبها من كود دلفي وC++Builder أصلي دون أي تنصيب لـ Excel، وهذا بالضبط ما يجعل هذا المستوى من الأمانة مسؤولية مكتبة — صفحة مكوّن جداول HotXLS لدلفي فيها قائمة الميزات وتنزيل تجريبي