Il blu scuro $000033 sta a 51 unità dal nero e a 77 unità dalla classica voce navy $000080 quando la distanza viene misurata come semplice distanza euclidea sui valori dei canali RGB. Un convertitore che scrive file .xls classici con matching nearest-RGB quindi dipinge di nero una riga di intestazione blu scuro, e il nero è quello che mostra ogni reader che si basa sul solo indice di palette. Il formato non lascia allo scrittore alcun margine: un formato cella memorizza un indice di palette, e la palette contiene 56 voci. Ottenere l'intestazione giusta richiede due cose, una misura di distanza che segua ciò che le persone vedono, e un modo per riscrivere la palette stessa quando un workbook usa più colori distinti di quanti la palette possa contenere
Da dove viene il limite dei 56 colori
Un record BIFF8 di font, fill o bordo non porta mai un valore RGB. Porta un indice di colore, e il record Palette globale del workbook ($0092, [MS-XLS] §2.4.188) fornisce esattamente 56 voci RGB opache per gli indici da 8 a 63. Gli indici da 0 a 7 sono copie fisse degli otto colori base. I valori sopra 63 non sono affatto colori ma token: $40 è il system foreground, $41 il system background, da $4D a $4F sono testo del grafico, sfondo del grafico e il colore neutro del grafico, e $7FFF significa automatic. Excel 2007 aggiunse il record XFExt ($087D, §2.4.355), che può attaccare un RGB pieno o un colore del tema a un formato cella, ma Excel 97–2003 e qualsiasi parser che legge solo il record XF continuano a non vedere nulla oltre l'indice
HotXLS espone la palette tramite un ColorIndex pubblico da 1 a 56, che è l'indice fisico meno 7. Tre schemi di numerazione sono in gioco, e ResolveIndexedColor li tiene separati tramite TXLSIndexedColorSpace: xicsPublicColorIndex per i valori API 1..56, xicsBiffIcv per gli indici grezzi su disco, e xicsOoxmlIndexed, dove 64 e 65 significano system foreground e system background. Gli indici BIFF grezzi vengono anche controllati contro il ruolo che passate come TXLSIndexedColorRole, perché un font, un fill e un grafico accettano insiemi di token diversi; $51 è legale per un font e invalido per un fill
Il risultato booleano di ResolveIndexedColor è dove i chiamanti inciampano. Restituisce True solo quando ha ottenuto un valore ARGB concreto, e l'overload corto non chiede mai al sistema operativo i colori del desktop. Un token system o automatic quindi torna False con Res.Kind impostato a xickSystem o xickAutomatic, e il codice che legge False come "nessun colore" scarta in silenzio il significato del token. L'unica eccezione è $4F, il colore neutro del grafico, che si risolve da solo in nero. Per valori RGB reali sugli altri token, passate un metodo TXLSTryResolveSystemColor all'overload lungo e applicate la vostra policy per la UI, un export o un servizio headless:
type
TExportColors = class
function ResolveSystemColor(AColor: TXLSSystemColorRole;
ARole: TXLSIndexedColorRole; out AArgb: LongWord): Boolean;
end;
function TExportColors.ResolveSystemColor(AColor: TXLSSystemColorRole;
ARole: TXLSIndexedColorRole; out AArgb: LongWord): Boolean;
begin
// export headless: colori fissi invece del tema corrente del desktop
Result := True;
case AColor of
xscrWindowText, xscrChartText, xscrToolTipText:
AArgb := $FF000000;
xscrWindowBackground, xscrChartBackground:
AArgb := $FFFFFFFF;
else
Result := False;
end;
end;
var
Colors: TExportColors; // creato una volta dal vostro codice di export
Res: TXLSIndexedColorResolution;
begin
// $41 è il token system background, valido per un fill
if Workbook.ResolveIndexedColor($41, xicsBiffIcv, xicrFillBackground,
Colors.ResolveSystemColor, Res) then
PaintBackground(Res.ARGB) // la vostra routine di disegno
else if Res.Kind = xickInvalid then
RejectToken(Res.RawIndex); // la vostra gestione degli errori
end;
Perché la distanza RGB sceglie la voce di palette sbagliata?
I valori dei canali sRGB sono gamma-encoded, quindi passi uguali nei numeri non sono passi uguali in ciò che una persona vede, e l'errore è massimo nei toni scuri e saturi che i template aziendali adorano. In RGB, $000033 sta a 51 dal nero e a 77 dal navy, quindi vince il nero. Una volta che ogni canale è linearizzato e il colore è proiettato in OKLab, le distanze quadrate sono circa 0.0312 verso il nero e 0.0235 verso il navy, e vince il navy. Il match è ColorIndex 11 allo slot fisico 18, e gli engine Classic e XLSX danno la stessa risposta
La palette di default aggiunge un dettaglio insidioso. Il navy compare due volte, al ColorIndex 11 e di nuovo al 25, quindi $000033 è esattamente alla stessa distanza da entrambi. Un matcher che scansione in un ordine diverso, o usa less-than-or-equal, sceglie 25 in una esecuzione e 11 in un'altra build, e due file generati smettono di fare diff puliti. HotXLS scandisce dallo slot 8 verso l'alto con un confronto less-than rigoroso, così un pareggio va sempre all'indice più basso
OKLab è una scelta deliberata. CIEDE2000 ordinerebbe alcune coppie di tinte con più precisione, ma le sue correzioni a tratti lo rendono lento e più difficile da ragionare dentro un loop iterativo. OKLab costa una linearizzazione, due piccoli prodotti di matrici e tre radici cubiche per colore, dopodiché una semplice distanza al quadrato è un sostituto abbastanza buono della differenza percepita. Per una scansione della palette a 56 voci che gira a ogni assegnazione di colore e dentro un passaggio di clustering, la metrica più economica e più regolare è la scelta giusta
Cosa restituisce NearestIndexedColor?
NearestIndexedColor prende un valore ARGB e riempie un record TXLSNearestIndexedColorMatch senza toccare il workbook. Ogni workbook tiene insieme i 56 colori di palette con le loro coordinate OKLab e un contatore di generazione della palette, quindi una query costa una conversione dell'input e una scansione fissa. Un reset della palette ricostruisce quella cache, la modifica di una sola voce aggiorna solo quello slot, e una query contro una cache fuori passo con la generazione corrente restituisce False invece di tirare a indovinare
L'alpha segue una regola stretta. Un byte alpha pari a zero viene trattato come opaco, il che corrisponde a come la maggior parte del codice Delphi passa valori RGB semplici, e $FF è opaco per definizione. Tutto ciò che sta in mezzo viene rifiutato: la funzione restituisce False con ColorIndex 0 e PaletteSlot -1, visto che una voce di palette non ha dove memorizzare la trasparenza
var
Match: TXLSNearestIndexedColorMatch;
begin
if Workbook.NearestIndexedColor($FF000033, Match) then
begin
// Palette di default: Match.ColorIndex = 11, Match.PaletteSlot = 18,
// Match.ARGB = $FF000080
if not Match.ExactMatch then
LogApproximation(Match.InputARGB, Match.ARGB, // il vostro logger
Match.DistanceSquared);
end;
end;
Lo stesso matcher sta dietro ai setter RGB. Assegnare Interior.Color, Font.Color o Border.Color su un workbook Classic conserva il colore esatto per il record XFExt e sceglie con questa scansione l'indice memorizzato nel record XF, e i writer di fill e bordi risolvono colori RGB e del tema in un indice allo stesso modo. Ciò che NearestIndexedColor riporta per un colore è lo slot che mostrerà un reader basato sul solo indice
Far stare un workbook true-color in 56 slot
Il matching contro la palette di default va bene per una manciata di colori. Un template di brand con una dozzina di blu quasi identici ha bisogno di più: la palette stessa deve cambiare. BuildBiffPalettePlan calcola una proposta completa per tutti i 56 slot e lascia il workbook intatto, così potete loggarla, confrontarla con una soglia o buttarla via
Il planner parte con ScanIndexedColorUsage. Ogni slot referenziato per indice da un font, un fill, un bordo, una formattazione condizionale, una forma, un commento, un font di grafico o una gridline del foglio di lavoro viene bloccato, perché cambiare quella voce ricolorerebbe in una volta tutti i consumatori dell'indice. I target sono i colori RGB diretti e i colori del tema risolti provenienti da font, fill, stop dei gradienti, bordi, stili di formattazione condizionale, data bar e color scale. Ogni target è pesato col maggiore tra il conteggio dei riferimenti e quello delle definizioni, e una formattazione condizionale conta ogni cella che i suoi range coprono, quindi un fill steso su un'intera colonna pesa più di un colore usato in un solo commento. Il posizionamento poi segue un ordine fisso:
- Gli slot bloccati conservano il loro colore attuale incondizionatamente
- Un target già presente nella palette conserva il suo slot corrispondente più basso, e quello slot diventa fisso
- Se i target distinti rimanenti stanno negli slot liberi, ognuno ottiene uno slot esatto, assegnato in ordine ARGB crescente
- Altrimenti viene impostato
Quantized, ogni slot libero viene inizializzato col target la cui distanza dal centro esistente più vicino, moltiplicata per il suo peso, è massima, e fino a 16 round di k-means pesati in OKLab spostano solo i centri liberi finché le assegnazioni smettono di cambiare
Il percorso di overflow merita una lettura a occhi aperti. Il k-means qui è un'ottimizzazione locale limitata, e uno slot libero finisce per ospitare un centro di cluster riconvertito in sRGB con clamping, che può essere un colore che nessuna cella ha usato alla lettera. Ciò che il planner consegna davvero è la ripetibilità: lo stesso workbook produce sempre lo stesso piano. Il piano riporta anche quanto ha dovuto piegarsi tramite WeightedError, MaxDistanceSquared, ExactTargetWeight e TotalTargetWeight, il che basta a un job batch per rifiutare un salvataggio quando l'approssimazione diventa troppo grossolana per una brand guideline. L'array Targets mostra, per ogni colore di input, lo slot a cui mappa e la sua distanza al quadrato
const
MaxAcceptedError = 0.002; // la vostra soglia, in unità OKLab al quadrato
var
Plan: TXLSBiffPalettePlan;
I: Integer;
begin
Plan := Workbook.BuildBiffPalettePlan; // read-only
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); // il vostro logger
if not Workbook.ApplyBiffPalettePlan(Plan) then
raise Exception.Create('The palette changed after planning');
end;
Quando ApplyBiffPalettePlan rifiuta un piano?
ApplyBiffPalettePlan controlla l'intero piano prima di scrivere un solo slot e restituisce False, palette intatta, se qualcosa non quadra col workbook corrente. Il piano porta con sé SourcePaletteGeneration e SourcePaletteHash, un hash FNV-1a a 64 bit sui 56 colori sorgente. La validazione riverifica anche ogni indice pubblico e fisico, ogni colore sorgente, che nessuno slot bloccato sia segnato come modificato, che i conteggi di bloccati e modificati tornino, e che ogni target sia opaco
Qualsiasi modifica reale alla palette tra pianificazione e applicazione rende il piano stantio, e questo include una precedente applicazione riuscita dello stesso piano. I piani sono di fatto monouso: costruite, ispezionate, applicate, e ricostruite se vi serve un altro. Un piano valido senza slot modificati riesce senza far avanzare la generazione. Una modifica reale la fa avanzare una volta e aggiorna una volta il matcher. Sull'engine Classic i nuovi colori finiscono direttamente nella palette; su quello XLSX diventano colori indicizzati personalizzati, che HasCustomIndexedColor e CustomIndexedColorCount riportano e che il writer XLSX emette come blocco indexedColors
Attivazione per i salvataggi XLS e la conversione da XLSX a XLS
La proprietà BiffPaletteSavePolicy vale xbpsPreserve per impostazione predefinita, quindi la palette di un workbook cambia solo su richiesta. Impostare xbpsOptimizeTrueColors fa sì che un workbook Classic costruisca e applichi un piano fresco dentro SaveAs, e solo quando il formato di destinazione è xlExcel97; i writer BIFF5, CSV, HTML, PDF, XLSX e gli altri ignorano l'impostazione. Dopo un salvataggio riuscito la palette ottimizzata resta nel workbook, quindi query e salvataggi successivi vedono la stessa mappatura. Se il salvataggio fallisce, viene annullato o solleva, i 56 colori originali e la generazione originale vengono ripristinati
Per i report generati, attivate la policy di ottimizzazione. Quei workbook sono costruiti da colori RGB e del tema, nessuno ha tarato a mano la loro palette, e un reader basato sul solo indice li mostra molto più vicini all'originale con essa. Tenete xbpsPreserve per i workbook la cui palette fa parte del contratto, come un template che codice a valle modifica per ColorIndex, dove uno slot che cambia colore in silenzio romperebbe quel codice
Le sorgenti XLSX prendono una strada diversa. SaveXLSXWorkbookAsXLS in lxXlsxExport costruisce un unico piano dal TXLSXWorkbook caricato e lo scrive nella palette di destinazione prima di convertire qualsiasi stile, così ogni cella convertita viene confrontata con la palette finale. La demo del workbench di audit e conversione dei workbook gira esattamente questo percorso. I colori del tema passano per lo stesso planner una volta che il loro tint è stato risolto in RGB; i fill dei grafici che devono restare riferimenti vivi al tema sono un caso a parte, trattato nell'articolo sui fill dei grafici con i colori del tema GelFrame, dove l'XLS binario memorizza un indice di schema invece di un colore appiattito
// Workbook Classic: opt in, vale solo per i salvataggi BIFF8
Workbook.BiffPaletteSavePolicy := xbpsOptimizeTrueColors;
if Workbook.SaveAs('report.xls', xlExcel97) <> 1 then
HandleSaveFailure; // il vostro handler; palette già ripristinata
// modello XLSX verso BIFF8 con un unico piano di palette deterministico
XWorkbook := TXLSXWorkbook.Create;
try
if XWorkbook.Open('report.xlsx') = 1 then
if SaveXLSXWorkbookAsXLS(XWorkbook, 'report.xls') <> 1 then
HandleSaveFailure;
finally
XWorkbook.Free;
end;
Le chiamate della palette (ResolveIndexedColor, NearestIndexedColor, ScanIndexedColorUsage, BuildBiffPalettePlan e ApplyBiffPalettePlan) hanno le stesse firme su IXLSWorkbook e TXLSXWorkbook, da Delphi e C++Builder. La build di prova sulla pagina del componente Excel HotXLS per Delphi basta per girare un piano contro il vostro template più colorato e leggere MaxDistanceSquared prima che qualsiasi cosa venga scritta