HotXLS จัดการสี RGB และ theme color แบบใดก็ได้ โดย map ลงพาเลตต์สี 56 ช่องของ BIFF8 เป็นสองชั้น: NearestIndexedColor หา entry เดิมในพาเลตต์ที่ใกล้เคียงทางการมองที่สุดในปริภูมิ OKLab ส่วน BuildBiffPalettePlan คู่กับ ApplyBiffPalettePlan เขียนช่องว่างของพาเลตต์ใหม่เพื่อให้เวิร์กบุ๊ก true-color รอดจากการ save เป็น XLS คลาสสิก สิ่งที่จุดปัญหาขึ้นมาทุกครั้งคือ support ticket แบบเดิมเสมอ มีคนสร้างรายงานใน XLSX ด้วยหัวตารางสีน้ำเงินเข้มองค์กรและจุดเน้นสี teal อ่อน save เป็น .xls ให้โปรแกรมรุ่นเก่าใช้ แล้วหัวตารางกลับมาเป็นดำสนิท ขณะที่ teal กลายเป็น turquoise ฉูดฉาด ไม่มีอะไร crash และไม่มี warning โผล่มา โมเดลสีของฟอร์แมตเก่าแค่ไม่มีที่พอสำหรับสิ่งที่ฟอร์แมตใหม่บรรยายไว้ และไลบรารีก็ต้องเลือกบางอย่างสักอย่าง
ทำไมไฟล์ XLS ถึงเก็บสีได้แค่ 56 สี
เพราะฟอร์แมตเซลล์ของ BIFF8 ไม่เคยเก็บค่า RGB ตรง ๆ เลย: font, fill และ border พก color index และ Palette record ระดับเวิร์กบุ๊ก ($0092, [MS-XLS] §2.4.188) เป็นคนจ่าย entry RGB แบบ opaque พอดี 56 ตัวให้ index 8 ถึง 63 index 0 ถึง 7 เป็นสำเนาคงที่ของแปดสีพื้นฐาน และค่าที่สูงกว่า 63 ไม่ใช่สีเลย แต่เป็น token อย่าง system foreground, system background และ chart text HotXLS เปิดพาเลตต์ออกไปผ่าน ColorIndex สาธารณะคือ 1 ถึง 56 ซึ่งก็คือ physical index ลบด้วย 7 และ ResolveIndexedColor กั้นสามระบบเลขนี้แยกจากกันผ่าน TXLSIndexedColorSpace: xicsPublicColorIndex สำหรับค่า API 1..56, xicsBiffIcv สำหรับ index ดิบบนดิสก์ ซึ่งจะถูกตรวจกับชุดย่อย IcvFont, IcvXF หรือ IcvChart ตาม role ที่คุณส่งเข้าไป และ xicsOoxmlIndexed ที่ 64 กับ 65 หมายถึง system foreground กับ background
var
Res: TXLSIndexedColorResolution;
begin
// $40 เป็น token icv ของ BIFF ไม่ใช่ช่องพาเลตต์
Workbook.ResolveIndexedColor($40, xicsBiffIcv, Res);
case Res.Kind of
xickPalette: UseArgb(Res.ARGB); // ช่องพาเลตต์ ถ้า resolve ได้
xickAutomatic,
xickSystem: UseSystemColor(Res.SystemColorRole);
xickInvalid: RejectToken(Res.RawIndex);
end;
end;
สังเกตว่าตัวอย่าง switch บน Res.Kind แล้วเมินค่า Boolean ที่คืนกลับมา ResolveIndexedColor คืน True เฉพาะเมื่อได้ ARGB รูปธรรมมาจริง และ overload แบบสั้นไม่เคยไปอ่าน desktop ของ Windows ดังนั้น token แบบ automatic หรือ system จึงคืน False อย่างถูกต้องตามสัญญา ทั้งที่ยังถูกจัดเป็น xickSystem อยู่ HotXLS เจอเรื่องนี้กับ serializer ของเวิร์กบุ๊กตัวเอง: โค้ดที่ถือว่า False คือ "ไม่มีสี" จะทิ้งความหมาย Automatic กับ System ของ token ไปเงียบ ๆ ถ้าคุณต้องการค่า RGB จริงของ token พวกนี้ ให้เรียก overload แบบยาวพร้อม callback TXLSTryResolveSystemColor ที่ใช้นโยบาย UI, export หรือ headless ของคุณเอง
ทำไม HotXLS ถึงเทียบสีใน OKLab แทนที่จะใช้ RGB
เพราะค่า channel ของ sRGB ถูกเก็บแบบ gamma-encoded ระยะแบบ Euclidean ใน RGB จึงตามสิ่งที่ตาคนเห็นไม่ทัน และความคลาดเคลื่อนแย่ที่สุดอยู่ในโทนมืดและอิ่มตัวพอดี ซึ่งเป็นโทนโปรดของพาเลตต์องค์กร ลองเอาสีน้ำเงินเข้ม $000033 มาดู ใน RGB ระยะไปดำคือ 51 และไป entry navy ค่าเริ่มต้น $000080 คือ 77 matcher แบบ RGB จึงลงมือระบายหัวตารางของคุณเป็นดำอย่างมั่นใจ ส่วนใน OKLab ระยะกำลังสองคือประมาณ 0.0312 ไปดำ กับ 0.0235 ไป navy และ HotXLS เลือก navy คือ ColorIndex 11 ที่ physical slot 18; เคสนี้ถูก pin ไว้ในเทสต์ suite ของทั้ง Classic engine และ XLSX engine การแปลงภายใน ArgbToOklab ทำให้ channel sRGB แต่ละตัวเป็น linear ก่อน คูณด้วยเมทริกซ์ LMS ของ OKLab ถอด cube root แล้วโปรเจกต์ลง L, a และ b จากนั้นระยะ Euclidean กำลังสองธรรมดาก็เป็นตัวแทนที่ใช้ได้ของความต่างที่ตารู้สึก OKLab ไม่ใช่ CIEDE2000 และก็ไม่ได้อวดว่าเป็น แต่มันไม่มีส่วนแก้ hue แบบ piecewise ต้นทุนต่อสีแค่ไม่กี่การคูณ และนิ่งพอจะขับ clustering loop ซึ่งตรงนั้นแหละที่มันคุ้มค่าจริง ๆ
NearestIndexedColor การันตีอะไร
NearestIndexedColor การันตีคำตอบที่ deterministic และ read-only: แปลง input หนึ่งครั้ง สแกนตายตัวหนึ่งรอบบน entry ที่ cache ไว้ 56 ตัว และเลือก public index ต่ำสุดเสมอเมื่อสอง entry ใกล้เคียงเท่ากัน เวิร์กบุ๊กแต่ละตัว cache ARGB ที่ normalize แล้วพร้อมพิกัด OKLab ของ physical slot ทั้ง 56 ช่องไว้กับตัวนับ palette generation การ reset พาเลตต์จะสร้าง cache ใหม่ทั้งก้อน การแก้ช่องเดียวอัปเดตเฉพาะช่องนั้น และการถามบน generation เก่าจะคืน False แทนที่จะเดา การสแกนใช้การเปรียบเทียบน้อยกว่าแบบเข้มงวดเริ่มจาก slot 8 พาเลตต์ที่มีสีเดียวกันซ้ำสองช่องจึงตอบด้วย index ต่ำกว่าเสมอ เรื่องนี้สำคัญตอนที่คุณ diff ไฟล์ที่สร้างขึ้นสองไฟล์แล้วคาดหวังผลลัพธ์ที่ byte-identical alpha ของ input อยู่ภายใต้สัญญาแคบ ๆ: alpha byte เป็นศูนย์ถือเป็น opaque และค่าที่โปร่งแสงบางส่วนถูกปฏิเสธด้วย ColorIndex 0 กับ PaletteSlot -1 เพราะ entry ในพาเลตต์ไม่มี alpha ตัวเขียน fill กับ border ของ Classic engine แปลง RGB และ theme color เป็น index ตอน save ด้วย routine จับคู่ 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 จัด true color ลง 56 ช่องอย่างไร
BuildBiffPalettePlan คำนวณข้อเสนอครบชุดสำหรับทั้ง 56 ช่องโดยไม่แตะเวิร์กบุ๊กเลย คุณจึงตรวจดู, log หรือทิ้งมันได้ planner เริ่มด้วยการเรียก ScanIndexedColorUsage: ช่องใดก็ตามที่ font, fill, border, conditional format, shape, comment หรือ gridline ของชีตอ้างถึงด้วย index จะถูกล็อก เพราะการแก้ entry หนึ่งในพาเลตต์เปลี่ยนสีของทุกตัวที่ใช้ index นั้นพร้อมกันหมด target คือ RGB ตรง ๆ และ theme color ที่ resolve แล้วจาก font, fill, border, differential style, data bar และ color scale แต่ละ target ถูกถ่วงน้ำหนักด้วยค่าที่มากกว่าระหว่างจำนวน reference ที่ render กับจำนวน definition และ conditional format นับเซลล์ที่ช่วงของมันครอบ สีที่ระบายทั้งคอลัมน์จึงชนะสีที่ใช้ในโน้ตเดียว การจัดวางแล้วเดินตามลำดับคงที่:
- ช่องที่ถูกล็อกคงสีต้นทางของมันไว้โดยไม่มีเงื่อนไข
- target ที่มีอยู่ในพาเลตต์แล้วถูกเก็บไว้ที่ช่องที่ตรงกันต่ำสุด และช่องนั้นกลายเป็น fixed
- ถ้า target ที่ไม่ซ้ำกันที่เหลือพอลงในช่องว่าง แต่ละตัวจะได้ช่องของตัวเองแบบตรงเป๊ะ โดยจัดสรรตามลำดับ ARGB จากน้อยไปมาก
- ไม่งั้น
Quantizedจะถูกตั้ง, แต่ละช่องว่างถูก seed ด้วย target ที่ระยะไป center เดิมที่ใกล้ที่สุดคูณด้วยน้ำหนักของมันแล้วมากที่สุด และ k-means แบบถ่วงความถี่ใน OKLab สูงสุด 16 รอบขยับเฉพาะ center อิสระจนกระทั่งการ assign ไม่เปลี่ยนอีก
จงซื่อสัตย์กับตัวเองว่าเส้นทาง overflow ให้อะไรมา การ clustering นี้เป็น local optimization ที่มีขอบเขต ไม่ใช่คำตอบที่ดีที่สุดระดับ global และช่องว่างหนึ่งจบลงด้วยการถือ centroid ที่แปลงกลับเป็น sRGB พร้อม clamping ซึ่งอาจเป็นสีที่ไม่มีเซลล์ไหนใช้ตรง ๆ เลย สิ่งที่คุณได้จริงคือความทำซ้ำได้: เวิร์กบุ๊กเดิมให้แผนเดิมเสมอ และแผนรายงานความเสียหายของตัวเองผ่าน WeightedError, MaxDistanceSquared, ExactTargetWeight กับ TotalTargetWeight batch job จึงปฏิเสธการ save ได้เมื่อค่าประมาณหยาบเกินไปสำหรับ brand guideline
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 ซึ่งเป็น hash FNV-1a 64-bit บนสีต้นทาง 56 สี การตรวจยังเช็กซ้ำ index สาธารณะกับ physical ทุกตัว, สีต้นทางทุกสี, ว่าไม่มีช่องที่ล็อกถูก mark ว่าเปลี่ยน, จำนวนที่ล็อกกับที่เปลี่ยน และว่า target ทุกตัวเป็น opaque การเปลี่ยนพาเลตต์ที่มีผลจริงใด ๆ ในระหว่างนั้น รวมถึงการ apply แผนเดิมสำเร็จไปแล้วครั้งหนึ่ง ทำให้แผนล้าสมัย แผนจึงเป็นของใช้ครั้งเดียวอย่างแท้จริง แผนที่ valid แต่ไม่มีช่องเปลี่ยนจะสำเร็จโดยไม่เลื่อน generation ส่วนการเปลี่ยนจริงจะเพิ่ม generation หนึ่งครั้งและสร้าง matcher OKLab ใหม่หนึ่งครั้ง ฝั่ง Classic engine โดยเขียน array พาเลตต์คงที่ใหม่ และฝั่ง XLSX engine โดยสลับเข้าเป็นรายการ indexed-color override ที่เตรียมไว้
เปิดใช้กับการ save BIFF8 และการแปลง XLSX เป็น XLS
property BiffPaletteSavePolicy ค่าเริ่มต้นเป็น xbpsPreserve การอัปเกรด HotXLS จึงไม่มีวันเขียนพาเลตต์ของใครทับหลังหลังโดยที่เขาไม่รู้ ถ้าตั้งเป็น xbpsOptimizeTrueColors เวิร์กบุ๊ก Classic จะสร้างแผนใหม่แล้ว apply ภายใน SaveAs แต่เฉพาะเมื่อฟอร์แมตปลายทางเป็น xlExcel97 ส่วน writer อื่น ๆ อย่าง BIFF5, CSV, HTML, PDF และ XLSX ไม่สนใจค่านี้ หลัง save สำเร็จ พาเลตต์ที่ optimize แล้วจะอยู่ต่อในโมเดลเวิร์กบุ๊ก การถามและการ save ครั้งถัด ๆ ไปจึงเห็น mapping เดิม ถ้า save ล้มเหลวหรือถูกยกเลิก สี 56 สีเดิมกับ generation เดิมจะถูกคืนกลับ สำหรับแหล่งเป็น XLSX SaveXLSXWorkbookAsXLS ใน lxXlsxExport สร้างแผนเดียวจากเวิร์กบุ๊กที่โหลดมาแล้วเขียนลงพาเลตต์ปลายทางก่อนสไตล์ใดจะถูกแปลง ซึ่งเป็นสะพานแบบ deterministic ที่ เดโม workbench ตรวจสอบและแปลงเวิร์กบุ๊ก ใช้เดินให้ดู theme color เดินผ่าน planner ตัวเดียวกันหลัง tint ถูก resolve เป็น RGB ถ้าคุณอยากให้ theme ยังมีชีวิตใน chart fill อยู่ บทความเรื่อง GelFrame theme color chart fills อธิบายว่า XLS แบบ binary เก็บ scheme index แทนสีที่แผ่แบนไว้อย่างไร
// เวิร์กบุ๊ก Classic: เลือกเปิดเอง ใช้ได้เฉพาะ BIFF8
Workbook.BiffPaletteSavePolicy := xbpsOptimizeTrueColors;
if Workbook.SaveAs('report.xls', xlExcel97) <> 1 then
HandleSaveFailure; // พาเลตต์ถูกคืนกลับไปแล้ว
// โมเดล XLSX เป็น BIFF8 ด้วยแผนพาเลตต์ deterministic หนึ่งแผน
XWorkbook := TXLSXWorkbook.Create;
try
if XWorkbook.Open('report.xlsx') = 1 then
SaveXLSXWorkbookAsXLS(XWorkbook, 'report.xls');
finally
XWorkbook.Free;
end;
API พาเลตต์ของ HotXLS ทำงานเหมือนกันบน IXLSWorkbook กับ TXLSXWorkbook เรียกได้ทั้งจาก Delphi และ C++Builder โหลดเวอร์ชันทดลองแล้วเอาไปลองกับสเปรดชีตที่มีสีเยอะที่สุดของคุณได้จาก หน้า HotXLS Delphi Excel component