Τεχνικό Άρθρο

Παλέτα 56 χρωμάτων του BIFF8: αντιστοίχιση OKLab στο HotXLS

Το HotXLS αντιστοιχίζει αυθαίρετα χρώματα RGB και theme στην παλέτα 56 θέσεων του BIFF8 σε δύο επίπεδα: το NearestIndexedColor βρίσκει την αντιληπτικά πλησιέστερη υπάρχουσα εγγραφή της παλέτας στον χώρο OKLab, και το BuildBiffPalettePlan μαζί με το ApplyBiffPalettePlan ξαναγράφουν τις ελεύθερες θέσεις της παλέτας ώστε ένα workbook με αληθινά χρώματα να επιβιώσει από ένα save σε κλασικό XLS. Η αφορμή είναι πάντα το ίδιο support ticket. Κάποιος φτιάχνει μια αναφορά σε XLSX με σκούρα μπλε headers της εταιρείας και ένα απαλό teal ως έμφαση, την αποθηκεύει ως .xls για έναν παλιό καταναλωτή, και τα headers επιστρέφουν καθαρά μαύρα ενώ το teal γίνεται ένα φωνακλίδικο turquoise. Τίποτα δεν κατέρρευσε και καμία προειδοποίηση δεν χτύπησε. Το χρωματικό μοντέλο της παλιάς μορφής απλώς δεν χωράει αυτό που περιέγραψε η νέα, και η βιβλιοθήκη έπρεπε να διαλέξει κάτι

Γιατί ένα αρχείο XLS χωράει μόνο 56 χρώματα;

Επειδή μια μορφή κελιού BIFF8 δεν αποθηκεύει ποτέ τιμή RGB: fonts, fills και borders κουβαλάνε έναν δείκτη χρώματος, και το record Palette σε επίπεδο workbook ($0092, [MS-XLS] §2.4.188) παρέχει ακριβώς 56 αδιαφανείς εγγραφές RGB για τους δείκτες 8 έως 63. Οι δείκτες 0 έως 7 είναι σταθερά αντίγραφα των οκτώ βασικών χρωμάτων, και οι τιμές πάνω από 63 δεν είναι καθόλου χρώματα αλλά tokens όπως system foreground, system background και κείμενο chart. Το HotXLS εκθέτει την παλέτα μέσω ενός public ColorIndex από 1 έως 56, που είναι ο φυσικός δείκτης μείον 7, και το ResolveIndexedColor κρατάει τα τρία συστήματα αρίθμησης ξεχωριστά μέσα από το TXLSIndexedColorSpace: xicsPublicColorIndex για τις τιμές API 1..56, xicsBiffIcv για τους ακατέργαστους δείκτες στον δίσκο, που επικυρώνονται απέναντι στο υποσύνολο IcvFont, IcvXF ή IcvChart για τον ρόλο που περνάτε, και xicsOoxmlIndexed, όπου 64 και 65 σημαίνουν system foreground και background

Το HotXLS κρατάει τα τρία συστήματα δεικτών χρώματος ξεχωριστά μέσα από το TXLSIndexedColorSpace: ακατέργαστες τιμές icv του BIFF, 0 έως 7 δεμένοι με τα οκτώ βασικά χρώματα, οι 56 θέσεις παλέτας 8 έως 63 του record Palette $0092, tokens πάνω από 63 όπως system foreground, public ColorIndex 1 έως 56 με μετατόπιση μείον 7, και xicsOoxmlIndexed όπου 64 και 65 σημαίνουν system foreground και background
Ο ίδιος δείκτης χρώματος σημαίνει διαφορετικούς αριθμούς σε κάθε σύστημα, οπότε το HotXLS δρομολογεί κάθε τιμή μέσω του ResolveIndexedColor αντί να αφήνει ένα ακατέργαστο token του BIFF να πλασάρεται ως public ColorIndex
var
  Res: TXLSIndexedColorResolution;
begin
  // Το $40 είναι ένα token icv του BIFF, όχι θέση παλέτας
  Workbook.ResolveIndexedColor($40, xicsBiffIcv, Res);
  case Res.Kind of
    xickPalette:   UseArgb(Res.ARGB);   // θέση παλέτας, αν επιλυθεί
    xickAutomatic,
    xickSystem:    UseSystemColor(Res.SystemColorRole);
    xickInvalid:   RejectToken(Res.RawIndex);
  end;
end;

Προσέξτε ότι το παράδειγμα κάνει switch πάνω στο Res.Kind και αγνοεί τη Boolean επιστρεφόμενη τιμή. Το ResolveIndexedColor επιστρέφει True μόνο όταν έχει αποκτήσει ένα συγκεκριμένο ARGB, και η σύντομη υπερφόρτωση δεν διαβάζει ποτέ την επιφάνεια εργασίας των Windows, οπότε ένα automatic ή system token επιστρέφει νόμιμα False ενώ ταξινομείται ως xickSystem. Το HotXLS το συνάντησε στον δικό του workbook serializer: κώδικας που αντιμετωπίζει το False ως «χωρίς χρώμα» πετάει σιωπηλά τη σημασία Automatic και System του token. Αν χρειάζεστε πραγματικές τιμές RGB για αυτά τα tokens, καλέστε τη μακρά υπερφόρτωση και δώστε ένα callback TXLSTryResolveSystemColor που εφαρμόζει τη δική σας πολιτική UI, εξαγωγής ή headless

Γιατί το HotXLS αντιστοιχίζει χρώματα σε OKLab αντί για RGB;

Επειδή οι τιμές καναλιών sRGB είναι gamma-encoded, η ευκλείδεια απόσταση στο RGB δεν ακολουθεί αυτό που βλέπει ένας άνθρωπος, και το σφάλμα είναι χειρότερο ακριβώς στους σκούρους, κορεσμένους τόνους που αγαπούν οι εταιρικές παλέτες. Πάρτε το σκούρο μπλε $000033. Στο RGB η απόσταση από το μαύρο είναι 51 και από την προεπιλεγμένη εγγραφή navy $000080 είναι 77, οπότε ένας matcher σε RGB σας βάφει το header μαύρο με πλήρη αυτοπεποίθηση. Στο OKLab οι τετραγωνικές αποστάσεις είναι περίπου 0.0312 προς το μαύρο και 0.0235 προς το navy, και το HotXLS διαλέγει το navy, ColorIndex 11 στη φυσική θέση 18· αυτή η ακριβής περίπτωση είναι καρφωμένη στη test suite και για τις δύο μηχανές, Classic και XLSX. Η μετατροπή μέσα στο ArgbToOklab γραμμοποιεί κάθε κανάλι sRGB, εφαρμόζει τον πίνακα LMS του OKLab, βγάζει κυβικές ρίζες και προβάλλει πάνω σε L, a και b, και μετά από εκεί μια σκέτη τετραγωνική ευκλείδεια απόσταση είναι λογική προσέγγιση της αντιληπτής διαφοράς. Το OKLab δεν είναι CIEDE2000 και δεν το ισχυρίζεται, αλλά δεν έχει piecewise διορθώσεις απόχρωσης, κοστίζει μερικούς πολλαπλασιασμούς ανά χρώμα, και είναι αρκετά σταθερό ώστε να τροφοδοτεί έναν βρόχο clustering — εκεί κερδίζει πραγματικά τη θέση του

Πώς το HotXLS αντιστοιχίζει το σκούρο μπλε $000033 στην παλέτα: η ευκλείδεια απόσταση σε gamma-encoded RGB μετρά 51 προς το μαύρο και 77 προς το navy και θα έβαφε το header μαύρο, ενώ οι τετραγωνικές αποστάσεις ArgbToOklab 0.0312 και 0.0235 αφήνουν το NearestIndexedColor να διαλέξει το navy, ColorIndex 11 στη φυσική θέση 18
Οι gamma-encoded τιμές καναλιών κάνουν την απόσταση RGB κακή προσέγγιση αυτού που βλέπει ο άνθρωπος, οπότε το HotXLS μετατρέπει μία φορά σε OKLab και αφήνει μια σκέτη τετραγωνική ευκλείδεια σύγκριση να καθοδηγήσει τη σάρωση της παλέτας

Τι εγγυάται το NearestIndexedColor;

Το NearestIndexedColor εγγυάται μια ντετερμινιστική απάντηση μόνο για ανάγνωση: μία μετατροπή της εισόδου, μία σταθερή σάρωση πάνω σε 56 εγγραφές της cache, και τον χαμηλότερο public index όποτε δύο εγγραφές ισαπέχουν. Κάθε workbook κρατάει σε cache το κανονικοποιημένο ARGB και τις συντεταγμένες OKLab και των 56 φυσικών θέσεων μαζί με έναν μετρητή γενιάς της παλέτας. Ένα reset της παλέτας ξαναχτίζει την cache, μια αλλαγή μοναδικής θέσης ενημερώνει μόνο εκείνη τη θέση, και ένα query πάνω σε παλιά γενιά επιστρέφει False αντί να μαντεύει. Η σάρωση χρησιμοποιεί αυστηρή σύγκριση μικρότερο-από ξεκινώντας από τη θέση 8, γι' αυτό μια παλέτα που περιέχει το ίδιο χρώμα δύο φορές απαντά πάντα με τον χαμηλότερο δείκτη· αυτό έχει σημασία όταν κάνετε diff δύο παραγόμενων αρχείων και περιμένετε byte-πανομοιότυπη έξοδο. Το alpha της εισόδου ακολουθεί ένα στενό συμβόλαιο: byte alpha μηδέν θεωρείται αδιαφανές, και μια μερικώς διαφανής τιμή απορρίπτεται με ColorIndex 0 και PaletteSlot -1, αφού οι εγγραφές της παλέτας δεν έχουν alpha. Οι writers των fills και borders της Classic μηχανής μετατρέπουν χρώματα RGB και theme σε δείκτη με την ίδια ρουτίνα αντιστοίχισης OKLab τη στιγμή του save, ώστε το 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 θέσεις χωρίς να αγγίξει το workbook, οπότε μπορείτε να την εξετάσετε, να την καταγράψετε σε log ή να την πετάξετε. Ο planner καλεί πρώτα το ScanIndexedColorUsage: κάθε θέση που κάποιο font, fill, border, conditional format, shape, σχόλιο ή γραμμή πλέγματος worksheet αναφέρει με δείκτη κλειδώνει, επειδή η αλλαγή μιας εγγραφής της παλέτας ξαναβάφει όλους μαζί τους καταναλωτές εκείνου του δείκτη. Στόχοι είναι τα απευθείας χρώματα RGB και τα επιλυμένα χρώματα theme από fonts, fills, borders, differential styles, data bars και color scales. Κάθε στόχος ζυγίζεται με το μεγαλύτερο από το πλήθος των αναφορών του στο rendered αποτέλεσμα και το πλήθος των ορισμών του, και ένα conditional format μετρά τα κελιά που καλύπτουν τα ranges του, ώστε ένα χρώμα απλωμένο σε ολόκληρη στήλη να ζυγίζει περισσότερο από ένα σε ένα μόνο σχόλιο. Η τοποθέτηση προχωρά μετά σε σταθερή σειρά:

  • Οι κλειδωμένες θέσεις κρατούν το χρώμα πηγής τους ανευ όρων
  • Ένας στόχος που υπάρχει ήδη στην παλέτα διατηρείται στη χαμηλότερη θέση που ταιριάζει, και εκείνη η θέση γίνεται σταθερή
  • Αν οι υπόλοιποι μοναδικοί στόχοι χωράνε στις ελεύθερες θέσεις, ο καθένας παίρνει ακριβή θέση, ανατεθειμένη σε αύξουσα σειρά ARGB
  • Αλλιώς ενεργοποιείται το Quantized, κάθε ελεύθερη θέση σπέρνεται με τον στόχο του οποίου η απόσταση από το πλησιέστερο υπάρχον κέντρο, πολλαπλασιασμένη με το βάρος του, είναι η μεγαλύτερη, και έως 16 γύροι k-means σταθμισμένων κατά συχνότητα στο OKLab μετακινούν μόνο τα ελεύθερα κέντρα μέχρι οι αναθέσεις να πάψουν να αλλάζουν

Να είστε ειλικρινείς με τον εαυτό σας για το τι προσφέρει το μονοπάτι υπερχείλισης. Το clustering είναι μια φραγμένη τοπική βελτιστοποίηση, όχι καθολικό βέλτιστο, και μια ελεύθερη θέση καταλήγει να κρατάει ένα centroid μετατραπμένο πίσω σε sRGB με clamping, που μπορεί να είναι χρώμα που κανένα κελί δεν χρησιμοποίησε verbatim. Αυτό που παίρνετε είναι επαναληψιμότητα: το ίδιο workbook δίνει πάντα το ίδιο σχέδιο, και το σχέδιο αναφέρει μόνο του τη ζημιά μέσω WeightedError, MaxDistanceSquared, ExactTargetWeight και TotalTargetWeight, ώστε μια batch εργασία να μπορεί να αρνηθεί το save όταν η προσέγγιση γίνεται πολύ χοντρική για ένα brand guideline

Ο σωλήνας παλέτας του HotXLS για ένα workbook με αληθινά χρώματα: το ScanIndexedColorUsage κλειδώνει κάθε θέση που αναφέρει font, fill, border, conditional format, shape, σχόλιο ή γραμμή πλέγματος, το BuildBiffPalettePlan τοποθετεί ακριβή χρώματα σε αύξουσα σειρά ARGB ή τρέχει έως 16 γύρους k-means σταθμισμένους κατά συχνότητα στο OKLab, και το ApplyBiffPalettePlan επικυρώνει τη γενιά και το hash FNV-1a πριν γράψει
Ο σχεδιασμός είναι μόνο για ανάγνωση και επαναλήψιμος, το σχέδιο αναφέρει τη ζημιά του μέσω WeightedError και MaxDistanceSquared, και ένα παρωχημένο σχέδιο απορρίπτεται με την παλέτα ανέγγιχτη επειδή τα σχέδια είναι ουσιαστικά μονοχρήσιμα
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 επικυρώνει ολόκληρο το σχέδιο πριν γράψει έστω μία θέση, και επιστρέφει False με την παλέτα ανέγγιχτη αν οτιδήποτε διαφωνεί με το τρέχον workbook. Το σχέδιο κουβαλάει SourcePaletteGeneration και SourcePaletteHash, ένα hash 64-bit FNV-1a πάνω στα 56 χρώματα πηγής· η επικύρωση ξαναελέγχει επίσης κάθε public και φυσικό δείκτη, κάθε χρώμα πηγής, ότι καμία κλειδωμένη θέση δεν είναι σημειωμένη ως αλλαγμένη, τα πλήθη κλειδωμένων και αλλαγμένων, και ότι κάθε στόχος είναι αδιαφανής. Οποιαδήποτε πραγματική αλλαγή της παλέτας εν τω μεταξύ, συμπεριλαμβανομένης μιας επιτυχημένης προηγούμενης εφαρμογής του ίδιου σχεδίου, κάνει το σχέδιο παρωχημένο, οπότε τα σχέδια είναι ουσιαστικά μονοχρήσιμα. Ένα έγκυρο σχέδιο χωρίς αλλαγμένες θέσεις πετυχαίνει χωρίς να προωθήσει τη γενιά, και μια πραγματική αλλαγή ανεβάζει τη γενιά μία φορά και ξαναχτίζει τον matcher OKLab μία φορά: στην Classic μηχανή ξαναγράφοντας τον σταθερό πίνακα παλέτας, και στην XLSX μηχανή αντικαθιστώντας με μια προετοιμασμένη λίστα override δεικτών χρώματος

Ενεργοποίηση για saves BIFF8 και μετατροπή XLSX σε XLS

Η ιδιότητα BiffPaletteSavePolicy έχει προεπιλογή το xbpsPreserve, ώστε μια αναβάθμιση του HotXLS να μην ξαναγράφει ποτέ τη παλέτα κανενός στα κρυφά. Αν την ορίσετε σε xbpsOptimizeTrueColors, ένα Classic workbook χτίζει και εφαρμόζει φρέσκο σχέδιο μέσα στο SaveAs, αλλά μόνο όταν η μορφή προορισμού είναι xlExcel97: οι writer για BIFF5, CSV, HTML, PDF, XLSX και τα υπόλοιπα αγνοούν τη ρύθμιση. Μετά από επιτυχημένο save η βελτιστοποιημένη παλέτα μένει στο μοντέλο του workbook, ώστε μεταγενέστερα query και saves να βλέπουν την ίδια αντιστοίχιση. Αν το save αποτύχει ή ακυρωθεί, επανέρχονται τα αρχικά 56 χρώματα και η αρχική γενιά. Για πηγές XLSX, το SaveXLSXWorkbookAsXLS στο lxXlsxExport χτίζει ένα σχέδιο από το φορτωμένο workbook και το γράφει στην παλέτα προορισμού πριν μετατραπεί οποιοδήποτε style, που είναι η ντετερμινιστική γέφυρα που εξασκεί το demo του workbench ελέγχου και μετατροπής workbook. Τα χρώματα theme περνούν από τον ίδιο planner αφού το tint τους επιλυθεί σε RGB· αν προτιμάτε τα themes να μένουν ζωντανά στα chart fills, το άρθρο για τα GelFrame theme color chart fills εξηγεί πώς το δυαδικό XLS αποθηκεύει δείκτη scheme αντί για ισοπεδωμένο χρώμα

// Classic workbook: opt in, μόνο BIFF8
Workbook.BiffPaletteSavePolicy := xbpsOptimizeTrueColors;
if Workbook.SaveAs('report.xls', xlExcel97) <> 1 then
  HandleSaveFailure;   // η παλέτα έχει ήδη επανέλθει

// Μοντέλο XLSX σε BIFF8 με ένα ντετερμινιστικό σχέδιο παλέτας
XWorkbook := TXLSXWorkbook.Create;
try
  if XWorkbook.Open('report.xlsx') = 1 then
    SaveXLSXWorkbookAsXLS(XWorkbook, 'report.xls');
finally
  XWorkbook.Free;
end;

Τα palette API του HotXLS δουλεύουν με τον ίδιο τρόπο πάνω σε IXLSWorkbook και TXLSXWorkbook, εξίσου από Delphi και C++Builder. Κατεβάστε το trial και στρέψτε το στο πιο πολύχρωμο spreadsheet σας από τη σελίδα του HotXLS Delphi Excel component