기술 문서

HotXLS의 BIFF8 56색 팔레트와 OKLab 색 매핑

짙은 파랑 $000033은 검은색에서 51, 클래식 네이비 항목 $000080에서 77만큼 떨어져 있는데, 이는 RGB 채널 값 위의 평범한 유클리드 거리로 재었을 때입니다. nearest-RGB 매칭으로 클래식 .xls 파일을 쓰는 변환기는 그래서 짙은 파랑 헤더 행을 검게 칠하고, 팔레트 인덱스만 보는 모든 리더가 보여주는 것도 검은색입니다. 형식은 라이터에게 여유를 주지 않습니다. 셀 서식은 팔레트 인덱스를 저장하고 팔레트는 56개 항목을 담을 뿐입니다. 헤더를 제대로 맞추려면 두 가지가 필요합니다. 사람이 보는 것을 따라가는 거리 척도와, 통합 문서가 팔레트 용량보다 많은 서로 다른 색을 쓸 때 팔레트 자체를 다시 쓰는 방법입니다

56색 제한은 어디에서 오나

BIFF8 폰트, 채우기, 테두리 레코드는 RGB 값을 실지 않습니다. 색 인덱스를 들고 있고, 통합 문서 전역의 Palette 레코드($0092, [MS-XLS] §2.4.188)가 인덱스 8부터 63까지 정확히 56개의 불투명 RGB 항목을 공급합니다. 인덱스 0부터 7은 여덟 기본색의 고정 복사본입니다. 63을 넘는 값은 색이 아니라 토큰입니다. $40은 시스템 전경, $41은 시스템 배경, $4D부터 $4F는 차트 텍스트, 차트 배경, 중립 차트 색, $7FFF는 자동입니다. Excel 2007은 셀 서식에 완전한 RGB나 테마 색을 붙일 수 있는 XFExt 레코드($087D, §2.4.355)를 더했지만, Excel 97~2003과 XF 레코드만 읽는 파서는 여전히 인덱스 외에는 아무것도 보지 못합니다

HotXLS는 1부터 56까지의 공개 ColorIndex, 즉 물리 인덱스에서 7을 뺀 값으로 팔레트를 노출합니다. 세 넘버링 체계가 얽혀 있고 ResolveIndexedColor는 TXLSIndexedColorSpace로 그들을 분리합니다. 1..56 API 값의 xicsPublicColorIndex, 디스크의 날 인덱스인 xicsBiffIcv, 그리고 64와 65가 시스템 전경과 배경을 뜻하는 xicsOoxmlIndexed입니다. 날 BIFF 인덱스는 또 TXLSIndexedColorRole로 넘긴 역할과 대조됩니다. 폰트, 채우기, 차트는 받아들이는 토큰 집합이 다르기 때문입니다. $51은 폰트에는 합법이고 채우기에는 무효입니다

HotXLS가 TXLSIndexedColorSpace로 세 인덱스 색 체계를 분리하는 방식: 날 BIFF icv 값, 여덟 기본색에 고정된 0부터 7, Palette 레코드 $0092의 8부터 63까지 56개 팔레트 슬롯, 시스템 전경 같은 63 초과 토큰, 7을 뺀 공개 ColorIndex 1부터 56, 그리고 64와 65가 시스템 전경과 배경을 뜻하는 xicsOoxmlIndexed
같은 색 인덱스도 체계마다 다른 숫자를 뜻하므로, HotXLS는 날 BIFF 토큰이 공개 ColorIndex로 둔갑하지 못하게 모든 값을 ResolveIndexedColor로 라우팅합니다

ResolveIndexedColor의 Boolean 반환값에서 호출자들이 미끄러집니다. 구체적인 ARGB 값을 얻었을 때만 True를 돌려주고, 짧은 오버로드는 데스크톱 색을 운영체제에 묻지 않습니다. 따라서 시스템이나 자동 토큰은 Res.Kind가 xickSystem이나 xickAutomatic으로 설정된 False로 돌아오는데, False를 "색 없음"으로 읽는 코드는 토큰의 의미를 조용히 버립니다. 유일한 예외는 중립 차트 색 $4F로, 도움 없이도 검은색으로 해석됩니다. 나머지 토큰의 실제 RGB 값이 필요하면 긴 오버로드에 TXLSTryResolveSystemColor 메서드를 넘겨 UI, 내보내기, 헤드리스 서비스에 맞는 자체 정책을 적용하십시오:

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
  // 헤드리스 내보내기: 현재 데스크톱 테마 대신 고정 색
  Result := True;
  case AColor of
    xscrWindowText, xscrChartText, xscrToolTipText:
      AArgb := $FF000000;
    xscrWindowBackground, xscrChartBackground:
      AArgb := $FFFFFFFF;
  else
    Result := False;
  end;
end;

var
  Colors: TExportColors;   // 내보내기 코드가 한 번 만들어 둔다
  Res: TXLSIndexedColorResolution;
begin
  // $41은 채우기에 유효한 시스템 배경 토큰
  if Workbook.ResolveIndexedColor($41, xicsBiffIcv, xicrFillBackground,
    Colors.ResolveSystemColor, Res) then
    PaintBackground(Res.ARGB)        // 여러분의 그리기 루틴
  else if Res.Kind = xickInvalid then
    RejectToken(Res.RawIndex);       // 여러분의 오류 처리
end;

RGB 거리는 왜 잘못된 팔레트 항목을 고르나?

sRGB 채널 값은 감마 인코딩되어 있어서 숫자의 같은 간격이 사람이 보는 같은 간격이 아니며, 오차는 회사 템플릿이 좋아하는 어둡고 채도 높은 색조에서 가장 큽니다. RGB에서 $000033은 검은색에서 51, 네이비에서 77이므로 검은색이 이깁니다. 각 채널을 선형화하고 색을 OKLab으로 투영하면 제곱 거리는 검은색까지 약 0.0312, 네이비까지 약 0.0235가 되어 네이비가 이깁니다. 매칭 결과는 물리 슬롯 18의 ColorIndex 11이고 Classic과 XLSX 엔진이 같은 답을 줍니다

HotXLS가 짙은 파랑 $000033을 팔레트에 대응시키는 방식: 감마 인코딩된 RGB의 유클리드 거리는 검은색까지 51, 네이비까지 77이어서 헤더가 검게 칠해지지만, 제곱 OKLab 거리 0.0312와 0.0235는 NearestIndexedColor가 물리 슬롯 18의 ColorIndex 11인 네이비를 고르게 만듦
감마 인코딩된 채널 값은 RGB 거리를 사람이 보는 것의 부실한 대리물로 만듭니다. 그래서 HotXLS는 한 번 OKLab으로 변환하고 평범한 제곱 유클리드 비교로 팔레트 스캔을 돌립니다

기본 팔레트에는 주름이 하나 있습니다. 네이비는 ColorIndex 11에 한 번, 25에 또 한 번 나오므로 $000033은 둘에게 정확히 같은 거리입니다. 다른 순서로 스캔하거나 이하 비교를 쓰는 매처는 빌드마다 25를 골랐다 11을 골라, 생성된 두 파일이 diff에서 어긋납니다. HotXLS는 슬롯 8부터 위로 엄격한 미만 비교로 스캔하므로 동점은 항상 낮은 인덱스로 갑니다

OKLab은 의도된 절충입니다. CIEDE2000은 일부 색조 쌍을 더 정확히 순위 매기지만, 조각별 보정 때문에 느리고 반복 루프 안에서 이유를 따지기 어렵습니다. OKLab은 색 하나당 선형화 한 번, 작은 행렬 곱 두 번, 세제곱근 셋이면 끝나고, 그 뒤의 평범한 제곱 거리는 지각 차이의 충분한 대행자입니다. 색 배정 때마다, 그리고 클러스터링 패스 안에서 돌아가는 56항목 팔레트 스캔에는 더 싸고 매끄러운 척도가 옳은 선택입니다

NearestIndexedColor는 무엇을 돌려주나?

NearestIndexedColor는 ARGB 값을 받아 TXLSNearestIndexedColorMatch 레코드를 채우며 통합 문서를 바꾸지 않습니다. 통합 문서마다 56개 팔레트 색을 OKLab 좌표와 팔레트 세대 카운터와 함께 쥐고 있으므로, 질의 하나는 입력 변환 한 번과 고정된 스캔 한 번으로 끝납니다. 팔레트를 리셋하면 그 캐시가 다시 만들어지고, 항목 하나를 바꾸면 그 슬롯만 갱신되며, 현재 세대와 어긋난 캐시에 대한 질의는 추측 대신 False를 돌려줍니다

알파는 좁은 규칙을 따릅니다. 알파 바이트 0은 불투명으로 취급되는데, 이는 대부분의 Delphi 코드가 평범한 RGB 값을 넘기는 방식과 맞고, $FF는 정의상 불투명입니다. 그 사이는 전부 거부됩니다. 함수는 ColorIndex 0과 PaletteSlot -1로 False를 돌려줍니다. 팔레트 항목은 투명도를 저장할 곳이 없습니다

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;

같은 매처가 RGB 세터 뒤에도 앉아 있습니다. Classic 통합 문서에서 Interior.Color, Font.Color, Border.Color를 대입하면 XFExt 레코드용으로는 정확한 색을 보존하고 XF 레코드에 저장될 인덱스는 이 스캔으로 고르며, 채우기와 테두리 라이터도 RGB와 테마 색을 같은 방식으로 인덱스에 해석합니다. NearestIndexedColor가 어떤 색에 대해 보고하는 것이 곧 인덱스 전용 리더가 보여줄 슬롯입니다

트루컬러 통합 문서를 56슬롯에 맞추기

색 몇 개라면 기본 팔레트 매칭으로 충분합니다. 서로 구별이 안 되는 파랑 열둘을 지닌 브랜드 템플릿에는 더 필요합니다. 팔레트 자체가 바뀌어야 하는데, BuildBiffPalettePlan은 56슬롯 전체에 대한 완전한 제안을 계산하고 통합 문서는 그대로 둡니다. 로그로 남기고, 임계값과 비교하고, 버리는 것은 여러분 몫입니다

플래너는 ScanIndexedColorUsage에서 시작합니다. 폰트, 채우기, 테두리, 조건부 서식, 도형, 주석, 차트 폰트, 워크시트 눈금선이 인덱스로 참조하는 슬롯은 잠깁니다. 그 항목을 바꾸면 인덱스를 쓰는 모든 소비자가 한꺼번에 재착색되기 때문입니다. 대상은 폰트, 채우기, 그라디언트 정지점, 테두리, 조건부 서식 스타일, 데이터 막대, 색 눈금이 제시하는 직접 RGB와 해석된 테마 색입니다. 대상마다 참조 횟수와 정의 횟수 중 큰 쪽으로 가중치가 매겨지고, 조건부 서식은 범위가 덮는 모든 셀을 세므로 열 전체에 칠한 채우기가 주석 하나에 쓴 색을 능가합니다. 배치는 이어서 고정된 순서를 따릅니다:

  • 잠긴 슬롯은 현재 색을 무조건 유지합니다
  • 팔레트에 이미 존재하는 대상은 가장 낮은 매칭 슬롯을 유지하고 그 슬롯은 고정됩니다
  • 남은 고유 대상이 여유 슬롯에 들어맞으면 각각 정확한 슬롯을 받고 ARGB 오름차순으로 배정됩니다
  • 그렇지 않으면 Quantized가 설정되고, 각 여유 슬롯은 가장 가까운 기존 중심까지의 거리에 가중치를 곱한 값이 가장 큰 대상으로 씨앗이 뿌려지며, 가중치가 적용된 k-means가 OKLab에서 최대 16 라운드 동안 할당이 더 바뀌지 않을 때까지 여유 중심만 움직입니다
트루컬러 통합 문서의 HotXLS 팔레트 파이프라인: ScanIndexedColorUsage가 폰트, 채우기, 테두리, 조건부 서식, 도형, 주석, 눈금선이 참조하는 모든 슬롯을 잠그고, BuildBiffPalettePlan이 정확한 색을 ARGB 오름차순으로 놓거나 OKLab에서 가중치 적용 k-means를 최대 16 라운드 돌리며, ApplyBiffPalettePlan이 쓰기 전에 세대와 FNV-1a 해시를 검증함
플래닝은 읽기 전용이고 반복 가능합니다. 플랜은 자기 손상을 WeightedError와 MaxDistanceSquared로 보고하고, 플랜은 사실상 일회용이므로 낡은 플랜은 팔레트를 건드리지 않은 채 거부됩니다

오버플로 경로는 냉정하게 읽을 필요가 있습니다. 여기서 k-means는 경계가 있는 국소 최적화이고, 여유 슬롯은 최후에 클램프까지 거쳐 sRGB로 되돌려진 클러스터 중심을 담으므로 어느 셀도 그대로 쓰지 않은 색일 수 있습니다. 플래너가 확실히 주는 것은 재현성입니다. 같은 통합 문서는 언제나 같은 플랜을 냅니다. 플랜은 또 얼마나 휘어야 했는지 WeightedError, MaxDistanceSquared, ExactTargetWeight, TotalTargetWeight로 보고하므로, 근사가 브랜드 가이드라인에 비해 너무 거칠어지면 배치 작업이 저장을 거부할 근거가 생깁니다. Targets 배열은 입력 색마다 어느 슬롯으로 사상되는지와 제곱 거리를 보여줍니다

const
  MaxAcceptedError = 0.002;   // 여러분의 임계값, 제곱 OKLab 단위
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를 돌려줍니다. 플랜은 56개 소스 색에 대한 64비트 FNV-1a 해시인 SourcePaletteGeneration과 SourcePaletteHash를 실어 다닙니다. 검증은 또 모든 공개 및 물리 인덱스와 모든 소스 색을 다시 확인하고, 잠긴 슬롯이 변경으로 표시되지 않았는지, 잠김 수와 변경 수가 맞아떨어지는지, 모든 대상이 불투명한지 확인합니다

플래닝과 적용 사이의 실제 팔레트 변화는 플랜을 낡게 만들며, 같은 플랜의 성공한 이전 적용도 포함됩니다. 플랜은 실제로 일회용입니다. 만들고, 살피고, 적용하고, 또 필요하면 다시 만드십시오. 변경된 슬롯이 없는 유효한 플랜은 세대를 올리지 않고 성공합니다. 실제 변화는 세대를 한 번 올리고 매처를 한 번 새로 고칩니다. Classic 엔진에서는 새 색이 팔레트에 곧장 들어가고, XLSX 엔진에서는 사용자 정의 인덱스 색이 되어 HasCustomIndexedColor와 CustomIndexedColorCount가 보고하고 XLSX 라이터가 indexedColors 블록으로 내보냅니다

XLS 저장과 XLSX-to-XLS 변환에서 옵트인하기

BiffPaletteSavePolicy 속성의 기본값은 xbpsPreserve이므로 통합 문서의 팔레트는 요청할 때만 바뀝니다. xbpsOptimizeTrueColors로 두면 Classic 통합 문서가 SaveAs 안에서 새 플랜을 만들어 적용하는데, 대상 형식이 xlExcel97일 때만 그렇습니다. BIFF5, CSV, HTML, PDF, XLSX 등 다른 라이터는 이 설정을 무시합니다. 저장에 성공하면 최적화된 팔레트가 통합 문서에 남아 이후의 질의와 저장이 같은 사상을 봅니다. 저장이 실패하거나 취소되거나 예외를 던지면 원래 56색과 원래 세대가 복원됩니다

생성되는 리포트라면 최적화 정책을 켜십시오. 그런 통합 문서는 RGB와 테마 색으로 만들어지고 누구도 팔레트를 손으로 다듬지 않았으며, 이 정책을 켜면 인덱스 전용 리더가 훨씬 원본에 가깝게 보여줍니다. 팔레트가 계약의 일부인 통합 문서에는 xbpsPreserve를 유지하십시오. 하류 코드가 ColorIndex로 편집하는 템플릿처럼, 슬롯이 조용히 색을 바꾸면 그 코드가 깨지는 경우입니다

XLSX 소스는 다른 길을 갑니다. lxXlsxExport의 SaveXLSXWorkbookAsXLS는 로드된 TXLSXWorkbook에서 플랜 하나를 만들어 스타일을 변환하기 전에 목적지 팔레트에 써넣으므로, 변환되는 모든 셀이 최종 팔레트와 대조됩니다. 통합 문서 감사 및 변환 워크벤치 데모가 정확히 이 경로를 돌립니다. 테마 색은 tint가 RGB로 해석된 뒤 같은 플래너를 지나고, 살아 있는 테마 참조로 남아야 할 차트 채우기는 별개 사례로 GelFrame 테마 색 차트 채우기 문서에서 다룹니다. 바이너리 XLS는 평탄화된 색 대신 방식 인덱스를 저장하는 곳입니다

// Classic 통합 문서: 옵트인, 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
    if SaveXLSXWorkbookAsXLS(XWorkbook, 'report.xls') <> 1 then
      HandleSaveFailure;
finally
  XWorkbook.Free;
end;

팔레트 호출들(ResolveIndexedColor, NearestIndexedColor, ScanIndexedColorUsage, BuildBiffPalettePlan, ApplyBiffPalettePlan)은 IXLSWorkbook과 TXLSXWorkbook에서 같은 시그니처를 가지며, Delphi와 C++Builder에서 동작합니다. HotXLS Delphi Excel 컴포넌트 페이지의 평가판 빌드만으로도 가장 색이 풍부한 템플릿에 플랜을 돌려 아무것도 쓰이기 전에 MaxDistanceSquared를 읽을 수 있습니다