Bài viết kỹ thuật

Bảng màu 56 màu BIFF8: ánh xạ màu OKLab trong HotXLS

HotXLS đưa màu RGB và màu theme tùy ý lên bảng màu BIFF8 56 slot qua hai lớp: NearestIndexedColor tìm mục bảng màu hiện có gần nhất theo cảm nhận trong không gian OKLab, còn BuildBiffPalettePlan cùng ApplyBiffPalettePlan ghi đè các slot bảng màu còn trống để một workbook true-color sống sót qua lần lưu về XLS cổ điển. Tình huống tác động lúc nào cũng là cùng một ticket hỗ trợ. Ai đó dựng báo cáo trong XLSX với header xanh navy chuẩn thương hiệu và điểm nhấn teal dịu, lưu thành .xls cho một chương trình cũ tiêu thụ, rồi header quay về đen hẳn còn màu teal hóa thành turquoise chói lóa. Không có gì crash, không cảnh báo nào vang lên. Model màu của định dạng cũ đơn giản là không chứa nổi những gì định dạng mới mô tả, và thư viện buộc phải chọn một cái gì đó

Vì sao một file XLS chỉ chứa được 56 màu?

Vì định dạng ô của BIFF8 không bao giờ lưu giá trị RGB: font, fill và border mang một color index, và record Palette toàn workbook ($0092, [MS-XLS] §2.4.188) cung cấp đúng 56 mục RGB đặc cho các chỉ mục 8 đến 63. Các chỉ mục 0 đến 7 là bản sao cứng của tám màu cơ bản, còn các giá trị trên 63 chẳng phải màu gì cả mà là token kiểu system foreground, system background và chart text. HotXLS phơi bảng màu ra ngoài qua một ColorIndex công khai chạy từ 1 đến 56, tức chỉ mục vật lý trừ đi 7, và ResolveIndexedColor giữ ba sơ đồ đánh số tách rời nhau qua TXLSIndexedColorSpace: xicsPublicColorIndex cho các giá trị API 1..56, xicsBiffIcv cho chỉ mục thô trên đĩa, được kiểm chứng chống tập con IcvFont, IcvXF hay IcvChart tương ứng với role bạn truyền vào, và xicsOoxmlIndexed, nơi 64 và 65 nghĩa là system foreground và background

HotXLS giữ ba sơ đồ màu indexed tách rời qua TXLSIndexedColorSpace: giá trị icv BIFF thô, 0 đến 7 cố định cho tám màu cơ bản, 56 slot bảng màu 8 đến 63 của record Palette $0092, các token trên 63 như system foreground, ColorIndex công khai 1 đến 56 lệch đi trừ 7, và xicsOoxmlIndexed nơi 64 và 65 nghĩa là system foreground và background
Cùng một color index mang nghĩa số khác nhau trong từng sơ đồ, nên HotXLS điều mọi giá trị đi qua ResolveIndexedColor thay vì để một token BIFF thô giả danh một ColorIndex công khai
var
  Res: TXLSIndexedColorResolution;
begin
  // $40 là một token icv của BIFF, không phải slot bảng màu
  Workbook.ResolveIndexedColor($40, xicsBiffIcv, Res);
  case Res.Kind of
    xickPalette:   UseArgb(Res.ARGB);   // slot bảng màu, nếu phân giải được
    xickAutomatic,
    xickSystem:    UseSystemColor(Res.SystemColorRole);
    xickInvalid:   RejectToken(Res.RawIndex);
  end;
end;

Hãy để ý ví dụ rẽ theo Res.Kind và bỏ qua giá trị trả về Boolean. ResolveIndexedColor chỉ trả True khi nó lấy được một ARGB cụ thể, và overload ngắn không bao giờ đọc desktop Windows, nên một token automatic hay system hoàn toàn chính đáng khi trả False trong khi vẫn được xếp loại xickSystem. HotXLS từng vấp phải chuyện này ngay trong serializer workbook của chính mình: code coi False là “không có màu” sẽ âm thầm vứt bỏ ý nghĩa Automatic và System của token. Nếu bạn cần giá trị RGB thật cho những token đó, hãy gọi overload dài và đưa vào một callback TXLSTryResolveSystemColor áp chính sách UI, export hay headless của riêng bạn

Vì sao HotXLS khớp màu trong OKLab thay vì RGB?

Vì giá trị kênh sRGB đã được mã hóa gamma, nên khoảng cách Euclid trong RGB không phản ánh cái mắt người thật sự thấy, và sai số tệ nhất lại rơi đúng vào các tông tối, bão hòa mà bảng màu doanh nghiệp yêu thích. Lấy màu xanh đậm $000033 làm ví dụ. Trong RGB khoảng cách tới đen là 51 và tới mục navy mặc định $000080 là 77, nên một bộ khớp màu RGB tự tin sơn header của bạn thành đen. Trong OKLab khoảng cách bình phương vào khoảng 0.0312 tới đen và 0.0235 tới navy, và HotXLS chọn navy, ColorIndex 11 ở slot vật lý 18; đúng ca đó được ghim trong bộ test cho cả engine Classic lẫn engine XLSX. Phép chuyển đổi bên trong ArgbToOklab tuyến tính hóa từng kênh sRGB, nhân với ma trận LMS của OKLab, lấy căn bậc ba rồi chiếu xuống L, a và b, sau đó một khoảng cách Euclid bình phương trơn là một proxy hợp lý cho độ khác biệt cảm nhận. OKLab không phải CIEDE2000 và cũng không giả vờ thế, nhưng nó không có các hiệu chỉnh hue theo đoạn, tốn chỉ vài phép nhân cho mỗi màu, và đủ ổn định để dẫn một vòng lặp clustering, đó mới là chỗ nó thật sự trả giá trị công

HotXLS khớp màu xanh đậm $000033 lên bảng màu thế nào: khoảng cách Euclid trong RGB mã hóa gamma đo được 51 tới đen và 77 tới navy rồi sẽ sơn header thành đen, trong khi khoảng cách bình phương qua ArgbToOklab là 0.0312 và 0.0235 để NearestIndexedColor chọn navy, ColorIndex 11 ở slot vật lý 18
Giá trị kênh mã hóa gamma khiến khoảng cách RGB là một proxy kém cho cái mắt người thấy, nên HotXLS chuyển đổi một lần sang OKLab và để phép so sánh Euclid bình phương trơn dẫn cuộc quét bảng màu

NearestIndexedColor bảo đảm điều gì?

NearestIndexedColor bảo đảm một câu trả lời tất định và chỉ đọc: một lần chuyển đổi đầu vào, một lần quét cố định qua 56 mục đã cache, và chọn chỉ mục công khai thấp nhất bất cứ khi nào hai mục gần bằng nhau. Mỗi workbook cache ARGB đã chuẩn hóa cùng tọa độ OKLab của cả 56 slot vật lý kèm một bộ đếm generation của bảng màu. Một lần reset bảng màu dựng lại cache, một thay đổi đơn slot chỉ cập nhật đúng slot đó, và một truy vấn gặp generation cũ sẽ trả False thay vì đoán mò. Lần quét dùng so sánh nhỏ hơn nghiêm ngặt bắt đầu từ slot 8, vì thế một bảng màu chứa cùng một màu hai lần luôn trả lời bằng chỉ mục thấp hơn; điều đó có ý nghĩa khi bạn diff hai file được sinh ra và kỳ vọng đầu ra giống nhau từng byte. Alpha đầu vào theo một hợp đồng hẹp: byte alpha bằng 0 được coi là đặc, một giá trị trong suốt một phần bị từ chối với ColorIndex 0 và PaletteSlot -1, vì các mục bảng màu không có alpha. Các writer fill và border của engine Classic chuyển RGB và màu theme thành chỉ mục bằng đúng routine khớp OKLab đó lúc lưu, nên API và file lưu xuống thống nhất với nhau về chỗ một màu đáp xuống

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 nhồi true color vào 56 slot thế nào?

BuildBiffPalettePlan tính một đề xuất hoàn chỉnh cho cả 56 slot mà không đụng vào workbook, nên bạn có thể inspect, log hay vứt bỏ nó. Planner đầu tiên gọi ScanIndexedColorUsage: bất kỳ slot nào được một font, fill, border, conditional format, shape, comment hay gridline của worksheet tham chiếu theo chỉ mục đều bị khóa, vì đổi một mục bảng màu là tô lại màu cho mọi bên tiêu thụ chỉ mục đó ngay một lúc. Target là các màu RGB trực tiếp và màu theme đã phân giải từ font, fill, border, differential style, data bar và color scale. Mỗi target được gán trọng số theo cái lớn hơn giữa số tham chiếu được render và số lần định nghĩa, và một conditional format đếm cả các ô mà dải của nó phủ, nên một màu tô trải cả cột được tính nặng hơn một màu dùng trong đúng một chú thích. Việc xếp chỗ sau đó chạy theo một thứ tự cố định:

  • Các slot bị khóa giữ nguyên màu nguồn của chúng vô điều kiện
  • Một target đã có sẵn trong bảng màu được giữ lại ở slot khớp thấp nhất của nó và slot đó trở thành cố định
  • Nếu các target duy nhất còn lại vừa vào các slot trống, mỗi cái nhận một slot đúng chính xác, phân theo thứ tự ARGB tăng dần
  • Nếu không, Quantized được bật, mỗi slot trống được gieo bằng target mà khoảng cách từ nó tới center hiện có gần nhất, nhân với trọng số, là lớn nhất, và tối đa 16 vòng k-means có trọng số tần suất trong OKLab chỉ dời các center trống cho tới khi việc phân bổ ngừng đổi

Hãy tự trung thực về những gì đường tràn ra mang lại. Phép clustering là một tối ưu cục bộ có chặn, không phải tối ưu toàn cục, và một slot trống cuối cùng giữ một centroid được chuyển ngược về sRGB kèm clamp, tức có thể là một màu chẳng ô nào từng dùng nguyên vẹn. Cái bạn thật sự nhận được là tính lặp lại: cùng một workbook luôn cho ra cùng một plan, và plan tự báo cáo mức tổn thất của nó qua WeightedError, MaxDistanceSquared, ExactTargetWeight và TotalTargetWeight, nên một batch job có thể từ chối lưu khi mức xấp xỉ thô quá so với bộ nhận diện thương hiệu

Pipeline bảng màu của HotXLS cho một workbook true-color: ScanIndexedColorUsage khóa mọi slot được font, fill, border, conditional format, shape, comment hay gridline tham chiếu, BuildBiffPalettePlan xếp màu chính xác theo thứ tự ARGB tăng dần hoặc chạy tới 16 vòng k-means có trọng số tần suất trong OKLab, và ApplyBiffPalettePlan kiểm chứng generation cùng hash FNV-1a trước khi ghi
Lập kế hoạch chỉ đọc và lặp lại được, plan tự báo mức tổn thất qua WeightedError và MaxDistanceSquared, còn một plan cũ bị từ chối với bảng màu nguyên vẹn vì plan thực chất chỉ dùng một lần
var
  Plan: TXLSBiffPalettePlan;
  I: Integer;
begin
  Plan := Workbook.BuildBiffPalettePlan;   // chỉ đọc
  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 từ chối một plan cũ ra sao?

ApplyBiffPalettePlan kiểm chứng toàn bộ plan trước khi ghi đúng một slot, và trả False với bảng màu nguyên vẹn nếu bất cứ thứ gì lệch với workbook hiện tại. Plan mang theo SourcePaletteGeneration và SourcePaletteHash, một hash FNV-1a 64-bit trên 56 màu nguồn; bước kiểm chứng còn soát lại mọi chỉ mục công khai và vật lý, mọi màu nguồn, việc không slot nào bị khóa lại bị đánh dấu changed, các con số locked và changed, và việc mọi target đều đặc. Bất kỳ thay đổi bảng màu hiệu lực nào xen vào, kể cả một lần áp cùng plan đó thành công trước đó, đều khiến plan già đi, nên plan thực chất chỉ dùng một lần. Một plan hợp lệ không có slot nào changed sẽ thành công mà không đẩy generation, còn một thay đổi thật bump generation đúng một lần và dựng lại bộ khớp OKLab đúng một lần, trên engine Classic bằng cách ghi lại mảng bảng màu cố định, trên engine XLSX bằng cách thay vào một danh sách indexed-color override đã chuẩn bị sẵn

Bật tính năng cho lần lưu BIFF8 và chuyển đổi XLSX sang XLS

Thuộc tính BiffPaletteSavePolicy mặc định là xbpsPreserve, nên nâng cấp HotXLS không bao giờ ghi lại bảng màu của ai sau lưng họ. Đặt nó thành xbpsOptimizeTrueColors khiến một workbook Classic dựng và áp một plan mới ngay trong SaveAs, nhưng chỉ khi định dạng đích là xlExcel97; các writer BIFF5, CSV, HTML, PDF, XLSX và còn lại đều bỏ qua thiết lập này. Sau một lần lưu thành công, bảng màu đã tối ưu ở lại trong model workbook, nên các truy vấn và lần lưu sau nhìn thấy cùng một ánh xạ. Nếu lần lưu thất bại hay bị hủy, 56 màu gốc cùng generation gốc được khôi phục. Với nguồn XLSX, SaveXLSXWorkbookAsXLS trong lxXlsxExport dựng một plan từ workbook đã nạp và ghi nó vào bảng màu đích trước khi bất kỳ style nào được chuyển đổi, đó chính là cây cầu tất định mà demo workbench kiểm toán và chuyển đổi workbook đi qua. Màu theme đi qua cùng planner đó sau khi tint của chúng được phân giải thành RGB; nếu bạn thà giữ theme sống động trong chart fill, bài về GelFrame theme color chart fills trình bày cách XLS nhị phân lưu một scheme index thay vì một màu đã làm phẳng

// Workbook Classic: chủ động bật, chỉ dành cho BIFF8
Workbook.BiffPaletteSavePolicy := xbpsOptimizeTrueColors;
if Workbook.SaveAs('report.xls', xlExcel97) <> 1 then
  HandleSaveFailure;   // bảng màu đã được khôi phục

// model XLSX sang BIFF8 với một palette plan tất định
XWorkbook := TXLSXWorkbook.Create;
try
  if XWorkbook.Open('report.xlsx') = 1 then
    SaveXLSXWorkbookAsXLS(XWorkbook, 'report.xls');
finally
  XWorkbook.Free;
end;

Các API bảng màu của HotXLS hoạt động như nhau trên IXLSWorkbook lẫn TXLSXWorkbook, từ Delphi cũng như C++Builder. Tải bản dùng thử và trỏ nó vào bảng tính nhiều màu nhất của bạn từ trang HotXLS Delphi Excel component