Debugging Colour Shift Between Screen and Print Exports

Sample the same pixel from both files and compute a perceptual distance before doing anything else, because “the print looks warmer” is consistent with four different faults — an untagged export, a gamut clip, a wrong rendering intent, and a viewer applying its own transform — and each has a different fix.

Core Diagnosis: Symptom to Root Cause to Fix

The four causes are distinguishable by which colours moved.

Everything moved, including neutrals. The file is untagged, or is tagged with a profile that does not describe how it was rendered. Every consumer then applies its own assumption, which is why the same file looks different in two applications. Fix by embedding the correct profile at export.

Only saturated colours moved; neutrals are fine. A gamut clip. The destination space cannot represent those colours and the conversion mapped them to its boundary. Fix in the palette, not the conversion — the discussion under Color Profile and CMYK Conversion covers which map colours typically fall outside a coated CMYK gamut.

Everything moved slightly, including colours that were well inside the destination gamut. A perceptual rendering intent, which compresses the whole gamut to make room for the out-of-range colours. Relative colorimetric would have reproduced the in-gamut colours exactly.

Nothing moved in the file; only the display differs. A viewer that is not colour-managed. The file is fine.

Which colours moved tells you which stage caused it A four-row table. If neutrals moved as well, the file is untagged or mis-tagged. If only saturated colours moved, it is a gamut clip. If in-gamut colours moved slightly along with everything else, it is a perceptual rendering intent. If the file's numbers are unchanged and only the display differs, the viewer is not colour-managed. what moved cause and fix neutrals too untagged export — embed the profile saturated only gamut clip — change the palette in-gamut colours too perceptual intent — use relative colorimetric nothing in the file viewer not colour-managed — no fix needed Sampling one grey and one saturated fill from the same export separates the first two rows, which are the two most often confused.
The diagnosis needs two samples, not an opinion. A grey and a saturated fill from the same file distinguish the two most commonly conflated causes.

Production-Ready Python Implementation

from PIL import Image, ImageCms
import io
import numpy as np


def describe_export(path: str) -> dict:
    """Report the colour contract of an exported raster."""
    img = Image.open(path)
    raw = img.info.get("icc_profile")
    if raw is None:
        return {"tagged": False,
                "note": "no embedded profile — every consumer will guess"}
    profile = ImageCms.ImageCmsProfile(io.BytesIO(raw))
    return {"tagged": True,
            "profile": ImageCms.getProfileDescription(profile).strip(),
            "space": ImageCms.getProfileInfo(profile)[:40].strip(),
            "mode": img.mode}


def compare_pixels(a_path: str, b_path: str, samples: list) -> list:
    """Perceptual distance between the same sample points in two exports."""
    a, b = Image.open(a_path).convert("LAB"), Image.open(b_path).convert("LAB")
    if a.size != b.size:
        raise ValueError("exports differ in size; compare like with like")

    out = []
    for x, y in samples:
        pa = np.array(a.getpixel((x, y)), dtype="float32")
        pb = np.array(b.getpixel((x, y)), dtype="float32")
        # Lab is roughly perceptually uniform: Euclidean distance is a usable ΔE.
        out.append({"xy": (x, y), "delta_e": round(float(np.linalg.norm(pa - pb)), 2),
                    "neutral": bool(abs(pa[1] - 128) < 4 and abs(pa[2] - 128) < 4)})
    return out

compare_pixels reports whether each sample was neutral, which is what turns a list of distances into a diagnosis: neutrals moving alongside saturated colours points at the profile, neutrals holding still points at the gamut.

Performance Tuning and Cartographic Best Practices

  • Tag every export, always. It costs one argument and removes an entire class of ambiguity. An untagged file is not “sRGB by convention” — it is undefined, and different consumers resolve it differently.
  • Soft-proof in the pipeline. Convert to the destination profile and back, then report the largest perceptual distance across the palette. Anything above a few units is a colour the press cannot reproduce, and the palette should change while it is still editable.
  • Check the gamut before conversion. Afterwards the original chroma is gone and two classes that were distinct may share a value.
  • Use relative colorimetric for maps. Perceptual is the common default in image software and the wrong choice for a map with a printed legend, because it moves colours that had exact equivalents.
  • Sample the same pixel, not the same feature. A feature can move by a pixel between exports for reasons unrelated to colour, and comparing across that offset measures the wrong thing.
Soft-proof round trip as an automated check Perceptual distance for seven palette classes after converting to a coated CMYK profile and back. Five classes move by less than two units. The hydrology cyan moves by 9.4 and the warning orange by 7.1, both above the tolerance line at 3, and both are marked as colours the press cannot reproduce. 0 6 12 ΔE tolerance 3.0 9.4 7.1 water warning The two failing classes are known before the file leaves the pipeline, while the palette can still be changed — rather than after a press proof arrives.
The round trip is two conversions and a subtraction. It converts a class of problem discovered on paper into one discovered in CI.

Integration and Next Steps

The soft-proof check belongs beside the contrast validation in the export pipeline, and its output feeds the palette work described in Color Theory for GIS — a class that fails the round trip should be adjusted in the palette generator rather than corrected per export. Where the destination is a specific press, obtain its profile rather than using a generic coated one; the difference between them is larger than most of the shifts being debugged.

Three checks, three stages Three checks placed along the export pipeline. Gamut checking runs against the palette while it is still editable. Soft-proof round trip runs at export against the destination profile. Profile presence verification runs on the finished file. Each is placed at the earliest stage where it can be performed. palette stage — gamut check the only stage where an out-of-gamut colour can still be changed export stage — soft-proof round trip measures what the destination profile will actually do to each class artefact stage — profile presence asserts the finished file states what its numbers mean Only the third check survives into the delivered file; the first two are why it passes.
Each check is at the earliest point it can run, which is also the latest point at which its findings are still cheap to act on.

Frequently Asked Questions

How do I tell a gamut clip from a profile problem?

By whether neutrals moved. A gamut clip affects only colours the destination cannot represent, so greys and pale fills reproduce correctly while the saturated hydrology cyan flattens. A profile problem reinterprets the whole encoding, so neutrals shift along with everything else. Sample one grey and one saturated fill from the same export and compare both — if only the saturated sample moved, the fix belongs in the palette rather than in the conversion settings.

Why does the same file look different in two applications?

Because one is colour-managed and the other is not. A managed application reads the embedded profile and converts to the display profile; a naive one sends the stored numbers straight to the screen. If the file is tagged, the managed view is the correct one and the naive view is showing raw numbers. If the file is untagged, neither view is authoritative — and that ambiguity is itself the bug, fixed by embedding a profile at export.

Is a visual comparison ever good enough?

For noticing that something changed, yes. For determining what changed, no. Surround colour, ambient lighting and display calibration each move a visual judgement by more than the shifts typically under investigation. Sample the same pixel from both files, convert to a perceptually uniform space, and compute a distance. A number distinguishes “the conversion moved this class by 6 units” from “this looks warmer to me in this room”.

Should I soft-proof before every print export?

For anything going to press, yes — and it should be automated rather than a manual step. Converting to the destination profile and back, then reporting the largest perceptual distance across the palette, costs two conversions and catches every colour the press cannot reproduce while the palette is still editable. Doing it manually means it happens on the first export of a project and not on the twentieth, which is where the palette will have quietly acquired a new class.


Back to Color Theory for GIS