Color management is the system of tagging pixel values with a color space and converting them between devices so a color looks the same on a screen, a printer and a file. The common mechanism is the ICC profile, defined by the International Color Consortium. In web work you meet it as the color profile embedded in a JPEG, PNG or TIFF, and as the color spaces CSS can name.
Raw numbers such as rgb(255 0 0) mean nothing until they are tied to a space that says what red is. A profile supplies that link. ICC uses a reference colour space, the profile connection space, based on CIE XYZ or CIELAB. A source profile converts the file's values to that reference, and a destination profile converts from it to the output device.
A conversion also needs a rendering intent, which decides what happens to colors the destination cannot reproduce. The ICC specification defines four:
The CSS Color 4 spec says an image is tied to a space by the format, usually through an embedded ICC profile. PNG also has a small sRGB chunk that tags an image as sRGB without a full profile, with a one-byte rendering intent. CSS itself converts colors you write in different spaces, for example color(display-p3 ...), to the display's space.
import struct, zlib
def chunk(t, d): return struct.pack('>I', len(d)) + t + d + struct.pack('>I', zlib.crc32(t + d))
ihdr = struct.pack('>IIBBBBB', 1, 1, 8, 2, 0, 0, 0)
idat = zlib.compress(b'\x00\xff\x00\x00')
def build(extra): return b'\x89PNG\r\n\x1a\n' + chunk(b'IHDR', ihdr) + extra + chunk(b'IDAT', idat) + chunk(b'IEND', b'')
names = {0: 'perceptual', 1: 'relative colorimetric', 2: 'saturation', 3: 'absolute colorimetric'}
files = {'untagged.png': build(b''),
'srgb.png': build(chunk(b'sRGB', bytes([0]))),
'relative.png': build(chunk(b'sRGB', bytes([1])))}
for fn, data in files.items():
pos, out = 8, []
while pos < len(data):
n = struct.unpack('>I', data[pos:pos+4])[0]; t = data[pos+4:pos+8].decode(); body = data[pos+8:pos+8+n]
out.append('sRGB intent=%d (%s)' % (body[0], names[body[0]]) if t == 'sRGB' else t)
pos += 12 + n
print(fn.ljust(14), ', '.join(out))
untagged.png IHDR, IDAT, IEND
srgb.png IHDR, sRGB intent=0 (perceptual), IDAT, IEND
relative.png IHDR, sRGB intent=1 (relative colorimetric), IDAT, IEND
The two apps are usually interpreting the same numbers in different spaces. A design tool working in Display P3 and a screenshot treated as sRGB will disagree on saturated colors. Fix it by exporting with a profile or converting to sRGB before export, and by checking that both apps use a profile.