HotXLS mappa colori RGB e del tema arbitrari sulla palette BIFF8 a 56 slot in due livelli: NearestIndexedColor trova nello spazio OKLab la voce di palette esistente percettivamente più vicina, mentre BuildBiffPalettePlan con ApplyBiffPalettePlan riscrive gli slot di palette liberi perché un workbook true-color sopravviva a un salvataggio in XLS classico. Lo spunto è sempre lo stesso ticket di supporto. Qualcuno costruisce un report in XLSX con intestazioni blu navy aziendali e un accento verde acqua tenue, lo salva come .xls per un'applicazione legacy, e le intestazioni tornano nero puro mentre il verde acqua diventa un turchese urlante. Niente è andato in crash e nessun warning è scattato. Il modello di colore del vecchio formato semplicemente non può contenere quello che il nuovo descriveva, e la libreria doveva pur scegliere qualcosa
Perché un file XLS può contenere solo 56 colori?
Perché un formato cella BIFF8 non memorizza mai un valore RGB: font, fill e bordi portano 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, e i valori sopra 63 non sono affatto colori ma token come system foreground, system background e testo dei grafici. HotXLS espone la palette tramite un ColorIndex pubblico da 1 a 56, che è l'indice fisico meno 7, e ResolveIndexedColor tiene separati i tre schemi di numerazione tramite TXLSIndexedColorSpace: xicsPublicColorIndex per i valori API 1..56, xicsBiffIcv per gli indici grezzi su disco, validati contro il sottoinsieme IcvFont, IcvXF o IcvChart per il ruolo che passate, e xicsOoxmlIndexed, dove 64 e 65 significano system foreground e system background
var
Res: TXLSIndexedColorResolution;
begin
// $40 è un token icv BIFF, non uno slot di palette
Workbook.ResolveIndexedColor($40, xicsBiffIcv, Res);
case Res.Kind of
xickPalette: UseArgb(Res.ARGB); // slot di palette, se risolto
xickAutomatic,
xickSystem: UseSystemColor(Res.SystemColorRole);
xickInvalid: RejectToken(Res.RawIndex);
end;
end;
Notate che l'esempio usa Res.Kind come selettore e ignora il valore booleano di ritorno. ResolveIndexedColor restituisce True solo quando ha ottenuto un ARGB concreto, e l'overload corto non legge mai il desktop Windows, quindi un token automatic o system torna legittimamente False pur essendo classificato come xickSystem. HotXLS ci è inciampato nel proprio serializzatore di workbook: codice che tratta False come "nessun colore" butta via in silenzio il significato Automatic e System del token. Se vi servono valori RGB reali per quei token, chiamate l'overload lungo e fornite una callback TXLSTryResolveSystemColor che applica la vostra policy di UI, di export o di esecuzione headless
Perché HotXLS trova il colore corrispondente in OKLab invece che in RGB?
Perché i valori di canale sRGB sono gamma-encoded, quindi la distanza euclidea in RGB non segue quello che un occhio umano vede, e l'errore è peggiore proprio nei toni scuri e saturi che le palette aziendali adorano. Prendete il blu scuro $000033. In RGB la distanza dal nero è 51 e quella dalla voce navy predefinita $000080 è 77, quindi un matcher RGB vi dipinge l'intestazione di nero con tutta sicurezza. In OKLab le distanze quadrate sono circa 0.0312 verso il nero e 0.0235 verso il navy, e HotXLS sceglie il navy, ColorIndex 11 allo slot fisico 18; questo caso esatto è bloccato nella suite di test sia per l'engine Classic sia per quella XLSX. La conversione dentro ArgbToOklab linearizza ogni canale sRGB, applica la matrice LMS di OKLab, estrae le radici cubiche e proietta su L, a e b, dopodiché una banale distanza euclidea al quadrato è un proxy ragionevole della differenza percepita. OKLab non è CIEDE2000 e non finge di esserlo, ma non ha correzioni di tonalità a tratti, costa una manciata di moltiplicazioni per colore, ed è abbastanza stabile da governare un loop di clustering, che è dove guadagna davvero il suo posto
Che cosa garantisce NearestIndexedColor?
NearestIndexedColor garantisce una risposta deterministica e read-only: una conversione dell'input, una scansione fissa sulle 56 voci in cache, e l'indice pubblico più basso quando due voci sono equidistanti. Ogni workbook mette in cache l'ARGB normalizzato e le coordinate OKLab di tutti i 56 slot fisici insieme a un contatore di generazione della palette. Un reset della palette ricostruisce la cache, una modifica a un solo slot aggiorna solo quello, e una query contro una generazione non aggiornata restituisce False invece di tirare a indovinare. La scansione usa un confronto strictly-less-than a partire dallo slot 8, ecco perché una palette che contiene due volte lo stesso colore risponde sempre con l'indice più basso; conta quando confrontate due file generati e vi aspettate output byte-identici. L'alpha in ingresso segue un contratto stretto: un byte alpha a zero è trattato come opaco, e un valore parzialmente trasparente viene rifiutato con ColorIndex 0 e PaletteSlot -1, visto che le voci di palette non hanno alpha. I writer di fill e bordi dell'engine Classic convertono colori RGB e del tema in un indice con la stessa routine di matching OKLab al salvataggio, così l'API e il file memorizzato concordano su quale slot riceve un colore
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;
Come fa BuildBiffPalettePlan a far stare i true color in 56 slot?
BuildBiffPalettePlan calcola una proposta completa per tutti i 56 slot senza toccare il workbook, così potete ispezionarla, loggarla o scartarla. Il planner chiama prima ScanIndexedColorUsage: ogni slot che un font, un fill, un bordo, una formattazione condizionale, una forma, un commento o una gridline del foglio di lavoro referenzia per indice viene bloccato, perché cambiare una voce di palette ricolora in una volta tutti i consumatori di quell'indice. I target sono i colori RGB diretti e i colori del tema risolti provenienti da font, fill, bordi, stili differenziali, data bar e color scale. Ogni target è pesato col maggiore tra il conteggio dei riferimenti renderizzati e quello delle definizioni, e una formattazione condizionale conta le celle coperte dai suoi range, così un colore steso su un'intera colonna pesa più di uno usato in una singola nota. Il posizionamento poi procede in un ordine fisso:
- Gli slot bloccati conservano il loro colore sorgente incondizionatamente
- Un target già presente nella palette viene mantenuto al suo slot corrispondente più basso e quello slot diventa fisso
- Se i target unici rimanenti stanno negli slot liberi, ognuno riceve 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 per frequenza in OKLab spostano solo i centri liberi finché le assegnazioni smettono di cambiare
Siate onesti con voi stessi su ciò che il percorso di overflow consegna. Il clustering è un'ottimizzazione locale limitata, non un ottimo globale, e uno slot libero finisce per ospitare un centroide riconvertito in sRGB con clamping, che può essere un colore che nessuna cella ha usato alla lettera. Quello che ottenete è la ripetibilità: lo stesso workbook produce sempre lo stesso piano, e il piano riporta i propri danni tramite WeightedError, MaxDistanceSquared, ExactTargetWeight e TotalTargetWeight, così un job batch può rifiutarsi di salvare quando l'approssimazione diventa troppo grossolana per una brand guideline
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);
if not Workbook.ApplyBiffPalettePlan(Plan) then
raise Exception.Create('The palette changed after planning');
end;
Come rifiuta ApplyBiffPalettePlan un piano non aggiornato?
ApplyBiffPalettePlan valida l'intero piano prima di scrivere un solo slot, e restituisce False con la 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, i conteggi di bloccati e modificati, e che ogni target sia opaco. Qualsiasi modifica effettiva alla palette nel frattempo, incluso un'applicazione riuscita precedente dello stesso piano, rende il piano stantio, quindi i piani sono di fatto monouso. Un piano valido senza slot modificati va a buon fine senza far avanzare la generazione, e una modifica reale fa avanzare la generazione una volta e ricostruisce una volta il matcher OKLab: sull'engine Classic riscrivendo l'array fisso della palette, su quello XLSX sostituendo una lista di override di colori indicizzati preparata in anticipo
Attivazione per i salvataggi BIFF8 e la conversione da XLSX a XLS
La proprietà BiffPaletteSavePolicy vale per default xbpsPreserve, così aggiornare HotXLS non riscrive mai la palette di nessuno alle sue spalle. Impostarla a xbpsOptimizeTrueColors fa sì che un workbook Classic costruisca e applichi un piano fresco dentro SaveAs, ma 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 modello del workbook, quindi query e salvataggi successivi vedono la stessa mappatura. Se il salvataggio fallisce o viene annullato, i 56 colori originali e la generazione originale vengono ripristinati. Per le sorgenti XLSX, SaveXLSXWorkbookAsXLS in lxXlsxExport costruisce un unico piano dal workbook caricato e lo scrive nella palette di destinazione prima che qualsiasi stile venga convertito, ed è il ponte deterministico che esercita la demo del workbench di audit e conversione dei workbook. I colori del tema passano per lo stesso planner dopo che il loro tint è stato risolto in RGB; se piuttosto preferite mantenere i temi vivi nei fill dei grafici, l'articolo sui fill dei grafici con i colori del tema GelFrame spiega come l'XLS binario memorizza un indice di schema invece di un colore appiattito
// Workbook Classic: opt in, solo BIFF8
Workbook.BiffPaletteSavePolicy := xbpsOptimizeTrueColors;
if Workbook.SaveAs('report.xls', xlExcel97) <> 1 then
HandleSaveFailure; // 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
SaveXLSXWorkbookAsXLS(XWorkbook, 'report.xls');
finally
XWorkbook.Free;
end;
Le API della palette di HotXLS funzionano allo stesso modo su IXLSWorkbook e TXLSXWorkbook, sia da Delphi sia da C++Builder. Scaricate la trial e puntatela al vostro foglio di calcolo più colorato dalla pagina del componente Excel HotXLS per Delphi