HotXLSは任意のRGBカラーやテーマカラーを2層構造でBIFF8の56色パレットに割り付けます。NearestIndexedColorがOKLab空間で知覚的に最も近い既存パレットエントリを探し、BuildBiffPalettePlanとApplyBiffPalettePlanが空きパレットスロットを書き換えることで、トゥルーカラーのワークブックもクラシックXLSへの保存を生き延びられる仕組みです。きっかけはいつも同じサポートチケットです。誰かが企業カラーのネイビーの見出しと落ち着いたティールのアクセントを使ったレポートをXLSXで組み立て、レガシーな読み手向けに.xlsで保存すると、見出しは真っ黒に戻り、ティールはけたたましいターコイズに化けます。クラッシュもせず、警告も出ません。旧形式のカラーモデルには新形式が記述した内容を保持する容量が単純になく、ライブラリ側は何かを選ばざるを得なかったのです
XLSファイルの色が56色までしか保持できない理由
BIFF8のセル書式はRGB値を一切格納しないためです。フォント、塗りつぶし、罫線が運ぶのはカラーコードで、ワークブック全体で1つのPaletteレコード($0092、[MS-XLS] §2.4.188)がインデックス8から63に対してちょうど56個の不透明RGBエントリを供給します。インデックス0から7は8つの基本色の固定コピーであり、63を超える値は色ではなく、システム前景色・システム背景色・チャートテキストのようなトークンです。HotXLSはパレットを1から56の公開ColorIndexとして露出します。これは物理インデックスから7を引いた値です。ResolveIndexedColorはTXLSIndexedColorSpaceで3つの番号体系を混在させません。1..56のAPI値にはxicsPublicColorIndex、ディスク上の生インデックスにはxicsBiffIcv(渡したロールに応じてIcvFont、IcvXF、IcvChartのいずれかのサブセットに対して検証されます)、そして64と65がシステム前景色・背景色を意味するxicsOoxmlIndexedです
var
Res: TXLSIndexedColorResolution;
begin
// $40はパレットスロットではなくBIFF icvトークン
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で分岐し、戻り値のBooleanを無視している点に注意してください。ResolveIndexedColorがTrueを返すのは具体的なARGBを取得できたときだけで、短い方のオーバーロードはWindowsデスクトップを読みに行かないため、automaticやsystemのトークンはxickSystemに分類されたまま正当にFalseを返します。HotXLS自身もワークブックシリアライザでこれに引っかかりました。Falseを「色なし」と扱うコードは、トークンのAutomaticとSystemの意味を静かに捨ててしまいます。これらのトークンに実際のRGB値が必要なら、長い方のオーバーロードを呼び、自前のUI・エクスポート・ヘッドレスポリシーを適用するTXLSTryResolveSystemColorコールバックを渡してください
HotXLSがRGBではなくOKLabで色を比較する理由
sRGBのチャンネル値はガンマエンコードされているため、RGB空間のユークリッド距離は人が知覚する差を追いかけてくれず、誤差は企業カラーパレットが好む暗く彩度の高いトーンで最悪になります。暗い青$000033を例に取りましょう。RGBでは黒までの距離が51、デフォルトのネイビーエントリ$000080までの距離が77なので、RGBでマッチングすると見出しは迷わず黒く塗られます。OKLabでは距離の2乗は黒まで約0.0312、ネイビーまで約0.0235となり、HotXLSはネイビー、つまり物理スロット18のColorIndex 11を選びます。このケースはまさにそのまま、ClassicエンジンとXLSXエンジンの両方についてテストスイートにピン留めされています。ArgbToOklab内部の変換は、各sRGBチャンネルを線形化し、OKLabのLMS行列を適用し、立方根を取ってL、a、bへ射影します。その後の自明なユークリッド距離の2乗は、知覚的な差として妥当な近似になります。OKLabはCIEDE2000ではなく、そう見せかけるつもりもありません。しかし分段形式の色相補正を持たず、色1つあたり数回の乗算で済み、クラスタリングのループを回せるほど安定しています。OKLabが本領を発揮するのはまさにそこです
NearestIndexedColorが保証するもの
NearestIndexedColorが保証するのは決定論的で読み取り専用の答えです。入力変換1回、キャッシュ済み56エントリの固定走査1回、そして2つのエントリが同じ近さなら常に小さい方の公開インデックスを返すこと。各ワークブックは正規化済みARGBと56物理スロット全部のOKLab座標を、パレット生成カウンタと一緒にキャッシュします。パレットのリセットでキャッシュを再構築し、単一スロットの変更ではそのスロットだけを更新し、古い生成に対する問い合わせは推測の代わりにFalseを返します。走査はスロット8から始まる厳密な小なり比較を使うため、同じ色が2回入ったパレットは常に小さい方のインデックスで答えます。生成ファイルを2つdiffしてバイト単位の同一出力を期待するとき、これが効いてきます。入力アルファの契約は狭いものです。アルファバイト0は不透明として扱われ、半透明の値はColorIndex 0とPaletteSlot -1で拒否されます。パレットエントリにはアルファがないためです。Classicエンジンの塗りつぶしと罫線のライターは保存時に、同じOKLabマッチングルーチンでRGBカラーやテーマカラーをインデックスへ変換するので、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を呼びます。フォント、塗りつぶし、罫線、条件付き書式、図形、コメント、ワークシートのグリッド線がインデックスで参照しているスロットはすべてロックされます。パレットエントリを1つ変えると、そのインデックスの消費者すべての色が一度に変わるからです。ターゲットになるのは、フォント、塗りつぶし、罫線、差分スタイル、データバー、カラースケールからの直接RGBと解決済みテーマカラーです。各ターゲットは描画時の参照数と定義数の大きい方で重み付けされ、条件付き書式はその範囲がカバーするセル数を数えるため、列全体に塗られた色は1つのコメントで使われた色より重くなります。配置は固定の順序で進みます
- ロック済みスロットは無条件にソースの色を保持する
- パレットに既に存在するターゲットは最も小さい一致スロットに保持され、そのスロットは固定される
- 残りのユニークなターゲットが空きスロットに収まるなら、各ターゲットにARGBの昇順で正確なスロットを割り当てる
- そうでなければ
Quantizedが立ち、各空きスロットには「既存の最近傍中心までの距離×重み」が最大のターゲットをシードとして入れ、OKLab空間の頻度重み付きk-meansを最大16ラウンド回して、割り当てが変化しなくなるまで空き中心だけを動かす
オーバーフローの経路が何を届けてくれるのか、そこは正直に見極めましょう。クラスタリングは有界な局所最適化であって大域最適ではなく、空きスロットが最終的に持つのはクランプ付きでsRGBに戻したセントロイドであり、どのセルもそのままでは使っていない色になり得ます。それでも手に入るのは再現性です。同じワークブックは常に同じプランを生み出し、プラン自身がWeightedError、MaxDistanceSquared、ExactTargetWeight、TotalTargetWeightで被害の程度を報告してくれるので、近似がブランドガイドラインに耐えない粗さになったとき、バッチジョブは保存を拒否できます
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は単一のスロットを書き込む前にプラン全体を検証し、現在のワークブックと何1つでも食い違いがあればパレットを触らないままFalseを返します。プランはSourcePaletteGenerationとSourcePaletteHash、つまり56個のソースカラーに対する64ビットFNV-1aハッシュを運んでおり、検証ではさらにすべての公開・物理インデックス、すべてのソースカラー、ロック済みスロットが変更済みマークされていないこと、ロック数と変更数、そしてすべてのターゲットが不透明であることを再チェックします。その間に実効的なパレット変更があれば、同じプランの以前の適用が成功したケースも含めて、プランは古くなります。実質的にプランは1回限りです。変更スロットのない有効なプランは生成カウンタを進めずに成功し、実際の変更があった場合は生成カウンタを1回だけ進めてOKLabマッチャーを1回だけ再構築します。Classicエンジンでは固定パレット配列の書き換えで、XLSXエンジンでは用意済みのインデックスカラー上書きリストへの差し替えで行われます
BIFF8保存とXLSXからXLSへの変換で有効にする
BiffPaletteSavePolicyプロパティのデフォルトはxbpsPreserveです。HotXLSをアップグレードしても、誰のパレットも密かに書き換わることはありません。xbpsOptimizeTrueColorsに設定すると、ClassicワークブックはSaveAs内で新しいプランを構築して適用します。ただし対象形式がxlExcel97のときだけです。BIFF5、CSV、HTML、PDF、XLSXその他のライターはこの設定を無視します。保存に成功すると最適化済みパレットはワークブックモデルに残るため、後続の問い合わせや保存は同じマッピングを見ます。保存が失敗またはキャンセルされた場合は、元の56色と元の生成カウンタが復元されます。XLSXソースについては、lxXlsxExportのSaveXLSXWorkbookAsXLSがロード済みワークブックからプランを1つ構築し、スタイルの変換が始まる前に出力先パレットへ書き込みます。これがワークブック監査・変換ワークベンチのデモが演じる決定論的な橋渡しです。テーマカラーはティントをRGBに解決した後で同じプランナーを通ります。チャートの塗りつぶしでテーマを生きたまま保ちたい場合は、GelFrameのテーマカラーチャート塗りつぶしの記事が、バイナリXLSが平坦化された色ではなくスキームインデックスを格納する仕組みを扱っています
// Classicワークブック:オプトイン、BIFF8限定
Workbook.BiffPaletteSavePolicy := xbpsOptimizeTrueColors;
if Workbook.SaveAs('report.xls', xlExcel97) <> 1 then
HandleSaveFailure; // パレットはすでに復元済み
// 決定論的なパレットプラン1つでXLSXモデルをBIFF8へ
XWorkbook := TXLSXWorkbook.Create;
try
if XWorkbook.Open('report.xlsx') = 1 then
SaveXLSXWorkbookAsXLS(XWorkbook, 'report.xls');
finally
XWorkbook.Free;
end;
HotXLSのパレットAPIはIXLSWorkbookとTXLSXWorkbookのどちらでも同じように動作し、DelphiからもC++Builderからも使い方は変わりません。トライアル版をダウンロードして、HotXLS Delphi Excelコンポーネントのページから、いちばん色数の多いスプレッドシートに向けてみてください