Skip to content

Commit 9789a94

Browse files
Glowieslgritz
authored andcommitted
feat(openexr): ACES Container hint for exr outputs (#4907)
Closes #4791 This PR introduces the `oiio:ACESContainer` hint for OpenEXR outputs. The hint can take one of the following values: `none` (default), `strict`, or `relaxed`. If not `none`, the spec will be checked to see if it is compliant with the ACES Container format defined in [ST 2065-4](https://pub.smpte.org/pub/st2065-4/st2065-4-2023.pdf). If it is, chromaticities will be set to the ACES AP0 ones, and the acesImageContainerFlag attribute will be set to 1. In `strict` mode, if the spec is non-compliant, the output will throw an error and avoid writing the image. While in `relaxed` mode, if the spec in non-compliant, only a warning will be printed and the attributes mentioned above will not be written to the spec. I've added several tests under `openexr-suite`. These use oiiotool to attempt writing several EXRs with the `oiio:ACESContainer` hint. --------- Signed-off-by: glowies <[email protected]> Signed-off-by: Oktay Comu <[email protected]>
1 parent 75903de commit 9789a94

File tree

4 files changed

+287
-0
lines changed

4 files changed

+287
-0
lines changed

src/doc/builtinplugins.rst

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1656,6 +1656,19 @@ control aspects of the writing itself:
16561656
* - Output Configuration Attribute
16571657
- Type
16581658
- Meaning
1659+
* - ``openexr:ACESContainerPolicy``
1660+
- string
1661+
- One of `none` (default), `strict`, or `relaxed`.
1662+
If not `none`, the spec will be checked to see if it is compliant
1663+
with the ACES Container format defined in `ST 2065-4`_. If it is,
1664+
`chromaticities` will be set to the ACES AP0 ones, `colorInteropId`
1665+
will be set to 'lin_ap0_scene' and the `acesImageContainerFlag`
1666+
attribute will be set to 1.
1667+
In `strict` mode, if the spec is non-compliant, the output will
1668+
throw an error and avoid writing the image.
1669+
While in `relaxed` mode, if the spec is non-compliant, `chromaticities`
1670+
and `colorInteropId` will be set, but `acesImageContainerFlag`
1671+
will NOT.
16591672
* - ``oiio:RawColor``
16601673
- int
16611674
- If nonzero, writing images with non-RGB color models (such as YCbCr)
@@ -1667,6 +1680,7 @@ control aspects of the writing itself:
16671680
- Pointer to a ``Filesystem::IOProxy`` that will handle the I/O, for
16681681
example by writing to a memory buffer.
16691682

1683+
.. _ST 2065-4: https://pub.smpte.org/pub/st2065-4/
16701684

16711685
**Custom I/O Overrides**
16721686

src/openexr.imageio/exroutput.cpp

Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -292,6 +292,198 @@ set_exr_threads()
292292

293293

294294

295+
static constexpr float ACES_AP0_chromaticities[8] = {
296+
0.7347f, 0.2653f, // red
297+
0.0f, 1.0f, // green
298+
0.0001f, -0.077f, // blue
299+
0.32168f, 0.33767f // white
300+
};
301+
302+
static const std::string ACES_AP0_colorInteropId = "lin_ap0_scene";
303+
304+
305+
bool
306+
is_spec_aces_container_channels_only(const OIIO::ImageSpec& spec)
307+
{
308+
// Note: this is constructing and comparing sets, so that channel order
309+
// doesn't matter.
310+
311+
// Allowed channel sets
312+
static const std::vector<std::set<std::string>> allowed_sets
313+
= { { "B", "G", "R" },
314+
{ "A", "B", "G", "R" },
315+
{ "B", "G", "R", "left.B", "left.G", "left.R" },
316+
{ "A", "B", "G", "R", "left.A", "left.B", "left.G", "left.R" } };
317+
318+
// Gather channel set from spec
319+
std::set<std::string> channels(spec.channelnames.begin(),
320+
spec.channelnames.end());
321+
322+
// Compare to allowed sets (unordered)
323+
for (const auto& allowed : allowed_sets) {
324+
if (channels == allowed) {
325+
return true;
326+
}
327+
}
328+
329+
return false;
330+
}
331+
332+
333+
334+
bool
335+
is_aces_container_attributes_non_empty(const OIIO::ImageSpec& spec,
336+
std::string& non_compliant_attr)
337+
{
338+
// attributes in this list should NOT be empty if they exist
339+
static const std::string nonEmptyAttribs[] = {
340+
"cameraFirmwareVersion",
341+
"cameraIdentifier",
342+
"cameraLabel",
343+
"cameraMake",
344+
"cameraModel",
345+
"cameraSerialNumber",
346+
"comments",
347+
"creator",
348+
"lensAttributes",
349+
"lensFirmwareVersion",
350+
"lensMake",
351+
"lensModel",
352+
"lensSerialNumber",
353+
"owner",
354+
"recorderFirmwareVersion",
355+
"recorderMake",
356+
"recorderModel",
357+
"recorderSerialNumber",
358+
"reelName",
359+
"storageMediaSerialNumber",
360+
};
361+
362+
for (const auto& label : nonEmptyAttribs) {
363+
const ParamValue* found = spec.find_attribute(label,
364+
OIIO::TypeDesc::STRING);
365+
if (found
366+
&& (found->type() != TypeString || found->get_string(1).empty())) {
367+
non_compliant_attr = label;
368+
return false;
369+
}
370+
}
371+
372+
return true;
373+
}
374+
375+
376+
377+
bool
378+
is_aces_container_compliant(const OIIO::ImageSpec& spec, std::string& reason)
379+
{
380+
if (!is_spec_aces_container_channels_only(spec)) {
381+
reason
382+
= "Spec channel names do not match those required for an ACES Container.";
383+
return false;
384+
}
385+
386+
// Check data type
387+
if (spec.format != OIIO::TypeDesc::HALF) {
388+
reason
389+
= "EXR data type is not 'HALF' as required for an ACES Container.";
390+
return false;
391+
}
392+
393+
// Check compression
394+
std::string compression = spec.get_string_attribute("compression", "zip");
395+
if (compression != "none") {
396+
reason = "Compression is not 'none' as required for an ACES Container.";
397+
return false;
398+
}
399+
400+
// Check non-empty attributes
401+
std::string non_compliant_attr = "";
402+
if (!is_aces_container_attributes_non_empty(spec, non_compliant_attr)) {
403+
reason = "Spec contains an empty string attribute (";
404+
reason += non_compliant_attr;
405+
reason += ") that is required to be non-empty in an ACES Container.";
406+
return false;
407+
}
408+
409+
// Check attributes with exact values if they exist
410+
if (spec.get_string_attribute("oiio:ColorSpace", ACES_AP0_colorInteropId)
411+
!= ACES_AP0_colorInteropId
412+
|| spec.get_string_attribute("colorInteropId", ACES_AP0_colorInteropId)
413+
!= ACES_AP0_colorInteropId) {
414+
reason
415+
= "Color space is not lin_ap0_scene as required for an ACES Container.";
416+
return false;
417+
}
418+
419+
if (spec.get_int_attribute("acesImageContainerFlag", 1) != 1) {
420+
reason
421+
= "acesImageContainerFlag is not set to '1' as required for an ACES Container.";
422+
return false;
423+
}
424+
425+
// Check chromaticities
426+
float chromaticities[8] = { 0., 0., 0., 0., 0., 0., 0., 0. };
427+
bool chroms_found
428+
= spec.getattribute("chromaticities",
429+
OIIO::TypeDesc(OIIO::TypeDesc::FLOAT, 8),
430+
chromaticities);
431+
bool chroms_equal = std::equal(std::begin(chromaticities),
432+
std::end(chromaticities),
433+
std::begin(ACES_AP0_chromaticities));
434+
435+
if (chroms_found && !chroms_equal) {
436+
reason
437+
= "Chromaticities are not set to AP0 chromaticities as required for an ACES Container.";
438+
return false;
439+
}
440+
441+
return true;
442+
}
443+
444+
445+
446+
void
447+
set_aces_container_attributes(OIIO::ImageSpec& spec)
448+
{
449+
spec.attribute("chromaticities", OIIO::TypeDesc(OIIO::TypeDesc::FLOAT, 8),
450+
ACES_AP0_chromaticities);
451+
spec.attribute("colorInteropId", ACES_AP0_colorInteropId);
452+
spec.attribute("acesImageContainerFlag", 1);
453+
}
454+
455+
456+
457+
bool
458+
process_aces_container(OIIO::ImageSpec& spec, std::string policy,
459+
int acesImageContainerFlag,
460+
std::string& non_compliance_reason)
461+
{
462+
bool treat_as_aces_container = policy == "strict"
463+
|| acesImageContainerFlag == 1;
464+
bool is_compliant = is_aces_container_compliant(spec,
465+
non_compliance_reason);
466+
467+
if (treat_as_aces_container && !is_compliant) {
468+
return false;
469+
}
470+
471+
set_aces_container_attributes(spec);
472+
473+
if (policy == "relaxed" && !is_compliant) {
474+
// When image is not compliant in relaxed mode, we should avoid
475+
// setting the flag, and we should print a warning
476+
477+
// TODO: When we have a way to report warnings, report one here
478+
// to indicate that the given image spec is not compliant
479+
spec.erase_attribute("acesImageContainerFlag");
480+
}
481+
482+
return true;
483+
}
484+
485+
486+
295487
OpenEXROutput::OpenEXROutput()
296488
{
297489
pvt::set_exr_threads();
@@ -814,6 +1006,26 @@ OpenEXROutput::spec_to_header(ImageSpec& spec, int subimage,
8141006
Imf::LevelMode(m_levelmode),
8151007
Imf::LevelRoundingMode(m_roundingmode)));
8161008

1009+
// Check ACES Container hint
1010+
int aces_container_flag = spec.get_int_attribute("acesImageContainerFlag",
1011+
0);
1012+
std::string aces_container_policy
1013+
= spec.get_string_attribute("openexr:ACESContainerPolicy", "none");
1014+
1015+
if (aces_container_policy != "none" || aces_container_flag == 1) {
1016+
std::string non_compliance_reason = "";
1017+
bool should_panic = !process_aces_container(spec, aces_container_policy,
1018+
aces_container_flag,
1019+
non_compliance_reason);
1020+
1021+
if (should_panic) {
1022+
errorfmt(
1023+
"Cannot output non-compliant ACES Container in 'strict' mode. REASON: {}",
1024+
non_compliance_reason);
1025+
return false;
1026+
}
1027+
}
1028+
8171029
// Deal with all other params
8181030
for (const auto& p : spec.extra_attribs)
8191031
put_parameter(p.name().string(), p.type(), p.data(), header);

testsuite/openexr-suite/ref/out.txt

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -341,3 +341,31 @@ negoverscan.exr : 64 x 64, 3 channel, half openexr
341341
screenWindowWidth: 1
342342
oiio:subimages: 1
343343
openexr:lineOrder: "increasingY"
344+
acesImageContainerFlag for relaxed-out.exr is (1)
345+
acesImageContainerFlag for fail.exr is ()
346+
acesImageContainerFlag for fail.exr is ()
347+
acesImageContainerFlag for fail.exr is ()
348+
Reading strict-out.exr
349+
strict-out.exr : 4 x 4, 3 channel, half openexr
350+
SHA-1: C49A9785B2243F2F080DAAD1747F119ACCECCFA5
351+
channel list: R, G, B
352+
acesImageContainerFlag: 1
353+
chromaticities: 0.7347, 0.2653, 0, 1, 0.0001, -0.077, 0.32168, 0.33767
354+
colorInteropId: "lin_ap0_scene"
355+
compression: "none"
356+
PixelAspectRatio: 1
357+
screenWindowCenter: 0, 0
358+
screenWindowWidth: 1
359+
oiio:ColorSpace: "lin_ap0_scene"
360+
oiio:subimages: 1
361+
openexr:ACESContainerPolicy: "strict"
362+
openexr:lineOrder: "increasingY"
363+
oiiotool ERROR: -o : Cannot output non-compliant ACES Container in 'strict' mode. REASON: Spec channel names do not match those required for an ACES Container.
364+
Full command line was:
365+
> oiiotool --create 4x4 3 -d half --compression none --ch left.R=R,G,B -sattrib openexr:ACESContainerPolicy strict -o strict-fail.exr
366+
oiiotool ERROR: -o : Cannot output non-compliant ACES Container in 'strict' mode. REASON: Compression is not 'none' as required for an ACES Container.
367+
Full command line was:
368+
> oiiotool --create 4x4 3 -d half --compression zip -sattrib openexr:ACESContainerPolicy strict -o strict-fail.exr
369+
oiiotool ERROR: -o : Cannot output non-compliant ACES Container in 'strict' mode. REASON: EXR data type is not 'HALF' as required for an ACES Container.
370+
Full command line was:
371+
> oiiotool --create 4x4 3 -d float --compression none -sattrib openexr:ACESContainerPolicy strict -o strict-fail.exr

testsuite/openexr-suite/run.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,3 +50,36 @@
5050
# Check writing overscan and negative range
5151
command += oiiotool("--create 64x64-16-16 3 -d half -o negoverscan.exr")
5252
command += info_command("negoverscan.exr", safematch=True)
53+
54+
# Check ACES Container output for relaxed mode
55+
#
56+
# Valid ACES Container
57+
command += oiiotool("--create 4x4 3 -d half --compression none -sattrib openexr:ACESContainerPolicy relaxed -o relaxed-out.exr")
58+
command += oiiotool("relaxed-out.exr --echo \"acesImageContainerFlag for {TOP.filename} is ({TOP[acesImageContainerFlag]})\"", failureok=True) # should give 1
59+
60+
# Invalid channel name set
61+
command += oiiotool("--create 4x4 3 -d half --compression none --ch left.R=R,G,B -sattrib openexr:ACESContainerPolicy relaxed -o fail.exr")
62+
command += oiiotool("fail.exr --echo \"acesImageContainerFlag for {TOP.filename} is ({TOP[acesImageContainerFlag]})\"", failureok=True) # should be empty
63+
64+
# Invalid compression
65+
command += oiiotool("--create 4x4 3 -d half --compression zip -sattrib openexr:ACESContainerPolicy relaxed -o fail.exr")
66+
command += oiiotool("fail.exr --echo \"acesImageContainerFlag for {TOP.filename} is ({TOP[acesImageContainerFlag]})\"", failureok=True) # should be empty
67+
68+
# Invalid data type
69+
command += oiiotool("--create 4x4 3 -d float --compression none -sattrib openexr:ACESContainerPolicy relaxed -o fail.exr")
70+
command += oiiotool("fail.exr --echo \"acesImageContainerFlag for {TOP.filename} is ({TOP[acesImageContainerFlag]})\"", failureok=True) # should be empty
71+
72+
# Check ACES Container output for strict mode
73+
#
74+
# Valid ACES Container
75+
command += oiiotool("--create 4x4 3 -d half --compression none -sattrib openexr:ACESContainerPolicy strict -o strict-out.exr")
76+
command += info_command("strict-out.exr", safematch=True)
77+
78+
# Invalid channel name set
79+
command += oiiotool("--create 4x4 3 -d half --compression none --ch left.R=R,G,B -sattrib openexr:ACESContainerPolicy strict -o strict-fail.exr", failureok=True)
80+
81+
# Invalid compression
82+
command += oiiotool("--create 4x4 3 -d half --compression zip -sattrib openexr:ACESContainerPolicy strict -o strict-fail.exr", failureok=True)
83+
84+
# Invalid data type
85+
command += oiiotool("--create 4x4 3 -d float --compression none -sattrib openexr:ACESContainerPolicy strict -o strict-fail.exr", failureok=True)

0 commit comments

Comments
 (0)