For each test, compare with the reference image, which is in sRGB.
The test passes if you cannot see any circles
in the middle of each patch.
The test fails if the circles
(which are in a variety of colorspaces,
indicated by an embedded ICC profile
stored in the PNG iCCP
chunk)
are visibly different to the surrounding rectanguar patches (in sRGB).
This failure indicates that the PNG image data is not being interpreted
according to the embedded ICC profile.
Or, what to do if your browser is failing the tests so you are pretty sure the browser is right and the tests must be wrong.
The test images show a Macbeth color checker, which is an industry standard test chart of 24 colored patches in a 6x4 grid. The D50 CIE Lab values (calculated from averaged measured spectra of 20 test charts) of each patch are known.
dark skin lab(38.358 13.802 14.646) |
light skin lab(66.056 17.737 17.848) |
blue sky lab(50.090 -4.407 -22.51) |
foliage lab(43.204 -13.464 21.73) |
blue flower lab(55.356 8.891 -24.82) |
bluish green lab(70.700 -32.892 -0.24) |
orange lab(62.559 35.135 58.050) |
purplish blue lab(40.178 9.551 -44.289) |
moderate red lab(51.711 47.694 16.857) |
purple lab(30.375 21.131 -20.30) |
yellow green lab(72.492 -23.462 57.07) |
orange yellow lab(71.963 19.486 67.998) |
blue lab(28.653 15.600 -50.52) |
green lab(55.046 -38.088 31.61) |
red lab(42.182 54.893 28.785) |
yellow lab(82.230 4.048 79.844) |
magenta lab(51.820 49.787 -13.90) |
cyan lab(50.555 -27.973 -28.13) |
white 9.5 lab(96.387 -0.404 2.238) |
neutral 8 lab(81.014 -0.570 0.180) |
neutral 6.5 lab(66.297 -0.434 -0.079) |
neutral 5 lab(50.830 -0.687 -0.268) |
neutral 3.5 lab(35.724 -0.521 -0.468) |
black 2 lab(20.706 0.025 -0.447) |
As a starting point, the patches were converted to sRGB.
These tests are designed to be run on a normal screen; a wide gamut screen is not required.
Thus, because the cyan
patch is outside the sRGB gamut,
(lab(50.555 -27.973 -28.13) is color(srgb -0.2 0.528 0.657))
for these tests the CIE LCH chroma was reduced to lab(50.555 -23.759 -23.900)
so that it was within gamut.
The sRGB values below are rounded to 8-bit (0 to 255) precision and given as decimal values.
dark skin rgb(117 82 67) |
light skin rgb(199 148 129) |
blue sky rgb(91 123 157) |
foliage rgb(89 108 65) |
blue flower rgb(131 129 176) |
bluish green rgb(94 190 172) |
orange rgb(223 124 44) |
purplish blue rgb(69 92 168) |
moderate red rgb(200 84 97) |
purple rgb(92 59 104) |
yellow green rgb(159 189 63) |
orange yellow rgb(231 162 39) |
blue rgb(39 62 147) |
green rgb(67 148 73) |
red rgb(182 46 56) |
yellow rgb(240 200 22) |
magenta rgb(193 84 149) |
desat_cyan rgb(0 133 160) |
white 9.5 rgb(245 245 240) |
neutral 8 rgb(200 201 201) |
neutral 6.5 rgb(160 161 161) |
neutral 5 rgb(120 121 121) |
neutral 3.5 rgb(83 84 85) |
black 2 rgb(50 50 50) |
To make each test, the sRGB colors
were converted to a different RGB colorspace
by applying an ICC profile.
The profile was also embedded in the PNG image,
in an iCCP
chunk.
See 12.3.2.3 iCCP Embedded ICC profile.
The test consists of the test image, overlaid with an RGBA sRGB image which produces a patch of the correct color with a circular hole in the middle of each patch to allow the test image to show. If the ICC profile is being applied, the patch and the circle (the test image, showing through) will be the same color so the circles will not be visible.
The test reference is just sRGB patches, with no circles. This will be a visual match (color difference will be significantly below 1 deltaE2000) and should be an image match (color difference less than 1 bit at 8 bits/component).
- Firefox Nightly 100.0a1 (2022-03-29) (64-bit) on Windows 10
gfx.color_management.mode
was set to 1 (full color management) - Chrome Canary Version 102.0.4999.0 (Official Build) canary (64-bit) on Windows 10
- Microsoft Edge Version 100.0.1185.39 (Official build) (64-bit) on Windows 10
- Safari Technology Preview release 143 (Safari 15.4, WebKit 17614.1.7.7) on OS X Monterrey 12.2.1
On Windows, test system was a Dell XPS 9560 with the 4k WCG display. Display was profiled with an X-Rite i1Pro2 spectrophotometer and DisplayCAL software.
On Mac, test system was a MacBook Pro 16-inch 2019, using the factory Display P3 profile.
These are in the profiles directory.
- sRGB v2, from ICC. Filename:
sRGB-v2-2014.icc
- sRGB with red and green colorants swapped. Filename:
swapped-v2.icc
- ProPhoto RGB from Elle Stone, gamma 1.8. Filename:
LargeRGB-elle-V2-g18.icc
- CIE RGB from Elle Stone, CIE L* TRC. Filename:
CIERGB-elle-V2-labl.icc
- sRGB v4 Preference, from ICC. Filename:
sRGB-v4-ICC_preference.icc
- ProPhoto RGB from Elle Stone,, gamma 1.8. Filename:
LargeRGB-elle-V4-g18.icc
- CIE RGB from Elle Stone, CIE L* TRC. Filename:
CIERGB-elle-V4-labl.icc
- Display P3 from ICC and Apple. Filename:
Display P3.icc
In progress. Considering using the sRGB iccMAX profiles, but edited to swap the red and green channels so that it is obvious that they have been applied.
Documenting the process, for verification and as a reminder should tests need to be added or modified.
The sRGB test image was created in SVG and then a color-accurate conversion of SVG to PNG was created with SVGtoPNG. The RGB values were verified by dumping the palette entries:
pngcheck -p foo.png
and then checking them off one by one against the expected RGB values.
For the reference, the mask image was created in SVG then rasterised with SVGtoPNG. The PNG raster mask was converted to PGM with netpbm tools then merged in as an alpha channel using pnmtopng, again from NetPBM tools.
- Convert image data from sRGB to destination colorspace (I used Little CMS Color Translator, with relative colorimetric rendering intent, sRGB as source colorspace, and desired new colorspace as the destination colorspace.)
- Run PNGcheck to be sure result is valid
- If the conversion tool does not embed the profile (or embeds an incorrect or modified one) add in the profile. I used pngcrush.
- If the conversion tool embedded the profile, extract it and check the ICC version and that it seems okay. (I used exiftool (actually exiftool for Windows) and then ICC profile inspector)
- View the PNG image, in a browser
- Make an HTML test file that overlays the result PNG with an sRGB reference.
For ICC v2 or v4 profiles, LittleCMS is a good implementation. It does not support ICC2 (iccMAX). LittleCMS itself is a library, but there is an application, Little CMS Color Translator which provides a drag and drop image colorspace transformation. The source and destination ICC profiles and the rendering intent are chosen in the options dialog, then all dropped images are converted from source to destination.
For ICC2 (iccMax), IccApplyProfiles from DemoIccMAX is a command line utility to apply iccMAX frofiles. Input and output images must be in TIFF format, only. The NetPBM utilities can be used to convert between PNG and TIFF (image data and alpha only).
The command to extract an existing ICC profile from an image is:
exiftool -icc_profile -b -w icc somefile.png
To remove an embedded profile (for example to create a rendering of a test failure):
pngcrush -rem iCCP foo.png result.png
This just adds an ICC profile into an iCCP
chunk;
it does not apply the profile to the image data,
which is assumed to already be in the colorspace
indicated by the profile.
To embed an ICC profile with exiftool, do this:
exiftool "-icc_profile<=somefile.icc" a.png
To embed an ICC profile using pngcrush:
pngcrush -iccp 536 "Profile name" foo.icc foo.png result.png
To list profile names, or profile descriptions:
exiftool -profiledescription *.png exiftool -profilename *.png
As a round-trip test, I used squoosh whose source is on GitHub. For PNG, Sqoosh uses OxiPNG which is a multithreaded lossless PNG optimizer written in Rust.
As a filesize optimization, Squoosh removes embedded ICC profiles by applying the profile to the image data and outputting sRGB. (Clearly, this is not lossless for colors outside the sRGB gamut).
https://squoosh.app/ (select OxiPNG as an encoder, leave resize and reduce palette alone). Applies the embedded ICC to pixel data during processing, outputting sRGB values. The palette indexes (there are only 25 unique colors in the image) can then be listed:
pngcheck -vv -p foo.png
and the rgb values compared with the sRGB values given above.
Here is the output of a png checker on a round-tripped sRGB image. The palette contains 25 colors:
$ pngcheck -vv -p roundtrip.png
File: roundtrip.png (1699 bytes)
chunk IHDR at offset 0x0000c, length 13
670 x 450 image, 8-bit palette, non-interlaced
chunk PLTE at offset 0x00025, length 75: 25 palette entries
0: (200, 84, 97) = (0xc8,0x54,0x61)
1: ( 89,108, 65) = (0x59,0x6c,0x41)
2: (199,148,129) = (0xc7,0x94,0x81)
3: ( 92, 59,104) = (0x5c,0x3b,0x68)
4: ( 91,123,157) = (0x5b,0x7b,0x9d)
5: (200,201,201) = (0xc8,0xc9,0xc9)
6: ( 0,133,160) = (0x00,0x85,0xa0)
7: ( 39, 62,147) = (0x27,0x3e,0x93)
8: (223,124, 44) = (0xdf,0x7c,0x2c)
9: (131,129,176) = (0x83,0x81,0xb0)
10: (240,200, 22) = (0xf0,0xc8,0x16)
11: ( 94,190,172) = (0x5e,0xbe,0xac)
12: (117, 82, 67) = (0x75,0x52,0x43)
13: (193, 84,149) = (0xc1,0x54,0x95)
14: ( 83, 84, 85) = (0x53,0x54,0x55)
15: (120,121,121) = (0x78,0x79,0x79)
16: (245,245,240) = (0xf5,0xf5,0xf0)
17: ( 50, 50, 50) = (0x32,0x32,0x32)
18: (231,162, 39) = (0xe7,0xa2,0x27)
19: ( 67,148, 73) = (0x43,0x94,0x49)
20: ( 34, 34, 34) = (0x22,0x22,0x22)
21: ( 69, 92,168) = (0x45,0x5c,0xa8)
22: (159,189, 63) = (0x9f,0xbd,0x3f)
23: (160,161,161) = (0xa0,0xa1,0xa1)
24: (182, 46, 56) = (0xb6,0x2e,0x38)
chunk IDAT at offset 0x0007c, length 1555
zlib: deflated, 32K window, default compression
row filters (0 none, 1 sub, 2 up, 3 avg, 4 paeth):
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
(450 out of 450)
chunk IEND at offset 0x0069b, length 0
No errors detected in indexed.png (4 chunks, 99.4% compression).
pngcrush -srgb 1 foo.png bar.png is supposed to add an sRGB chunk but sadly it adds a gAMA instead (!!) at least, if foo.png is indexed.