Teknisk artikkel

BIFF8-palett med 56 farger: OKLab-fargetilordning i HotXLS

HotXLS mapper vilkårlige RGB- og temafarger inn på BIFF8-palettens 56 plasser i to lag: NearestIndexedColor finner den perseptuelt nærmeste eksisterende palettoppføringen i OKLab-rommet, og BuildBiffPalettePlan med ApplyBiffPalettePlan skriver om de ledige palettplassene slik at en arbeidsbok med truecolor-farger overlever en lagring til klassisk XLS. Utløseren er alltid den samme supportsaken. Noen bygger en rapport i XLSX med marineblå overskrifter og en dempet teal-aksent, lagrer den som .xls til en eldre forbruker, og overskriftene kommer tilbake helt svarte mens teal-en blir til en skrikende turkis. Ingenting krasjet, og ingen advarsel ble gitt. Fargemodellen i det gamle formatet klarer rett og slett ikke å holde på det det nye beskriver, og biblioteket måtte bare velge noe

Hvorfor kan en XLS-fil bare romme 56 farger?

Fordi et BIFF8-celleformat aldri lagrer en RGB-verdi: fonter, fyll og kantlinjer bærer en fargeindeks, og den arbeidsbokomfattende Palette-recorden ($0092, [MS-XLS] §2.4.188) leverer nøyaktig 56 opaque RGB-oppføringer for indeksene 8 til 63. Indeksene 0 til 7 er faste kopier av de åtte grunnfargene, og verdiene over 63 er ikke farger i det hele tatt, men tokens som systemforgrunn, systembakgrunn og chart-tekst. HotXLS eksponerer paletten gjennom en offentlig ColorIndex fra 1 til 56, som er den fysiske indeksen minus 7, og ResolveIndexedColor holder de tre nummereringsordningene fra hverandre gjennom TXLSIndexedColorSpace: xicsPublicColorIndex for API-verdiene 1..56, xicsBiffIcv for rå indekser på disk, som valideres mot IcvFont-, IcvXF- eller IcvChart-delmengden for rollen du oppgir, og xicsOoxmlIndexed, der 64 og 65 betyr systemforgrunn og systembakgrunn

HotXLS holder de tre indekserte fargeordningene fra hverandre gjennom TXLSIndexedColorSpace: rå BIFF icv-verdier, 0 til 7 fastlåst til de åtte grunnfargene, de 56 palettplassene 8 til 63 i Palette-recorden $0092, tokens over 63 som systemforgrunn, offentlig ColorIndex 1 til 56 forskjøvet med minus 7, og xicsOoxmlIndexed der 64 og 65 betyr systemforgrunn og systembakgrunn
Samme fargeindeks betyr forskjellige tall i hver ordning, så HotXLS sender hver verdi gjennom ResolveIndexedColor i stedet for å la et rått BIFF-token utgi seg for en offentlig ColorIndex
var
  Res: TXLSIndexedColorResolution;
begin
  // $40 er et BIFF icv-token, ikke en palettplass
  Workbook.ResolveIndexedColor($40, xicsBiffIcv, Res);
  case Res.Kind of
    xickPalette:   UseArgb(Res.ARGB);   // palettplass, hvis den løses opp
    xickAutomatic,
    xickSystem:    UseSystemColor(Res.SystemColorRole);
    xickInvalid:   RejectToken(Res.RawIndex);
  end;
end;

Merk at eksemplet svitsjer på Res.Kind og ignorerer den boolske returverdien. ResolveIndexedColor returnerer True bare når den har fått tak i en konkret ARGB, og den korte overloaden leser aldri Windows-skrivebordet, så et automatisk- eller system-token kan fullt lovlig komme tilbake som False og likevel være klassifisert som xickSystem. HotXLS traff på dette i sin egen arbeidsbokserialiserer: kode som behandler False som «ingen farge», kaster stille bort tokens Automatic- og System-betydning. Trenger du ekte RGB-verdier for de tokenene, kaller du den lange overloaden og oppgir en TXLSTryResolveSystemColor-callback som anvender din egen UI-, eksport- eller headless-policy

Hvorfor matcher HotXLS farger i OKLab i stedet for RGB?

Fordi sRGB-kanalverdier er gamma-kodet, så euklidisk avstand i RGB følger ikke det et menneske ser, og feilen er verst i akkurat de mørke, mettede tonene bedriftspaletter elsker. Ta den mørkeblå $000033. I RGB er avstanden til svart 51, og avstanden til standard marineblå-oppføringen $000080 er 77, så en RGB-matcher maler overskriften din trygt svart. I OKLab er de kvadrerte avstandene omkring 0,0312 til svart og 0,0235 til marineblå, og HotXLS velger marineblå, ColorIndex 11 på fysisk plass 18; akkurat det tilfellet er festet i testpakken for både Classic- og XLSX-motoren. Konverteringen inne i ArgbToOklab lineariserer hver sRGB-kanal, anvender OKLab LMS-matrisen, tar kubikkroter og projiserer på L, a og b, og etter det er en enkel kvadrert euklidisk avstand en rimelig proxy for opplevd forskjell. OKLab er ikke CIEDE2000 og later som det ikke, men den har ingen stykkevise kulørkorreksjoner, koster en neve multiplikasjoner per farge, og er stabil nok til å drive en clustering-løkke, og det er der den virkelig tjener plassen sin

Hvordan HotXLS matcher den mørkeblå $000033 mot paletten: euklidisk avstand i gamma-kodet RGB måler 51 til svart og 77 til marineblå og ville malt overskriften svart, mens ArgbToOklab-kvadrerte avstander på 0,0312 og 0,0235 lar NearestIndexedColor velge marineblå, ColorIndex 11 på fysisk plass 18
Gamma-kodede kanalverdier gjør RGB-avstand til en dårlig proxy for det et menneske ser, så HotXLS konverterer én gang til OKLab og lar en enkel kvadrert euklidisk sammenligning drive palettskanningen

Hva garanterer NearestIndexedColor?

NearestIndexedColor garanterer et deterministisk, skrivebeskyttet svar: én inngangskonvertering, én fast skanning over de 56 oppføringene i cachen, og lavest offentlig indeks når to oppføringer er like nære. Hver arbeidsbok cacher den normaliserte ARGB-en og OKLab-koordinatene for alle 56 fysiske plasser sammen med en palettgenerasjonsteller. En palettnullstilling bygger cachen på nytt, en endring av én plass oppdaterer bare den plassen, og en spørring mot en foreldet generasjon returnerer False i stedet for å gjette. Skanningen bruker en streng mindre-enn-sammenligning som starter på plass 8, og det er derfor en palett som inneholder samme farge to ganger, alltid svarer med den lavere indeksen; det teller når du diff-er to genererte filer og forventer byte-identisk output. Inngangs-alpha følger en snever kontrakt: en null alpha-byte behandles som opaque, og en delvis gjennomsiktig verdi avvises med ColorIndex 0 og PaletteSlot -1, siden palettoppføringer ikke har alpha. Fyll- og kantlinjeskriverne i Classic-motoren konverterer RGB- og temafarger til en indeks med samme OKLab-matcherutinene ved lagring, så API-en og den lagrede filen er enige om hvilken plass en farge lander på

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;

Hvordan får BuildBiffPalettePlan truecolor-farger inn på 56 plasser?

BuildBiffPalettePlan beregner et komplett forslag til alle 56 plassene uten å røre arbeidsboken, så du kan inspisere, logge eller forkaste det. Planleggeren kaller først ScanIndexedColorUsage: enhver plass som en font, et fyll, en kantlinje, en betinget formatering, en shape, en kommentar eller rutenettet i regnearket refererer til via indeks, låses, fordi en endring av en palettoppføring farger om alle forbrukerne av den indeksen på én gang. Målene er de direkte RGB- og de løste temafargene fra fonter, fyll, kantlinjer, differentialstiler, databar og fargeskalaer. Hvert mål vektes etter det største av antall rendererte referanser og antall definisjoner, og en betinget formatering teller cellene områdene sine dekker, så en farge malt på tvers av en hel kolonne veier tyngre enn én brukt i en enkelt kommentar. Plasseringen går så videre i fast rekkefølge:

  • Låste plasser beholder kildefargen sin betingelsesløst
  • Et mål som allerede finnes i paletten, beholdes på sin laveste matchende plass, og den plassen blir fastlåst
  • Passer de gjenværende unike målene inn i de ledige plassene, får hver av dem en eksakt plass, tildelt i stigende ARGB-rekkefølge
  • Ellers settes Quantized, hver ledig plass seres med målet hvis avstand til nærmeste eksisterende senter, ganget med vekten sin, er størst, og opptil 16 runder med frekvensvektet k-means i OKLab flytter bare de ledige sentrene til fordelingene slutter å endre seg

Vær ærlig med deg selv på hva overflow-stien leverer. Clusteringen er en avgrenset lokal optimalisering, ikke et globalt optimum, og en ledig plass ender opp med å holde en centroid konvertert tilbake til sRGB med clamping, som kan være en farge ingen celle brukte ordrett. Det du får, er repeterbarhet: samme arbeidsbok gir alltid samme plan, og planen rapporterer sin egen skade gjennom WeightedError, MaxDistanceSquared, ExactTargetWeight og TotalTargetWeight, så en batchjobb kan nekte å lagre når approksimasjonen blir for grov for merkevarelinjene

HotXLS-palettpipelinen for en arbeidsbok med truecolor-farger: ScanIndexedColorUsage låser hver plass en font, et fyll, en kantlinje, en betinget formatering, en shape, en kommentar eller rutenettet refererer til, BuildBiffPalettePlan plasserer eksakte farger i stigende ARGB-rekkefølge eller kjører opptil 16 runder med frekvensvektet k-means i OKLab, og ApplyBiffPalettePlan validerer generasjonen og FNV-1a-hashen før skriving
Planlegging er skrivebeskyttet og repeterbar, planen rapporterer sin egen skade gjennom WeightedError og MaxDistanceSquared, og en foreldet plan avvises med paletten urørt, fordi planer i praksis er engangs
var
  Plan: TXLSBiffPalettePlan;
  I: Integer;
begin
  Plan := Workbook.BuildBiffPalettePlan;   // skrivebeskyttet
  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;

Hvordan avviser ApplyBiffPalettePlan en foreldet plan?

ApplyBiffPalettePlan validerer hele planen før den skriver en eneste plass, og returnerer False med paletten urørt hvis noe ikke stemmer med den gjeldende arbeidsboken. Planen bærer SourcePaletteGeneration og SourcePaletteHash, en 64-bit FNV-1a-hash over de 56 kildefargene; valideringen sjekker på nytt hver offentlig og fysisk indeks, hver kildefarge, at ingen låst plass er merket som endret, antall låste og endrede, og at hvert mål er opaque. Enhver effektiv palettendring i mellomtiden, inkludert en vellykket tidligere anvendelse av samme plan, gjør planen foreldet, så planer er i praksis engangs. En gyldig plan uten endrede plasser lykkes uten å rykke generasjonen fremover, og en ekte endring rykker generasjonen én gang og bygger OKLab-matcheren én gang på nytt — på Classic-motoren ved å skrive det faste palettarrayet om, og på XLSX-motoren ved å bytte inn en ferdig forberedt indexed-color-override-liste

Slå det på for BIFF8-lagring og XLSX-til-XLS-konvertering

Egenskapen BiffPaletteSavePolicy står som standard til xbpsPreserve, så en HotXLS-oppgradering skriver aldri om noens palett bak ryggen deres. Setter du den til xbpsOptimizeTrueColors, bygger og anvender en Classic-arbeidsbok en fersk plan inne i SaveAs, men bare når målformatet er xlExcel97; BIFF5, CSV, HTML, PDF, XLSX og de andre skriverne ignorerer innstillingen. Etter en vellykket lagring blir den optimaliserte paletten værende i arbeidsbokmodellen, så senere spørringer og lagringer ser den samme mappen. Feiler lagringen eller avbrytes den, gjenopprettes de opprinnelige 56 fargene og den opprinnelige generasjonen. For XLSX-kilder bygger SaveXLSXWorkbookAsXLS i lxXlsxExport én plan fra den innleste arbeidsboken og skriver den inn i målpaletten før noen stil konverteres, som er den deterministiske broen workbench-demonstrasjonen for arbeidsbok-audit og konvertering trener på. Temafarger går gjennom samme planlegger etter at tinten deres er løst til RGB; vil du heller holde temaene live i chart-fyll, dekker artikkelen om GelFrame-temafarge-chart-fyll hvordan binær XLS lagrer en skjemaindeks i stedet for en utflatet farge

// Klassisk arbeidsbok: opt in, kun BIFF8
Workbook.BiffPaletteSavePolicy := xbpsOptimizeTrueColors;
if Workbook.SaveAs('report.xls', xlExcel97) <> 1 then
  HandleSaveFailure;   // paletten er allerede gjenopprettet

// XLSX-modell til BIFF8 med én deterministisk palettplan
XWorkbook := TXLSXWorkbook.Create;
try
  if XWorkbook.Open('report.xlsx') = 1 then
    SaveXLSXWorkbookAsXLS(XWorkbook, 'report.xls');
finally
  XWorkbook.Free;
end;

HotXLS-palett-API-ene jobber på samme måte på IXLSWorkbook og TXLSXWorkbook, både fra Delphi og C++Builder. Last ned prøveversjonen og pek den på det mest fargerike regnearket ditt fra HotXLS Delphi Excel-komponentsiden