EDIT: 13th July 2026 - A new v2.1, minor script update!
/* Calculates the CIE colour difference (dE76/dE94/dE2000) between either:
- the Foreground and Background colors or - Color Sampler 1 and Color Sampler 2 on the active document or - Manual float entry to two decimal places (Photoshop Lab mode natively supports integers)
Stephen Marsh https://github.com/MarshySwamp/CIE_Color_Difference_Calculator https://community.adobe.com/t5/photoshop-ecosystem-discussions/how-does-photoshop-calculate-lab-values/m-p/15028840
Changelog: v1.0 - 10th December 2024: Private testing. v1.1 - 10th December 2024: Initial public release, no GUI. v1.2 - 31st May 2026: Added (LCh) Lightness, Chroma & hue readings. v1.3 - 1st June 2026: Replaced the native alert with a ScriptUI dialog with copy to clipboard button. v1.4 - 9th June 2026: Added Delta L, a, b, C, h component breakdown & dE traffic light colouring. v1.5 - 17th June 2026: Added editable L, a, b floating value input fields for foreground and background, removed the traffic light colouring due to legibility issues. v1.6 - 1st July 2026: Combined the separate dE76/dE94/dE00 scripts into a single script with radio buttons to switch between the three formulas. v1.7 - 1st July 2026: Changed the colour source from the foreground/background swatches to Color Sampler 1 and Color Sampler 2 on the active document. v1.8 - 1st July 2026: Combined v1.6 and v1.7 into a single script. Added a checkbox to toggle between Foreground/Background and Color Sampler 1 and 2 as the colour source. v1.9 - 3rd July 2026: Minor GUI change, moved the "Enable manual entry" checkbox under the "Use Color Samplers" checkbox. v1.10 - 5th July 2026: Replaced the two source checkboxes with three radio buttons (Foreground/Background, Color Samplers and Manual Entry). The initial Foreground/Background Lab values are stored once and used to restore when switching back from samplers or manual entry. v1.11 - 11th July 2026: Minor GUI change, moved the Manual Entry input panel into the Color Source panel. v2.0 - 12th July 2026: Added range validation/clamping to the Manual Entry fields. v2.1 - 13th July 2026: Fixed a rounding display error in the results panel. Minor GUI changes. */
#target photoshop
main();
function main() {
// ----------------------------------------------------------------------- // Start with the Foreground / Background colors - always available, // no document or color samplers required. // ----------------------------------------------------------------------- var fgColor = app.foregroundColor.lab; var bgColor = app.backgroundColor.lab;
// ----------------------------------------------------------------------- // Initial Foreground/Background Lab values. These are kept for the lifetime of the // dialog so that the Foreground/Background source can always be restored exactly, // even after the user has switched to the Color Sampler or Manual Entry sources. // ----------------------------------------------------------------------- var initialFgLab = [Math.round(fgColor.l), Math.round(fgColor.a), Math.round(fgColor.b)]; var initialBgLab = [Math.round(bgColor.l), Math.round(bgColor.a), Math.round(bgColor.b)];
// Tracks which colour source is currently active: "fgbg", "sampler", or "manual" var colorSourceMode = "fgbg";
// ----------------------------------------------------------------------- // ScriptUI Dialog // ----------------------------------------------------------------------- var win = new Window("dialog", "CIE Color Difference Calculator (v2.1)"); win.alignChildren = "fill"; win.spacing = 12; win.margins = 12;
var cancelBtn = btnGroup.add("button", undefined, "Cancel", { name: "cancel" }); var copyBtn = btnGroup.add("button", undefined, "Copy");
// ----------------------------------------------------------------------- // Helper: returns the currently selected dE mode as a string // ----------------------------------------------------------------------- function getSelectedMode() { if (rbdE76.value) return "dE76"; if (rbdE94.value) return "dE94"; return "dE00"; }
// ----------------------------------------------------------------------- // Helper: updates all static text (heading, panel titles, help tips, // summary label prefixes) to match the active colour source // ----------------------------------------------------------------------- function updateLabelsForMode() { if (colorSourceMode === "sampler") { headingText.text = "Color Sampler 1 vs. Color Sampler 2 Difference:"; } else if (colorSourceMode === "manual") { headingText.text = "Manual Entry 1 vs. Manual Entry 2 Difference:"; } else { headingText.text = "Foreground vs. Background Color Picker Difference:"; } }
// ----------------------------------------------------------------------- // Central recalculate-and-refresh function // ----------------------------------------------------------------------- function recalculate() {
var fL = manOneL.value; var fA = manOneA.value; var fB = manOneB.value; if (isNaN(fL)) { fL = 0; } if (isNaN(fA)) { fA = 0; } if (isNaN(fB)) { fB = 0; }
var bL = manTwoL.value; var bA = manTwoA.value; var bB = manTwoB.value; if (isNaN(bL)) { bL = 0; } if (isNaN(bA)) { bA = 0; } if (isNaN(bB)) { bB = 0; }
var currentFgLab = [fL, fA, fB]; var currentBgLab = [bL, bA, bB];
var currentFgLCH = labToLCH(currentFgLab); var currentBgLCH = labToLCH(currentBgLab);
var mode = getSelectedMode(); var dE, label;
if (mode === "dE76") { dE = calculateCIEdE76(currentFgLab, currentBgLab); label = "\u0394E76"; } else if (mode === "dE94") { dE = calculateCIEdE94(currentFgLab, currentBgLab); label = "\u0394E94"; } else { dE = calculateCIEdE00(currentFgLab, currentBgLab); label = "\u0394E00"; }
// ----------------------------------------------------------------------- // v2.1 - Result value formatting. // fmtComponent: for raw L*/a*/b* values and their direct deltas // (deltaL/deltaA/deltaB are plain differences, so they stay integer // whenever the inputs are integer). FG/BG and Color Sampler inputs // are always whole numbers in Photoshop, so those modes display as // integers; Manual Entry keeps 2 decimal places. // fmt: for derived values (C*, h*, dE, deltaC, deltaH) which involve // sqrt/trig and are not guaranteed to be whole numbers even when the // L*/a*/b* inputs are integers - always shown to 2 decimal places. // ----------------------------------------------------------------------- function fmtComponent(val) { if (colorSourceMode === "manual") { return val.toFixed(2); } return Math.round(val).toString(); }
function fmt(val) { return val.toFixed(2); }
var deltaL = fL - bL; var deltaA = fA - bA; var deltaB = fB - bB; var deltaC = currentFgLCH.C - currentBgLCH.C;
// ----------------------------------------------------------------------- // Hue angle difference, normalised to [-180, +180] // ----------------------------------------------------------------------- var deltaH = currentFgLCH.H - currentBgLCH.H; if (deltaH > 180) { deltaH -= 360; } if (deltaH < -180) { deltaH += 360; }
// ----------------------------------------------------------------------- // Update dE label text // ----------------------------------------------------------------------- deText.text = label + ": " + fmt(dE);
// ----------------------------------------------------------------------- // Update footer summary labels using the current color source's prefixes // ----------------------------------------------------------------------- var prefixes = getSummaryPrefixes();
// ----------------------------------------------------------------------- // Radio buttons: switch dE formula and recalculate live // ----------------------------------------------------------------------- rbdE76.onClick = recalculate; rbdE94.onClick = recalculate; rbdE00.onClick = recalculate;
// ----------------------------------------------------------------------- // Radio buttons: switch between Foreground/Background, Color Sampler 1 & 2, // and Manual Entry as the colour source. Only one can be active at a time. // -----------------------------------------------------------------------
// Start disabled - only Manual Entry allows direct editing of the fields manOneInputPanel.enabled = false; manTwoInputPanel.enabled = false;
// Foreground / Background - restores the Lab values captured at script // start, so the original swatches are always recoverable. rbFgBg.onClick = function () { manOneL.value = initialFgLab[0]; manOneA.value = initialFgLab[1]; manOneB.value = initialFgLab[2]; manTwoL.value = initialBgLab[0]; manTwoA.value = initialBgLab[1]; manTwoB.value = initialBgLab[2];
// ----------------------------------------------------------------------- // Color Samplers - validates that a document with at least 2 color // samplers is available before committing to this mode. // ----------------------------------------------------------------------- rbSampler.onClick = function () {
if (!app.documents.length) { alert("No document open.\n\nOpen a document and place at least 2 color samplers before using this mode."); rbFgBg.value = true; colorSourceMode = "fgbg"; return; }
var activeDoc = app.activeDocument;
if (activeDoc.colorSamplers.length < 2) { alert("At least 2 color samplers are required.\n\n" + "Currently found: " + activeDoc.colorSamplers.length + "\n\n" + "Add color samplers with the Color Sampler tool (I) and try again.\n" + "This mode compares Color Sampler 1 vs. Color Sampler 2."); rbFgBg.value = true; colorSourceMode = "fgbg"; return; }
// ----------------------------------------------------------------------- // Validation passed - pull the Lab values from the two color samplers // ----------------------------------------------------------------------- var sampler1 = activeDoc.colorSamplers[0].color.lab; var sampler2 = activeDoc.colorSamplers[1].color.lab;
// ----------------------------------------------------------------------- // Manual Entry - unlocks both input panels for free editing. The values // already showing (whichever source was active) are left in place as a // convenient starting point for manual adjustment. // ----------------------------------------------------------------------- rbManual.onClick = function () { colorSourceMode = "manual"; manOneInputPanel.enabled = true; manTwoInputPanel.enabled = true;
updateLabelsForMode(); recalculate(); };
// ----------------------------------------------------------------------- // Manual entry field validation - clamps each field to its valid Lab // range and rounds to 2 decimal places, then triggers a live recalculate. // L*: 0.00 to 100.00 a*/b*: -128.00 to 127.00 as editable floats. // ----------------------------------------------------------------------- function roundTo2(val) { return Math.round(val * 100) / 100; }
function makeManualFieldValidator(field, min, max) { return function () { var v = field.value; if (isNaN(v)) { v = 0; } v = roundTo2(v); if (v < min) { v = min; } if (v > max) { v = max; } field.value = v; recalculate(); }; }
// ----------------------------------------------------------------------- // Set onChange on each editnumber field to validate/clamp and then // trigger a live recalculate // ----------------------------------------------------------------------- manOneL.onChange = makeManualFieldValidator(manOneL, 0, 100); manOneA.onChange = makeManualFieldValidator(manOneA, -128, 127); manOneB.onChange = makeManualFieldValidator(manOneB, -128, 127); manTwoL.onChange = makeManualFieldValidator(manTwoL, 0, 100); manTwoA.onChange = makeManualFieldValidator(manTwoA, -128, 127); manTwoB.onChange = makeManualFieldValidator(manTwoB, -128, 127);
copyBtn.onClick = function () { var d = new ActionDescriptor(); d.putString(stringIDToTypeID("textData"), win._alertText || ""); executeAction(stringIDToTypeID("textToClipboard"), d, DialogModes.NO); win.close(); };
// ----------------------------------------------------------------------- // Populate all display fields with the initial Photoshop values // ----------------------------------------------------------------------- updateLabelsForMode(); recalculate();
win.show(); }
// ----------------------------------------------------------------------- // LCh - converts a LAB array [L, a, b] to an LCh object { L, C, H } // ----------------------------------------------------------------------- function labToLCH(lab) { var L = lab[0]; var C = Math.sqrt(lab[1] * lab[1] + lab[2] * lab[2]); var H = (Math.atan2(lab[2], lab[1]) * 180) / Math.PI; if (H < 0) { H += 360; } return { L: L, C: C, H: H }; }
// ----------------------------------------------------------------------- // I'm unsure of the source for the dE formula's, however, I'm guessing // that credit should go to Bruce Lindbloom: // http://www.brucelindbloom.com/Eqn_DeltaE_CIE76.html // http://www.brucelindbloom.com/Eqn_DeltaE_CIE94.html // http://www.brucelindbloom.com/Eqn_DeltaE_CIE2000.html // http://www.brucelindbloom.com/ColorDifferenceCalc.html // -----------------------------------------------------------------------
// ----------------------------------------------------------------------- // dE76 (CIE76 / dEab) - simple Euclidean distance in Lab space // ----------------------------------------------------------------------- function calculateCIEdE76(lab1, lab2) { var deltaL = lab1[0] - lab2[0]; var deltaA = lab1[1] - lab2[1]; var deltaB = lab1[2] - lab2[2]; return Math.sqrt(deltaL * deltaL + deltaA * deltaA + deltaB * deltaB); }
// ----------------------------------------------------------------------- // dE94 (CIE94) - Formula improved for perceptual uniformity // ----------------------------------------------------------------------- function calculateCIEdE94(lab1, lab2) { var kL = 1, kC = 1, kH = 1; var K1 = 0.045, K2 = 0.015;
var deltaL = lab1[0] - lab2[0]; var deltaA = lab1[1] - lab2[1]; var deltaB = lab1[2] - lab2[2];
It might help to understand the quote below from the book “Real World Photoshop CS3” which was published in 2008. It also appeared in the book’s earlier editions by the late Bruce Fraser, as well as his book “Real World Color Management” published in 2003. Bruce didn’t just say that out of thin air. He worked closely with the Photoshop team.
The second quote, below, is from page 33 of “The Digital Print” (2014) by Jeff Schewe, who also works closely with the Photoshop, Camera Raw, and Lightroom teams.
Anyone who studies how Photoshop color works eventually understands that:
Lab is its reference color space for color conversions.
Any color conversions must be corrected for the specific color space of the current color mode (such as sRGB vs Adobe RGB, or FOGRA CMYK vs US SWOP CMYK).
If you show an Excel spreadsheet or color conversion table that doesn’t account for color space, or doesn’t use Lab as a reference color space, that conversion table is not useful or reliable when discussing Photoshop.
'If you show an Excel spreadsheet or color conversion table that doesn’t account for color space, or doesn’t use Lab as a reference color space, that conversion table is not useful or reliable' when discussing Photoshop.
Make 2 separate RGB files, one sRGB, the other ProPhoto RGB. Type in the same set of RGB values and compare the Lab readings. Type in the same Lab values and compare the RGB readings.
I appreciate the time you invested to respond to my question. However, you answered a question I didn't ask. I asked how does Photoshop calculate the numbers that appear in the color picker when a user enters RGB values. I've found formulas online that purport to do it, but they're written in code or use calculus. I took calculus around 40 years ago and have never needed to use it until now. I need to calculate the Delta-E between multiple colors, and plugging them into a website form one by one takes too much time. I'm too old to waste it on that kind of stuff, know what I mean?
We did answer it. The answer is color management. The Lab numbers are always there as the definition of the RGB numbers in the given RGB color space.
The online calculators are rubbish because they usually don't even consider color management, color spaces and icc profiles. RGB what? There is no such thing as "RGB". Numbers are undefined until associated with a specific color space.
Look at the screenshot I posted below. That's how given Lab numbers are represented in different color spaces.