chardata

Free color-data explorer

An interactive tool for inspecting and comparing CGATS and CSV datasets — with spectral plotting, G7 validation, and colorimetry generation. Built as a Node.js app. Free to access and use.

✓ Free to use Open source
Latest release: 1.8.0 — May 31, 2026

CharData 1.8.0

CxF/X-3 (Color Exchange Format, ISO 17972-3)

  • Read CxF/X-3 as a third characterisation data format alongside CSV and CGATS/IT8. Prefix-agnostic XML parsing maps Object name → sample label, ColorCIELab → L\*a\*b\*, ReflectanceSpectrum → spectral columns, and ColorCMYK/ColorCMYKPlusN/ColorRGB → device colorants.
  • Write/Export a full CxF/X-3 document (device colorants + CIELab + spectrum + a ColorSpecification derived from the current illuminant / observer / M-condition).

Measurement-only datasets

Datasets with L\*a\*b\* and/or spectral data but no device colorants (common in CxF, also valid for CGATS/CSV) now load and support:

  • the 3D L\*a\*b\* point cloud (sample label on hover),
  • the 2D gamut-slice point cloud (projected, no boundary — like image-derived clouds),
  • the data table with a sample-label column,
  • Compare matched by sample label.

Gamut shell, Extract, Tone Value, Estimate, and G7 validation are unavailable for these (no device→colour relationship to model).

Other

  • New UI strings localised across all 12 languages (+ translation matrices); fixed the measurement-only notice so it re-localizes live on language change.
  • Refreshed the APTEC_CMYKOGV_Coated_LinearCTV_2025_M1 standard dataset (corrected CGATS header field/set counts).
  • Manual and in-app help updated.
Older releases
1.7.3 — May 23, 2026

The ICC-vs-ICC branch of Compare was previously hardcoded to 4-channel

CMYK — two NCLR profiles refused with "ICC vs ICC comparison requires

CMYK profiles." That guard also masked a latent alignment bug

(#2): the

source-order remap used a fixed CMYK index, so any future NCLR support

would have silently mis-aligned non-CMYK inks.

This release lifts the restriction for the well-defined case, and

replaces the "CMYK only" error with a targeted message for the cases

that remain ill-defined.

What changed

------------

In runCompareWithIcc, the both-ICC branch now:

  • Requires both profiles to expose identical ordered colorant lists

(e.g. two CMYKOGV proofers). When they don't match, the comparison is

refused with a message that names both lists.

  • For 4-channel CMYK, continues to evaluate against the standard

IT8.7/5 patch set (unchanged).

  • For any other matching colorant list, evaluates against a generated

N-channel patch set: paper white, single-ink ramps, and a two-ink

overprint grid that thins with channel count to keep patch counts

bounded (N=5 → 591 patches, N=7 → 666, N=10 → 821, N=15 → 1831).

  • Drops the previously hardcoded ['CYAN','MAGENTA','YELLOW','BLACK']

source-order constant in favour of a single ICC-slot lookup — the

deep-equal guard makes that sufficient across every branch.

Wording

-------

  • icc_vs_icc_cmyk_onlyicc_vs_icc_colorant_mismatch with {a} /

{b} placeholders for the two colorant lists.

  • Translations refreshed in all 12 supported UI languages (en, fr, de,

it, es, pt-PT, pt-BR, zh-CN, zh-TW, ja, ko, sv) and in the canonical

translations/Eng-*.xlsx spreadsheets. Eng-Pt.xlsx and

Eng-Zh.xlsx pick up the previously-empty pt-BR / zh-TW columns for

this row in passing.

Manual

------

MANUAL.md §5 (Compare mode) bullet for ICC vs ICC rewritten to

describe the colorant-list requirement and the CMYK / NCLR patch sets.

Closes #2.

1.7.2 — May 21, 2026

The Image tab is now always visible — Explore as well as Compare — and the

3D plot and 2D slice can render the image trace on its own, with no

Dataset A loaded, whenever the image carries an embedded ICC profile or

is a Lab TIFF. Removing the image (or unloading the last dataset) hides

both plots again.

Bind dropdown

-------------

The dataset-binding dropdown now adapts to what is actually bindable:

  • If the image has an embedded ICC profile, empty dataset slots are

omitted entirely — only the embedded entry and any loaded datasets

appear.

  • If the image has no embedded profile but at least one dataset is

loaded, empty slots still appear with a - placeholder so the slot

positions remain visible.

  • If the image has no embedded profile and no datasets are loaded,

the dropdown is empty and disabled.

Plot defaults

-------------

  • 2D slicer Show data points now defaults to ON (the prior default

hid the patch cloud, which is the most common reason people open the

slicer in the first place).

Wording

-------

  • Bind-error message reworded for the new behaviour:

Dataset {slot} is not loaded.No color dataset loaded.

  • Translations refreshed in all 12 supported UI languages

(en, fr, de, it, es, pt-PT, pt-BR, zh-CN, zh-TW, ja, ko, sv) and in

the canonical translations/Eng-*.xlsx spreadsheets.

Manual

------

MANUAL.md §5.4 (Image gamut) updated to reflect the new always-visible

tab, the dropdown rules, and the image-only plotting case. The §4.4

slicer table notes the new Show data points default.

🤖 Generated with Claude Code

1.7.1 — May 21, 2026

Adds an Image tab in Compare mode that loads a real image file and plots its

colour gamut alongside the loaded Dataset A / Dataset B traces in the 3D plot

and 2D slice. Colorimetry is derived from the image's embedded ICC profile

when one is present, and falls back to channel-count inference otherwise.

Lab TIFFs plot in their native L*a*b* coordinates with no transformation.

Supported image formats

-----------------------

  • PNG Embedded iCCP profile honoured (zlib-inflated via

DecompressionStream); Gray and RGB via Canvas.

  • JPEG Embedded APP2 ICC profile honoured. Gray/RGB via Canvas; CMYK

decoded via UTIF's bundled JpegDecoder (PDF.js lineage), which

handles Adobe APP14 YCCK and emits CMYK in normal orientation.

  • TIFF Embedded ICC profile (tag 34675); 8 / 16-bit Gray, RGB, CMYK,

and NCLR. PhotometricInterpretation 8 / 9 (CIELAB / ICCLab) is

auto-detected and plotted as Lab.

  • BMP Indexed and 16 / 24 / 32-bit decoded as RGB via Canvas. V5 ICC

tag not yet read.

  • GIF Palette → RGB via Canvas; first frame only for animated GIFs.

(GIF has no embedded ICC support in the format.)

  • PDF Photoshop-generated PDFs only — strict 'Adobe Photoshop' check

on /Info Producer or Creator. Single-page; first image XObject

extracted. Linearized PDFs are walked via the /Prev xref chain.

Filters: /DCTDecode (incl. CMYK via UTIF) and /FlateDecode with

TIFF Predictor 2 or PNG predictors (10-15). Embedded ICC via

/ICCBased on the image, OR via page-level /DefaultGray,

/DefaultRGB, /DefaultCMYK colour-space override on a Device*-

tagged image (PDF spec §8.6.5.6).

Embedded ICC profile dropdown

-----------------------------

When an image carries an embedded ICC profile, the bind dropdown prepends an

'Embedded: <profile name>' entry and selects it by default. Conversion runs

the image's device pixels through the embedded profile via lcms2 (Absolute

Colorimetric) directly — no dataset binding required, and no family/channel

compatibility check applies (the profile is the image's colour space by

construction). The user can still pick Dataset A or B with the existing

compatibility check.

UI

--

The Image section sits above the 3D plot in Compare mode (collapsed by

default; hidden in Explore mode). Dark-mode styling matches the existing

plot / slice sections. Status messages now render outside the

display:none image-info wrapper so format-unsupported / PDF-rejected /

file-too-large errors are visible regardless of decode success.

Plumbing

--------

  • inflateAsync() thin wrapper over native DecompressionStream('deflate'),

used by PNG iCCP and PDF /FlateDecode (image streams +

/ICCBased ICC streams). Lifts the prior PNG iCCP stub

that returned null.

  • Minimal PDF parser (~300 lines) — classic xref tables, indirect-ref

resolver, dict/array/string lexer, /Prev chain following. Handles

Photoshop's linearized output (xref at offset 116, /Prev to the small

auxiliary table at end of file).

  • decodeCmykJpegViaUtif() shared CMYK decode for standalone .jpg files

and PDF /DCTDecode CMYK streams. CMYK thumb

preview via (1-C)(1-K), (1-M)(1-K), (1-Y)(1-K).

i18n

----

29 new image_* keys added to all 12 locale dictionaries and the nine

Eng-*.xlsx translation spreadsheets. image_format_unsupported lists every

supported format. image_err_cmyk_jpeg removed (CMYK JPEGs are now

supported). check-translations audit clean.

Server

------

CSP img-src extended to allow blob: so URL.createObjectURL() works for

image thumbnails. script-src unchanged. The icctools postMessage handoff

is unchanged.

Docs

----

MANUAL.md §5.4 documents the format table, Photoshop-PDF scope, and the

embedded-ICC parity across formats.

Post-tag translation polish

---------------------------

Three follow-up commits address language-switch gaps discovered after the

initial 1.7.1 tag was cut. Re-tagged in place so 1.7.1 is the single

shipping artifact for the entire image-gamut feature line.

  • Image tab strings written imperatively (colour-space line, points

summary, status messages, "Embedded: …" dropdown entry) now re-render

on language change via renderImageInfo() / repopulateImageBindDropdown()

/ bindImageToCurrentSelection() inside onLangChange().

  • 18 previously untranslated strings ported to t(): file-select handles,

Contact button, file-card delete tooltip, rendering-intent dropdown

(label + 4 options) and ICC summary panel labels, Settings language

"System default" entry, ICC load/parse error prefixes, popup-blocked

alert, file-load failure alerts. New data-i18n-aria attribute handler

added to applyTranslations() for aria-labels.

  • Estimate tab "ICC A2B Evaluation" / "ICC Output", Settings language

"System default (…)", and Compare table no-match error message now all

re-translate on language switch.

  • scripts/generate-help.js TOC updated to include 5.4 Image gamut so the

in-app Help page links to the section.

  • All translation spreadsheets (nine Eng-*.xlsx) carry the new rows;

scripts/check-translations.js reports zero drift.

Image detail (Coarse / Standard / Fine)

---------------------------------------

The Image tab now exposes an "Image detail" dropdown driving the

spatial-bin and dedupe-radius parameters used by the gamut pipeline.

Persists per browser via localStorage; Standard is the default.

- Coarse bins=900 radius=2.0 fastest, sparser trace

- Standard bins=2025 radius=1.0 default

- Fine bins=5000 radius=0.5 denser trace, slower

Changing the level re-runs binImage + dedupeRadial on the cached decoded

pixels and re-binds — no source-file re-decode. Four new image_detail_*

i18n keys added to all 12 locale dictionaries and the nine Eng-*.xlsx

spreadsheets. The opening of MANUAL.md now also calls out the icctools

companion and the in-app Launch editor hand-off.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

1.6.1 — May 15, 2026

Security patch following an audit of the 1.6.0 launch-editor handoff. No new features; defence-in-depth fixes only.

Fixes

  • CGATS validation warnings/errors are now wrapped in escapeHtml before being interpolated into the slot panel's innerHTML. All current messages are literal/numeric so this isn't exploitable today; the change closes the door for a future contributor wiring user-derived text into the validator output.
  • launchIccEditor postMessage targetOrigin is now pinned to the concrete icctools origin (http://localhost:5173 in dev, location.origin in prod) instead of '*'. Profile bytes can no longer be delivered to an unexpected origin if the popup is navigated cross-origin between window.open and the icctools:ready reply.
  • Translation strings documented as trusted HTML near the t() definition, so future maintainers know el.innerHTML = t('foo') callers depend on this invariant.

Companion icctools changes

A parallel hardening commit went out on icctools (https://chardata.colourbill.com/profiletool/) covering: a 256 MB input-size cap on the postMessage launch handler, an origin allowlist on ?source=chardata, a meta CSP locking connect-src to 'self', a parsed-profile cache so describeTag doesn't re-parse on every tag-detail open, and an XML pre-scan that rejects <!DOCTYPE / <!ENTITY declarations to defeat billion-laughs attacks via IccLibXML's XML_PARSE_HUGE flag.

See commits 25ee853 (chardata) and 93a82bd (icctools) for details.

1.6.0 — May 15, 2026

Highlights

  • Launch editor button added to the ICC profile viewer overlay. Opens icctools in a new tab with the current profile pre-loaded — useful for inspecting tag XML/JSON or making round-trip edits.
  • Profile bytes are handed off in-browser via postMessage. No upload, no server round-trip.
  • icctools moved to a new home: https://chardata.colourbill.com/profiletool/ (replaces the old :5173 deploy).
  • icctools redressed with a desktop/mobile responsive layout matching chardata's ≤720 px breakpoint.

Internal

  • Cross-app launch protocol: chardata opens icctools with ?source=chardata, awaits icctools:ready, replies with {type:'icctools:load', filename, bytes}. One-way; icctools owns the editing session.
  • helmet COOP relaxed from same-origin to same-origin-allow-popups so cross-origin popups in local dev retain window.opener. Prod is same-origin so the change is a no-op there.
  • i18n + 9 translation xlsx files updated with icc_launch_editor.
1.5.0 — May 14, 2026

ICC profile viewer

Click Display File on an ICC profile slot to open an in-page Header + Tags viewer (lazy-loaded ~730 KB IccProfLib WASM; chardata's cold load is unchanged). Each tag row expands inline to show its full Describe() output. Mobile-responsive: rows reflow to stacked cards at ≤720 px viewport — no horizontal scroll.

Highlights

  • New icc-viewer-wasm/ module compiled from IccProfLib (iccDEV), shipped lazily as public/wasm/icc-viewer.{mjs,wasm}
  • chardata-gamut.wasm keeps using lcms2 for transforms — no regression to existing ICC eval / mesh / slice
  • Pre-commit hook now rebuilds and stages the ICC viewer WASM whenever icc-viewer-wasm/ is touched
  • i18n strings added to all 12 supported languages and the 9 translation spreadsheets
  • Docs updated (CLAUDE.md, MANUAL.md, README.md)

Tag

1.5.0 — commit 851ccb0

1.4.0 — May 3, 2026

Highlights

ICC profile support

CharData now loads ICC profiles (.icc / .icm) alongside characterization datasets and treats them as virtual datasets evaluated through the profile's A2B (device-to-Lab) transform.

  • Header validation by acsp magic at offset 36; under- or over-sized files are rejected.
  • Supported device classes: Output (printer), Input (scanner), Display (monitor).
  • Supported color spaces: CMYK, CMY, RGB, GRAY, and generic NCLR (2..15 channel) profiles — including extended-gamut printer profiles such as 7CLR OGV (CMYK + Orange/Green/Violet).
  • For NCLR profiles the wrapper reads the colorantTableTag so real ink names ("Cyan", "Orange", "Violet"…) surface in the UI instead of generic CH1..CHn.
  • All four ICC rendering intents are selectable; intents not present in the profile are greyed out.
  • Vendored lcms2 2.19 for parsing and transforms (compiled into the WASM module).

Per-slot Rendering Intent control

Each slot holding an ICC profile gains a Rendering Intent dropdown (default Absolute Colorimetric, falling back to the first intent the profile supports). Switching intent re-runs the full pipeline:

  • 3D gamut shell + IT8.7/5 patch cloud
  • 2D gamut slice
  • Comparison table + summary statistics
  • Tone Value chart (Y-axis range invalidated and re-fit)
  • Estimate prediction

The control is hidden whenever the slot holds char data instead of a profile.

File Select pane redesigned

  • Two collapsible sections — Characterization data and ICC Profiles — with persistent expand/collapse state.
  • Drag-resizable width (vertical grip dots on the right edge) with width remembered across sessions.
  • One picker button + drag-and-drop into the pane; CharData sniffs each file's content and routes it to the correct section automatically.

Comparison table — unified and expanded

The Compare table now uses a single renderer for all four data/profile combinations (data vs data, data vs ICC, ICC vs data, ICC vs ICC) with identical column structure and decimal-aligned colorant cells throughout.

The Difference section now includes:

| Column | Meaning |

|---|---|

| ΔE | Selected ΔE method |

| ΔL\* | Lightness difference (B − A) |

| ΔC\* | Chroma difference (B − A) |

| ΔH\* | Geometric hue distance (chroma-weighted, via the selected ΔE method) |

| Δh\*(deg) | Hue-angle difference, signed shortest path in (−180°, +180°] |

Bold rows are new in 1.4.0.

Tone Value with ICC

For ICC slots, CharData synthesises a primary tone ramp by sampling the profile at fixed tint percentages (0, 5, 10, 15, 20, 25, 30, 40, 50, 60, 70, 80, 90, 95, 100) along each colorant. ICC slots have no spectral data, so only CTV works for them; Murray-Davies traces from ICC slots are skipped.

Estimate

  • Char data: polynomial fit (existing behaviour).
  • ICC profiles: prediction uses the profile's A2B transform directly — no fit step required.

3D plot defaults differ by file type

  • Char dataset → shell off / points on (existing behaviour).
  • ICC profile → shell on / points off (continuous transform, sample cloud less meaningful).

Per-slot checkboxes still let you override either default.

Translations

Refreshed across 11 languages (English, French, German, Italian, Spanish, Portuguese PT/BR, Chinese Simplified/Traditional, Japanese, Korean, Swedish) and the 11 spreadsheet sources under translations/. New strings cover the File Select sections, ICC type label, and ICC error message.

A reusable audit script (scripts/check-translations.js) was added for catching drift between the in-app I18N dict and the canonical Eng-*.xlsx sources.

Documentation

[MANUAL.md](MANUAL.md) and the in-app help (public/help.html) updated with new sections covering ICC profiles, the redesigned File Select pane, the Rendering Intent control, the unified comparison table columns, and the ICC paths through Tone Value and Estimate.

[CLAUDE.md](CLAUDE.md) refreshed to document the WASM/help-html pipeline, pre-commit hook, deployment, and i18n flow for future contributors.

Security

  • escapeHtml applied to file-name and colorant-name interpolations in the Compare and Explore renderers.
  • 32 MB upper bound on ICC profile size to protect the WASM heap from oversized inputs.
  • Deferred items tracked in [SECURITY-FOLLOWUPS.md](SECURITY-FOLLOWUPS.md) — escapeHtml sweep of pre-existing sinks, CSP header in server.js, max-input-size guard.

Operations

  • New /health endpoint returning 200 ok for lightweight uptime monitors. Avoids fetching the full SPA on every check.

Build / infra

  • Pre-commit hook now detects whether it is running inside WSL or on a Windows shell that needs the wsl interop, so contributors can commit from either environment.

Known limitations

  • Comparing two NCLR profiles to each other still aligns colorants assuming CMYK ordering; non-CMYK inks (Orange/Green/Violet etc.) may be dropped or misaligned in the table. Tracked as #2. ICC-vs-CGATS and CMYK-vs-CMYK ICC compares are unaffected.

---

Full diff: 1.3.1...1.4.0

1.3.1 — April 30, 2026

Changes

  • Vendor Plotly 2.27.0: plotly-2.27.0.min.js (3.5 MB) is now served locally instead of from cdn.plot.ly, eliminating SRI breakage when the CDN silently updates bytes for the same version URL.
  • Weekly CDN hash monitor: A GitHub Actions workflow runs every Monday and opens a plotly-cdn-drift issue if cdn.plot.ly diverges from the vendored file, so upgrades can be reviewed and applied deliberately.
1.2.2 — April 29, 2026

Fixes the 'Choose char data file' button being greyed-out and unresponsive in mobile layout (and desktop browser resized to ≤700px).

Root cause: the desktop CSS rules for both drawer blades set z-index:200 after the mobile media query's z-index:1000 in the stylesheet. Equal-specificity rules resolve by source order, so the later desktop value always won — leaving the blades behind the backdrop (z-index:999) on mobile. The backdrop visually darkened the button and consumed all clicks (closing the blade instead of opening the file picker).

Fix: added !important to z-index:1000 in the mobile media query for both #file-select-blade and #blade.

1.2.1 — April 28, 2026

Fixing some issues with multi-localization (incorrect, missing translations, esp Chinese).

Fixing some minor bugs wrt display when resizing browser window.

v1.2.0 — April 28, 2026

What's new since v1.1.5

Modeling

  • Weighted polynomial model — new Settings option (Model → Weighted on/off). Anchors primaries, secondaries, and white point with higher influence during fitting, producing a gamut shell that passes through measured boundary conditions.
  • Model-based gamut for both slots — in Compare mode, both datasets A and B are now fitted with a polynomial model. The 3D gamut shell and 2D gamut slice use the model-synthesised boundary for both slots.

2D Gamut Slice

  • Bandwidth control removed — the model isoline extraction replaces the raw data bandwidth filter in all modes.

3D Plot UX

  • Controls panel moved below the Plotly canvas (no longer overlaid on the plot).
  • Computing indicator repositioned to top-right, clear of dataset controls.
  • Computing indicator now shown for all Plotly update operations (shell toggle, points toggle, color mode, opacity, lighting).
  • Fixed ghost shell rendering: shell traces now use visible: false when disabled, not opacity: 0, preventing spurious alphahull computation.
  • Fixed vertex color count mismatch in recolorPlot when model boundary cloud and raw data have different point counts.

Settings

  • New Model section (between Spectral → LAB and Display).
  • Filter Duplicates now defaults to No.

Localisation

  • Multi-language support for all new UI strings (EFIGS, CJK, Portuguese).
1.2.0 — April 28, 2026

Updated modeling to include ability to extra-weight primaries and secondaries for gamut calculation.

Updated 3d plot of gamut and gamut-slicer to use model, rather than convex hull of characterization data.

Added basic (EFIGS+P+CJK) multi-localized support. (no update for help file yet)

1.1.5 — April 22, 2026

Added Murray-Davies and CTV generation

1.1.4 — April 17, 2026

Replaced check for hard presence of CMYK device colorants - this is not necessary when just examining data files, especially ECG ones, which may or may not have CMYK.

1.1.3 — April 17, 2026

Improved performance of gamut shell visualization through pre-sorting of points.

Added Estimate section using simple modeling.

1.1.2 — April 14, 2026

Modified behaviour of gamut slicer visibility.

Modified behaviour of show/hide gamut shells in 3d Plot.

Added dark mode.

Modified data table behaviour.

Many changes to 3d plot to improve performance.

1.1.1 — April 14, 2026

Added functionality to display gamut shell slices.

1.1.0 — April 14, 2026

Added gamut shell visualization capability for both Explore and Compare modes.

Cleaned up some usability issues, especially for mobile.

1.0.0 — April 13, 2026

First public release of chardata app.

Chardata is a node.js app designed to do simple inspection and comparison of color characterization data sets created using the CGATS file format (CSV also accepted), with or without spectral information.

If colorimetric information is missing, Chardata will use the spectral info to generate colorimetry.

Density information is discarded for now.

Plotting and comparison functions are available.

G7 validation is available with a detailed report.

Spectral plotting of single data points is also available.

Color blog: posts about chardata and color science ↓

profiletool

In-browser ICC profile inspector & editor

A browser-based companion to chardata for inspecting and editing ICC profile internals — headers, tags, and type-specific data. Runs entirely client-side; no uploads. Free to access and use.

✓ Free to use Open source