Skip to content

Commit 4d1b420

Browse files
authored
feat: CICP metadata support for PNG (#4746)
This PR addresses the primary concern of #4678 -- implement support for reading and writing the `cICP` chunk for PNGs. CICP (Coding Independent Code Points) is a means for using a tuple of integers to communicate characteristics of a variety of, traditionally, video encodings. The tuple is a series of four values that map to integer enumerations (codified in [ITU-T H.273](https://www.itu.int/rec/T-REC-H.273-202407-I/en)) representing a certain ColourPrimaries, TransferCharacteristics, and MatrixCoefficients, as well as a VideoFullRangeFlag. In particular, CICP is used for describing HDR encodings to browsers and image viewers capable of displaying the image as intended. For example, if one were to write P3-D65 PQ-encoded RGB values to a PNG, it will only look "correct" if there's an appropriate `cICP` chunk describing the primaries (14) and transfer function (16); without such metadata, PQ-encoded images will appear significantly darker and lower in contrast. Internally, CICP metadata is stored in a `int[4]` type `CICP` ImageSpec attribute. This PR adds the following: - An oiiotool `--cicp` flag for setting, modifying, or removing CICP metadata for the top image - Methods for reading and writing PNG `cICP` chunk metadata <--> ImageSpec `CICP` - Tests I've included tests. I've also embedded CICP metadata in the existing 16-bit test png in the testsuite. But to see what this is all about with your own eyes, you can quickly convert a scene-linear AP0-encoded EXR to a PQ-encoded P3D65 HDR PNG with the following command: `$ oiiotool -i input_linap0.exr --ociodisplay:from=ACES2065-1 "ST2084-P3-D65 - Display" "ACES 1.1 - HDR Video (1000 nits & P3 lim)" --cicp "12,16,0,1" -o output_p3d65pq.png` If you compare in a capable image viewer on a capable display the image produced with the above command compared to another one that lacks CICP metadata, you should see a pretty significant difference: `$ oiiotool -i output_p3d65pq.png --cicp "" -o output_no_cicp.png` ("Preview" on a ~5 year old Macbook should suffice). --------- Signed-off-by: Zach Lewis <[email protected]>
1 parent d6f5baf commit 4d1b420

File tree

13 files changed

+171
-11
lines changed

13 files changed

+171
-11
lines changed

src/doc/oiiotool.rst

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4690,6 +4690,15 @@ will be printed with the command `oiiotool --colorconfiginfo`.
46904690

46914691
This was added to OpenImageIO 2.5.
46924692

4693+
.. option:: --cicp <pri>,<trc>,<mtx>,<vfr>
4694+
4695+
The `--cicp` command adds, modifies, or removes a `"CICP"` attribute
4696+
belonging to the top image, stored as an array of four integers.
4697+
The integers represent, in order, the color primaries, transfer
4698+
function, color matrix (for YUV colorspaces), and
4699+
video-full-range-flag.
4700+
4701+
This was added to OpenImageIO 3.1.
46934702

46944703
|
46954704

src/doc/stdmetadata.rst

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,17 @@ Color information
164164
window that are not overlapping the pixel data window. If not supplied,
165165
the default is black (0 in all channels).
166166

167+
.. option:: "CICP" : int[4]
168+
169+
The CICP color space information, as defined by
170+
`ITU-T H.273 <https://www.itu.int/rec/T-REC-H.273>`_. This is an array
171+
of four integers, with the following meanings:
172+
173+
- `[0]` : color primaries
174+
- `[1]` : transfer characteristics
175+
- `[2]` : matrix coefficients
176+
- `[3]` : full range flag
177+
167178
.. option:: "ICCProfile" : uint8[]
168179
"ICCProfile:...various..." : ...various types...
169180

src/include/OpenImageIO/imageio.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1087,6 +1087,9 @@ class OIIO_API ImageInput {
10871087
/// - `"exif"` :
10881088
/// Can this format store Exif camera data?
10891089
///
1090+
/// - `"cicp"` :
1091+
/// Does this format support embedding CICP metadata?
1092+
///
10901093
/// - `"ioproxy"` :
10911094
/// Does this format reader support reading from an `IOProxy`?
10921095
///
@@ -2527,6 +2530,9 @@ class OIIO_API ImageOutput {
25272530
/// Does this format allow 0x0 sized images, i.e. an image file
25282531
/// with metadata only and no pixels?
25292532
///
2533+
/// - `"cicp"` :
2534+
/// Does this format support embedding CICP metadata?
2535+
///
25302536
/// This list of queries may be extended in future releases. Since this
25312537
/// can be done simply by recognizing new query strings, and does not
25322538
/// require any new API entry points, addition of support for new

src/libOpenImageIO/formatspec.cpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1295,6 +1295,8 @@ void
12951295
ImageSpec::set_colorspace(string_view colorspace)
12961296
{
12971297
ColorConfig::default_colorconfig().set_colorspace(*this, colorspace);
1298+
// Invalidate potentially contradictory metadata
1299+
erase_attribute("CICP");
12981300
}
12991301

13001302

src/oiiotool/oiiotool.cpp

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2248,6 +2248,60 @@ icc_read(Oiiotool& ot, cspan<const char*> argv)
22482248
}
22492249

22502250

2251+
// Set, modify, or remove the top image's CICP (ITU-T H.273) metadata.
2252+
class OpSetCICP final : public OiiotoolOp {
2253+
public:
2254+
OpSetCICP(Oiiotool& ot, string_view opname, cspan<const char*> argv)
2255+
: OiiotoolOp(ot, opname, argv, 1)
2256+
{
2257+
inplace(true); // This action operates in-place
2258+
cicp = args(1);
2259+
}
2260+
OpSetCICP(Oiiotool& ot, string_view opname, int argc, const char* argv[])
2261+
: OpSetCICP(ot, opname, { argv, span_size_t(argc) })
2262+
{
2263+
}
2264+
bool setup() override
2265+
{
2266+
ir(0)->metadata_modified(true);
2267+
return true;
2268+
}
2269+
bool impl(span<ImageBuf*> img) override
2270+
{
2271+
// Because this is an in-place operation, img[0] is the same as
2272+
// img[1].
2273+
if (cicp.empty()) {
2274+
img[0]->specmod().erase_attribute("CICP");
2275+
return true;
2276+
}
2277+
std::vector<int> vals { 0, 0, 0, 1 };
2278+
auto p = img[0]->spec().find_attribute("CICP",
2279+
TypeDesc(TypeDesc::INT, 4));
2280+
if (p) {
2281+
const int* existing = static_cast<const int*>(p->data());
2282+
for (int i = 0; i < 4; ++i)
2283+
vals[i] = existing[i];
2284+
}
2285+
Strutil::extract_from_list_string<int>(vals, cicp);
2286+
img[0]->specmod().attribute("CICP", TypeDesc(TypeDesc::INT, 4),
2287+
vals.data());
2288+
return true;
2289+
}
2290+
2291+
private:
2292+
string_view cicp;
2293+
};
2294+
2295+
2296+
// --cicp
2297+
static void
2298+
action_cicp(Oiiotool& ot, cspan<const char*> argv)
2299+
{
2300+
OpSetCICP op(ot, "cicp", argv);
2301+
op();
2302+
}
2303+
2304+
22512305

22522306
// --colorconfig
22532307
static void
@@ -7176,6 +7230,9 @@ Oiiotool::getargs(int argc, char* argv[])
71767230
ap.arg("--iccread %s:FILENAME")
71777231
.help("Add the contents of the file to the top image as its ICC profile")
71787232
.OTACTION(icc_read);
7233+
ap.arg("--cicp %s:CICP")
7234+
.help("Set or modifiy CICP metadata for supporting output formats (e.g., \"12,16,0,1\")") //; selectively persist existing values if not specified (e.g., \",,,0\")")
7235+
.OTACTION(action_cicp);
71797236
// clang-format on
71807237

71817238
if (ap.parse_args(argc, (const char**)argv) < 0) {

src/png.imageio/png_pvt.h

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ For further information see the following mailing list threads:
4040
OIIO_PLUGIN_NAMESPACE_BEGIN
4141

4242
#define ICC_PROFILE_ATTR "ICCProfile"
43-
43+
#define CICP_ATTR "CICP"
4444

4545
namespace PNG_pvt {
4646

@@ -224,7 +224,7 @@ read_info(png_structp& sp, png_infop& ip, int& bit_depth, int& color_type,
224224
int srgb_intent;
225225
double gamma = 0.0;
226226
if (png_get_sRGB(sp, ip, &srgb_intent)) {
227-
spec.set_colorspace("srgb_rec709_scene");
227+
spec.attribute("oiio:ColorSpace", "srgb_rec709_scene");
228228
} else if (png_get_gAMA(sp, ip, &gamma) && gamma > 0.0) {
229229
// Round gamma to the nearest hundredth to prevent stupid
230230
// precision choices and make it easier for apps to make
@@ -235,7 +235,7 @@ read_info(png_structp& sp, png_infop& ip, int& bit_depth, int& color_type,
235235
set_colorspace_rec709_gamma(spec, g);
236236
} else {
237237
// If there's no info at all, assume sRGB.
238-
set_colorspace(spec, "srgb_rec709_scene");
238+
spec.attribute("oiio:ColorSpace", "srgb_rec709_scene");
239239
}
240240

241241
if (png_get_valid(sp, ip, PNG_INFO_iCCP)) {
@@ -326,6 +326,16 @@ read_info(png_structp& sp, png_infop& ip, int& bit_depth, int& color_type,
326326

327327
interlace_type = png_get_interlace_type(sp, ip);
328328

329+
#ifdef PNG_cICP_SUPPORTED
330+
{
331+
png_byte pri = 0, trc = 0, mtx = 0, vfr = 0;
332+
if (png_get_cICP(sp, ip, &pri, &trc, &mtx, &vfr)) {
333+
int cicp[4] = { pri, trc, mtx, vfr };
334+
spec.attribute(CICP_ATTR, TypeDesc(TypeDesc::INT, 4), cicp);
335+
}
336+
}
337+
#endif
338+
329339
#ifdef PNG_eXIf_SUPPORTED
330340
// Recent version of PNG and libpng (>= 1.6.32, I think) have direct
331341
// support for Exif chunks. Older versions don't support it, and I'm not
@@ -713,6 +723,21 @@ write_info(png_structp& sp, png_infop& ip, int& color_type, ImageSpec& spec,
713723
(png_uint_32)(yres * scale), unittype);
714724
}
715725

726+
#ifdef PNG_cICP_SUPPORTED
727+
const ParamValue* p = spec.find_attribute(CICP_ATTR,
728+
TypeDesc(TypeDesc::INT, 4));
729+
if (p) {
730+
const int* int_vals = static_cast<const int*>(p->data());
731+
png_byte vals[4];
732+
for (int i = 0; i < 4; ++i)
733+
vals[i] = static_cast<png_byte>(int_vals[i]);
734+
if (setjmp(png_jmpbuf(sp))) // NOLINT(cert-err52-cpp)
735+
return "Could not set PNG cICP chunk";
736+
// libpng will only write the chunk if the third byte is 0
737+
png_set_cICP(sp, ip, vals[0], vals[1], (png_byte)0, vals[3]);
738+
}
739+
#endif
740+
716741
#ifdef PNG_eXIf_SUPPORTED
717742
std::vector<char> exifBlob;
718743
encode_exif(spec, exifBlob, endian::big);

src/png.imageio/pnginput.cpp

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,14 @@ class PNGInput final : public ImageInput {
1818
const char* format_name(void) const override { return "png"; }
1919
int supports(string_view feature) const override
2020
{
21-
return (feature == "ioproxy" || feature == "exif");
21+
return (feature == "ioproxy"
22+
#ifdef PNG_eXIf_SUPPORTED
23+
|| feature == "exif"
24+
#endif
25+
#ifdef PNG_cICP_SUPPORTED
26+
|| feature == "cicp"
27+
#endif
28+
);
2229
}
2330
bool valid_file(Filesystem::IOProxy* ioproxy) const override;
2431
bool open(const std::string& name, ImageSpec& newspec) override;

src/png.imageio/pngoutput.cpp

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ class PNGOutput final : public ImageOutput {
2424
return (feature == "alpha" || feature == "ioproxy"
2525
#ifdef PNG_eXIf_SUPPORTED
2626
|| feature == "exif"
27+
#endif
28+
#ifdef PNG_cICP_SUPPORTED
29+
|| feature == "cicp"
2730
#endif
2831
);
2932
}

testsuite/png/ref/out-libpng15.txt

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@ Reading exif.png
2626
exif.png : 64 x 64, 3 channel, uint8 png
2727
SHA-1: 7CB41FEA50720B48BE0C145E1473982B23E9AB77
2828
channel list: R, G, B
29+
Exif:ExifVersion: "0230"
30+
Exif:FlashPixVersion: "0100"
31+
Exif:FocalLength: 45.7 (45.7 mm)
32+
Exif:WhiteBalance: 0 (auto)
2933
oiio:ColorSpace: "srgb_rec709_scene"
3034
alphagamma:
3135
1 x 1, 4 channel, float png
@@ -86,5 +90,17 @@ gimp_gradient:
8690
Monochrome: No
8791
smallalpha.png : 1 x 1, 4 channel, uint8 png
8892
Pixel (0, 0): 240 108 119 1 (0.94117653 0.42352945 0.4666667 0.003921569)
93+
cicp:
94+
16 x 16, 4 channel, float png
95+
channel list: R, G, B, A
96+
oiio:ColorSpace: "srgb_rec709_scene"
97+
removed_cicp:
98+
16 x 16, 4 channel, float png
99+
channel list: R, G, B, A
100+
oiio:ColorSpace: "srgb_rec709_scene"
101+
remove_cicp_via_set_colorspace:
102+
16 x 16, 4 channel, float png
103+
channel list: R, G, B, A
104+
oiio:ColorSpace: "g22_rec709_display"
89105
Comparing "test16.png" and "ref/test16.png"
90106
PASS

testsuite/png/ref/out.txt

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,5 +90,18 @@ gimp_gradient:
9090
Monochrome: No
9191
smallalpha.png : 1 x 1, 4 channel, uint8 png
9292
Pixel (0, 0): 240 108 119 1 (0.94117653 0.42352945 0.4666667 0.003921569)
93+
cicp:
94+
16 x 16, 4 channel, float png
95+
channel list: R, G, B, A
96+
CICP: 1, 13, 0, 1
97+
oiio:ColorSpace: "srgb_rec709_scene"
98+
removed_cicp:
99+
16 x 16, 4 channel, float png
100+
channel list: R, G, B, A
101+
oiio:ColorSpace: "srgb_rec709_scene"
102+
remove_cicp_via_set_colorspace:
103+
16 x 16, 4 channel, float png
104+
channel list: R, G, B, A
105+
oiio:ColorSpace: "g22_rec709_display"
93106
Comparing "test16.png" and "ref/test16.png"
94107
PASS

0 commit comments

Comments
 (0)