مقال تقني

لوحة ألوان BIFF8 ذات 56 لونًا: مطابقة OKLab في HotXLS

يحوّل HotXLS ألوان RGB وألوان السمات مهما كانت إلى لوحة ألوان BIFF8 ذات الخانات الـ 56 على طبقتين: NearestIndexedColor يجد أقرب مدخل موجود في اللوحة إدراكيًا ضمن فضاء OKLab، وBuildBiffPalettePlan مع ApplyBiffPalettePlan يعيدان كتابة الخانات الحرة كي ينجو مصنف بالألوان الحقيقية من عملية حفظ إلى XLS الكلاسيكي. الدافع هو تذكرة الدعم ذاتها دائمًا: أحدهم يبني تقريرًا في XLSX برؤوس بلون كحلي مؤسسي ولمسة فيروزية هادئة، ثم يحفظه بصيغة .xls لمستهلك قديم، فتعود الرؤوس سوداء صرفة بينما يتحول الفيروزي إلى تركوازي صارخ. لم ينهار شيء ولم يُطلق أي تحذير. النموذج اللوني للصيغة القديمة لا يتسع ببساطة لما وصفته الجديدة، وكل ما على المكتبة أن تختار شيئًا

لماذا لا يستطيع ملف XLS أن يحمل أكثر من 56 لونًا؟

لأن تنسيق خلية BIFF8 لا يخزن قيمة RGB أبدًا: الخطوط والتعبئات والحدود تحمل مؤشر لون، وسجل Palette العالمي للمصنف ($0092، [MS-XLS] §2.4.188) يزود 56 مدخل RGB معتمًا بالضبط للمؤشرات من 8 إلى 63. المؤشرات من 0 إلى 7 نسخ مثبتة من الألوان الأساسية الثمانية، والقيم فوق 63 ليست ألوانًا أصلًا بل رموز مثل لون النظام الأمامي ولون النظام الخلفي ونص الرسم البياني. يعرض HotXLS اللوحة عبر ColorIndex عام من 1 إلى 56، وهو المؤشر الفعلي ناقص 7، ويبقي ResolveIndexedColor أنظمة الترقيم الثلاثة منفصلة عبر TXLSIndexedColorSpace: xicsPublicColorIndex لقيم الـ API من 1 إلى 56، وxicsBiffIcv للمؤشرات الخام على القرص، وهي تتحقق مقابل المجموعة الجزئية IcvFont أو IcvXF أو IcvChart حسب الدور الذي تمرره، وxicsOoxmlIndexed حيث تعني 64 و65 لوني النظام الأمامي والخلفي

يفصل HotXLS أنظمة الألوان المفهرسة الثلاثة عبر TXLSIndexedColorSpace: قيم icv الخام في BIFF، ومن 0 إلى 7 مثبتة على الألوان الأساسية الثمانية، وخانات اللوحة 56 من 8 إلى 63 في سجل Palette $0092، والرموز فوق 63 مثل لون النظام الأمامي، وColorIndex العام من 1 إلى 56 بإزاحة ناقص 7، وxicsOoxmlIndexed حيث تعني 64 و65 لوني النظام الأمامي والخلفي
المؤشر اللوني نفسه يعني أرقامًا مختلفة في كل نظام، لذا يمرر HotXLS كل قيمة عبر ResolveIndexedColor بدل أن يترك رمز BIFF خامًا يتقمص دور ColorIndex عام
var
  Res: TXLSIndexedColorResolution;
begin
  // $40 رمز icv في BIFF، وليس خانة في اللوحة
  Workbook.ResolveIndexedColor($40, xicsBiffIcv, Res);
  case Res.Kind of
    xickPalette:   UseArgb(Res.ARGB);   // خانة في اللوحة، إن حُسمت
    xickAutomatic,
    xickSystem:    UseSystemColor(Res.SystemColorRole);
    xickInvalid:   RejectToken(Res.RawIndex);
  end;
end;

لاحظ أن المثال يتفرع على Res.Kind ويتجاهل القيمة المنطقية المعادة. إن ResolveIndexedColor يعيد True فقط حين يحصل على ARGB ملموس، والتحميل الزائد القصير لا يقرأ سطح مكتب Windows قط، فرمز تلقائي أو نظامي يعيد False بشكل مشروع وهو ما يزال مصنفًا xickSystem. اصطدم HotXLS بذلك في مُسلسل مصنفاته هو: كود يعامل False بوصفها «لا لون» يتخلص بصمت من معنى Automatic وSystem للرمز. وإن احتجت قيم RGB حقيقية لتلك الرموز، فاستدعِ التحميل الزائد الطويل وزوّد رد نداء TXLSTryResolveSystemColor الذي يطبق سياستك أنت في الواجهة أو التصدير أو التشغيل بلا واجهة

لماذا يطابق HotXLS الألوان في OKLab بدل RGB؟

لأن قيم قنوات sRGB مشفرة غاما، فالمسافة الإقليدية في RGB لا تتبع ما يراه الإنسان، والخطأ يبلغ أسوأ حالاته في الدرجات الداكنة المشبعة تحديدًا التي تعشقها اللوحات المؤسسية. خذ الأزرق الداكن $000033. في RGB تبعد مسافة 51 عن الأسود و77 عن مدخل الكحلي الافتراضي $000080، فأي مطابِق يعتمد RGB يرسم رأسك أسود بكل ثقة. أما في OKLab فالمسافات المربعة نحو 0.0312 إلى الأسود و0.0235 إلى الكحلي، فيختار HotXLS الكحلي، ColorIndex 11 في الخانة الفعلية 18؛ وهذه الحالة بالذات مثبتة في جناح الاختبارات لكلا المحركين Classic وXLSX. التحويل داخل ArgbToOklab يجعل كل قناة sRGB خطية، ويطبق مصفوفة LMS الخاصة بـ OKLab، ويأخذ الجذور التكعيبية ويسقط على L وa وb، وعندها تصبح المسافة الإقليدية المربعة المجرّدة وكيلًا معقولًا للفرق المدرك. إن OKLab ليس CIEDE2000 ولا يدّعي كونه كذلك، لكنه لا يحمل أي تصحيحات تدرج مجزأة، وتكلفته حفنة ضربات لكل لون، وهو مستقر بما يكفي لقيادة حلقة تجميع، وهناك حيث يكسب مقعده فعلًا

كيف يطابق HotXLS الأزرق الداكن $000033 على اللوحة: تقيس المسافة الإقليدية في RGB المشفر غاما 51 إلى الأسود و77 إلى الكحلي وترسم الرأس أسود، بينما تتيح المسافات المربعة في ArgbToOklab البالغة 0.0312 و0.0235 اختيار NearestIndexedColor للكحلي، ColorIndex 11 في الخانة الفعلية 18
قيم القنوات المشفرة غاما تجعل مسافة RGB وكيلًا رديئًا لما يراه الإنسان، لذا يحوّل HotXLS مرة واحدة إلى OKLab ويدع مقارنة إقليدية مربعة مجردة تقود مسح اللوحة

ماذا يضمن NearestIndexedColor؟

يضمن NearestIndexedColor إجابة حتمية للقراءة فقط: تحويل واحد للمدخل، ومسح ثابت واحد فوق 56 مدخلًا مخزنًا مؤقتًا، وأدنى مؤشر عام متى تساوى مدخلان في القرب. يخزن كل مصنف مؤقتًا الـ ARGB المطبع وإحداثيات OKLab لكل الخانات الفعلية الـ 56 مع عدّاد توليد للوحة. إعادة ضبط اللوحة تعيد بناء المخزن، وتغيير خانة واحدة يحدّث تلك الخانة فقط، واستعلام ضد توليد قديم يعيد False بدل التخمين. يستخدم المسح مقارنة أصغر تمامًا بدءًا من الخانة 8، ولهذا تجيب لوحة تحوي اللون نفسه مرتين دائمًا بالمؤشر الأدنى؛ وهذا يهم حين تقارن ملفين مولدين وتتوقع مخرجات متطابقة بايتيًا. ألفا المدخل تتبع عقدًا ضيقًا: بايت ألفا معدوم يعامل كمعتم، والقيمة شبه الشفافة ترفض بـ ColorIndex مساوٍ 0 وPaletteSlot مساوٍ -1، لأن مداخيل اللوحة لا تحمل ألفا. كاتب التعبئات والحدود في المحرك Classic يحوّل RGB وألوان السمات إلى مؤشر بالروتين نفسه لمطابقة OKLab وقت الحفظ، فتتفق الـ API والملف المخزن على الخانة التي يهبط إليها لون

var
  Match: TXLSNearestIndexedColorMatch;
begin
  if Workbook.NearestIndexedColor($FF000033, Match) then
  begin
    // Match.ColorIndex تساوي 11 وMatch.PaletteSlot تساوي 18 وMatch.ARGB تساوي $FF000080
    if not Match.ExactMatch then
      LogApproximation(Match.InputARGB, Match.ARGB, Match.DistanceSquared);
  end;
end;

كيف يُدخل BuildBiffPalettePlan الألوان الحقيقية في 56 خانة؟

يحسب BuildBiffPalettePlan مقترحًا كاملًا لكل الخانات الـ 56 دون أن يلمس المصنف، فيمكنك فحصه أو تسجيله أو التخلص منه. يستدعي المخطط أولًا ScanIndexedColorUsage: أي خانة يشير إليها خط أو تعبئة أو حدود أو تنسيق شرطي أو شكل أو تعليق أو خط شبكة ورقة عمل بمؤشر تُقفل، لأن تغيير مدخل في اللوحة يعيد تلوين كل مستهلك لذلك المؤشر دفعة واحدة. الأهداف هي ألوان RGB المباشرة وألوان السمات المحسومة من الخطوط والتعبئات والحدود والأنماط التفاضلية وأشرطة البيانات ومقاييس الألوان. وكل هدف يوزن بالأكبر بين عدد مرات إشارته المرئية وعدد تعريفاته، والتنسيق الشرطي يحسب الخلايا التي تغطيها نطاقاته، فاللون المرسوم على عمود كامل يفوق المستخدم في تعليق واحد. ثم يجري الإسناد بترتيب ثابت:

  • الخانات المقفلة تحتفظ بلونها المصدر بلا شروط
  • الهدف الموجود سلفًا في اللوحة يبقى عند أدنى خانة مطابقة له وتصير تلك الخانة مثبتة
  • إن اتسعت الخانات الحرة للأهداف الفريدة المتبقية، نالت كل واحدة خانة مطابقة تمامًا، تسند بترتيب ARGB تصاعديًا
  • وإلا يضبط Quantized، وتُبذر كل خانة حرة بالهدف الذي تساوي مسافته إلى أقرب مركز موجود مضروبةً في وزنه أكبر ما يمكن، وتمر حتى 16 جولة من k-means موزونة بالتكرار في OKLab تحرك المراكز الحرة فقط حتى تتوقف الإسنادات عن التغير

كن صريحًا مع نفسك بشأن ما يقدمه مسار الفيضان. التجميع تحسين محلي محدود لا أمثلية عالمية، وتنتهي الخانة الحرة حاملة مركزًا ناقلًا تحول مرة أخرى إلى sRGB مع القطع إلى المدى، وقد يكون ذلك لونًا لم تستخدمه أي خلية حرفيًا. ما تحصل عليه فعلًا هو التكرارية: المصنف نفسه يعطي الخطة نفسها دائمًا، وتبلغ الخطة عن ضررها بنفسها عبر WeightedError وMaxDistanceSquared وExactTargetWeight وTotalTargetWeight، فيستطيع عمل دفعي أن يرفض الحفظ حين يصير التقريب أغظ من دليل هوية العلامة

مسار لوحة HotXLS لمصنف بالألوان الحقيقية: يقفل ScanIndexedColorUsage كل خانة يشير إليها خط أو تعبئة أو حدود أو تنسيق شرطي أو شكل أو تعليق أو خط شبكة، ويسند BuildBiffPalettePlan الألوان المطابقة بترتيب ARGB تصاعديًا أو يشغل حتى 16 جولة من k-means موزونة بالتكرار في OKLab، ويتحقق ApplyBiffPalettePlan من التوليد ومن بصمة FNV-1a قبل الكتابة
التخطيط للقراءة فقط وقابل للتكرار، وتبلغ الخطة عن ضررها عبر WeightedError وMaxDistanceSquared، وتُرفض الخطة القديمة واللوحة بلا مساس لأن الخطط عمليًا ذات استخدام واحد
var
  Plan: TXLSBiffPalettePlan;
  I: Integer;
begin
  Plan := Workbook.BuildBiffPalettePlan;   // للقراءة فقط
  if Plan.Quantized and (Plan.MaxDistanceSquared > MaxAcceptedError) then
    raise Exception.Create('Too many distinct colors for a BIFF8 palette');
  for I := 0 to High(Plan.Slots) do
    if Plan.Slots[I].Changed then
      LogSlot(Plan.Slots[I].ColorIndex, Plan.Slots[I].SourceARGB,
        Plan.Slots[I].TargetARGB);
  if not Workbook.ApplyBiffPalettePlan(Plan) then
    raise Exception.Create('The palette changed after planning');
end;

كيف يرفض ApplyBiffPalettePlan خطة قديمة؟

يتحقق ApplyBiffPalettePlan من الخطة بأكملها قبل أن يكتب خانة واحدة، ويعيد False واللوحة بلا مساس إن خالف شيء ما المصنف الحالي. تحمل الخطة SourcePaletteGeneration وSourcePaletteHash، وهي بصمة FNV-1a بعرض 64 بت فوق الألوان المصدرية الـ 56؛ ويعيد التحقق فحص كل مؤشر عام وفعلي، وكل لون مصدري، وأنه لا توجد خانة مقفلة موسومة كمتغيرة، وعددَي المقفل والمتغير، وأن كل هدف معتم. أي تغيير فعلي في اللوحة بينهما، بما فيه تطبيق سابق ناجح للخطة ذاتها، يجعل الخطة قديمة، فالخطط عمليًا ذات استخدام واحد. الخطة الصحيحة بلا خانات متغيرة تنجح دون تقديم التوليد، والتغيير الحقيقي يدفع التوليد مرة واحدة ويعيد بناء مطابِق OKLab مرة واحدة، في المحرك Classic بإعادة كتابة مصفوفة اللوحة المثبتة وفي محرك XLSX باستبدال قائمة تجاوز ألوان مفهرسة معدّة سلفًا

تشغيل الميزة في حفظ BIFF8 وتحويل XLSX إلى XLS

القيمة الافتراضية للخاصية BiffPaletteSavePolicy هي xbpsPreserve، فترقية HotXLS لا تعيد كتابة لوحة أحد خلف ظهره قط. وضبطها على xbpsOptimizeTrueColors يجعل مصنف Classic يبني خطة جديدة ويطبقها داخل SaveAs، لكن فقط حين يكون تنسيق الهدف xlExcel97؛ وكتابات BIFF5 وCSV وHTML وPDF وXLSX وغيرها تتجاهل الضبط. بعد حفظ ناجح تبقى اللوحة المحسنة في نموذج المصنف، فترى الاستعلامات والحفظ اللاحقة mapping نفسه. وإن فشل الحفظ أو ألغي، استُرجعت الألوان الـ 56 الأصلية والتوليد الأصلي. أما مصادر XLSX فيبني SaveXLSXWorkbookAsXLS في lxXlsxExport خطة واحدة من المصنف المحمل ويكتبها في لوحة الوجهة قبل تحويل أي نمط، وهي الجسر الحتمي الذي تمارسته تجربة منضدة تدقيق المصنفات وتحويلها. وتعبر ألوان السمات المخطط ذاته بعد حسم درجتها إلى RGB؛ وإن كنت تفضل إبقاء السمات حية في تعبئات الرسوم البيانية، فمقال تعبئات رسوم GelFrame بألوان السمة يغطي كيف يخزن XLS الثنائي مؤشر مخطط بدل لون مسطّح

// مصنف Classic: تفعيل اختياري، BIFF8 فقط
Workbook.BiffPaletteSavePolicy := xbpsOptimizeTrueColors;
if Workbook.SaveAs('report.xls', xlExcel97) <> 1 then
  HandleSaveFailure;   // اللوحة استُرجعت سلفًا

// من نموذج XLSX إلى BIFF8 بخطة لوحة حتمية واحدة
XWorkbook := TXLSXWorkbook.Create;
try
  if XWorkbook.Open('report.xlsx') = 1 then
    SaveXLSXWorkbookAsXLS(XWorkbook, 'report.xls');
finally
  XWorkbook.Free;
end;

تعمل واجهات لوحة الألوان في HotXLS بالطريقة نفسها على IXLSWorkbook وTXLSXWorkbook، من Delphi وC++Builder على السواء. نزّل النسخة التجريبية ووجهها إلى أكثر جداولك ألوانًا من صفحة مكوّن HotXLS Excel لدلفي