Skip to content

Commit cb679f2

Browse files
brechtvllgritz
authored andcommitted
feat(heif): Read and write of CICP and bit depth 10 and 12 (#4880)
* Read and write 10 and 12 bit images * Read and write CICP This makes it possible to write HDR images that are much smaller than PNGs, in a file format that is supported in all modern web browsers. By setting the matrix coefficients, the library will perform RGB to YUV conversion on write. The YUV to RBG conversion was already happening on read automatically. A future improvement would be a better default choice of matrix coefficients for writing. When no CICP is specified or when CICP is read from PNG, then no conversion to YUV will be performed and compression will not work as well. This is the same behavior as before. When there is support for conversion between CICP and display interop ID, a good default choice for matrix coefficients could be made as part of that. Tests for CICP and 10 bit were added. --------- Signed-off-by: Brecht Van Lommel <[email protected]>
1 parent 2900f75 commit cb679f2

12 files changed

+330
-18
lines changed

src/doc/builtinplugins.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -688,6 +688,12 @@ preferred except when legacy file access is required.
688688
- string
689689
- Color space (see Section :ref:`sec-metadata-color`). We currently
690690
assume that any RGBE files encountered are linear with sRGB primaries.
691+
* - ``CICP``
692+
- int[4]
693+
- Coding-independent code points to describe the color profile.
694+
* - ``oiio:BitsPerSample``
695+
- int
696+
- Bits per sample in the file: 8, 10 or 12.
691697
* - ``heif:Orientation``
692698
- int
693699
- If the configuration option ``heif:reorient`` is nonzero and

src/heif.imageio/heifinput.cpp

Lines changed: 78 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@
33
// https://github.com/AcademySoftwareFoundation/OpenImageIO
44

55
#include <OpenImageIO/filesystem.h>
6+
#include <OpenImageIO/fmath.h>
67
#include <OpenImageIO/imageio.h>
8+
#include <OpenImageIO/platform.h>
79
#include <OpenImageIO/tiffutils.h>
810

911
#include <libheif/heif_cxx.h>
@@ -36,7 +38,11 @@ class HeifInput final : public ImageInput {
3638
const char* format_name(void) const override { return "heif"; }
3739
int supports(string_view feature) const override
3840
{
39-
return feature == "exif";
41+
return feature == "exif"
42+
#if LIBHEIF_HAVE_VERSION(1, 9, 0)
43+
|| feature == "cicp"
44+
#endif
45+
;
4046
}
4147
bool valid_file(const std::string& filename) const override;
4248
bool open(const std::string& name, ImageSpec& newspec) override;
@@ -53,6 +59,7 @@ class HeifInput final : public ImageInput {
5359
std::string m_filename;
5460
int m_subimage = -1;
5561
int m_num_subimages = 0;
62+
int m_bitdepth = 0;
5663
int m_has_alpha = false;
5764
bool m_associated_alpha = true;
5865
bool m_keep_unassociated_alpha = false;
@@ -203,11 +210,30 @@ HeifInput::seek_subimage(int subimage, int miplevel)
203210
return false;
204211
}
205212

206-
auto id = (subimage == 0) ? m_primary_id : m_item_ids[subimage - 1];
207-
m_ihandle = m_ctx->get_image_handle(id);
213+
auto id = (subimage == 0) ? m_primary_id : m_item_ids[subimage - 1];
214+
m_ihandle = m_ctx->get_image_handle(id);
215+
216+
m_bitdepth = m_ihandle.get_luma_bits_per_pixel();
217+
if (m_bitdepth < 0) {
218+
errorfmt("Image has undefined bit depth");
219+
m_ctx.reset();
220+
return false;
221+
} else if (!(m_bitdepth == 8 || m_bitdepth == 10 || m_bitdepth == 12)) {
222+
errorfmt("Image has unsupported bit depth {}", m_bitdepth);
223+
m_ctx.reset();
224+
return false;
225+
}
226+
208227
m_has_alpha = m_ihandle.has_alpha_channel();
209-
auto chroma = m_has_alpha ? heif_chroma_interleaved_RGBA
210-
: heif_chroma_interleaved_RGB;
228+
auto chroma = m_has_alpha ? (m_bitdepth > 8)
229+
? littleendian()
230+
? heif_chroma_interleaved_RRGGBBAA_LE
231+
: heif_chroma_interleaved_RRGGBBAA_BE
232+
: heif_chroma_interleaved_RGBA
233+
: (m_bitdepth > 8) ? littleendian()
234+
? heif_chroma_interleaved_RRGGBB_LE
235+
: heif_chroma_interleaved_RRGGBB_BE
236+
: heif_chroma_interleaved_RGB;
211237
#if 0
212238
try {
213239
m_himage = m_ihandle.decode_image(heif_colorspace_RGB, chroma);
@@ -238,13 +264,40 @@ HeifInput::seek_subimage(int subimage, int miplevel)
238264
}
239265
#endif
240266

241-
int bits = m_himage.get_bits_per_pixel(heif_channel_interleaved);
242-
m_spec = ImageSpec(m_himage.get_width(heif_channel_interleaved),
243-
m_himage.get_height(heif_channel_interleaved), bits / 8,
244-
TypeUInt8);
267+
m_spec = ImageSpec(m_himage.get_width(heif_channel_interleaved),
268+
m_himage.get_height(heif_channel_interleaved),
269+
m_has_alpha ? 4 : 3,
270+
(m_bitdepth > 8) ? TypeUInt16 : TypeUInt8);
245271

272+
if (m_bitdepth > 8) {
273+
m_spec.attribute("oiio:BitsPerSample", m_bitdepth);
274+
}
246275
m_spec.set_colorspace("srgb_rec709_scene");
247276

277+
#if LIBHEIF_HAVE_VERSION(1, 9, 0)
278+
// Read CICP. Have to use the C API to get it from the image handle,
279+
// the one on the decoded image is not what was written in the file.
280+
enum heif_color_profile_type profile_type
281+
= heif_image_handle_get_color_profile_type(
282+
m_ihandle.get_raw_image_handle());
283+
if (profile_type == heif_color_profile_type_nclx) {
284+
heif_color_profile_nclx* nclx = nullptr;
285+
const heif_error err = heif_image_handle_get_nclx_color_profile(
286+
m_ihandle.get_raw_image_handle(), &nclx);
287+
288+
if (nclx) {
289+
if (err.code == heif_error_Ok) {
290+
const int cicp[4] = { int(nclx->color_primaries),
291+
int(nclx->transfer_characteristics),
292+
int(nclx->matrix_coefficients),
293+
int(nclx->full_range_flag ? 1 : 0) };
294+
m_spec.attribute("CICP", TypeDesc(TypeDesc::INT, 4), cicp);
295+
}
296+
heif_nclx_color_profile_free(nclx);
297+
}
298+
}
299+
#endif
300+
248301
#if LIBHEIF_HAVE_VERSION(1, 12, 0)
249302
// Libheif >= 1.12 added API call to find out if the image is associated
250303
// alpha (i.e. colors are premultiplied).
@@ -402,7 +455,22 @@ HeifInput::read_native_scanline(int subimage, int miplevel, int y, int /*z*/,
402455
return false;
403456
}
404457
hdata += (y - m_spec.y) * ystride;
405-
memcpy(data, hdata, m_spec.width * m_spec.pixel_bytes());
458+
if (m_bitdepth == 10 || m_bitdepth == 12) {
459+
const size_t num_values = m_spec.width * m_spec.nchannels;
460+
const uint16_t* hdata16 = reinterpret_cast<const uint16_t*>(hdata);
461+
uint16_t* data16 = static_cast<uint16_t*>(data);
462+
if (m_bitdepth == 10) {
463+
for (size_t i = 0; i < num_values; ++i) {
464+
data16[i] = bit_range_convert<10, 16>(hdata16[i]);
465+
}
466+
} else {
467+
for (size_t i = 0; i < num_values; ++i) {
468+
data16[i] = bit_range_convert<12, 16>(hdata16[i]);
469+
}
470+
}
471+
} else {
472+
memcpy(data, hdata, m_spec.width * m_spec.pixel_bytes());
473+
}
406474
return true;
407475
}
408476

src/heif.imageio/heifoutput.cpp

Lines changed: 65 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@
44

55

66
#include <OpenImageIO/filesystem.h>
7+
#include <OpenImageIO/fmath.h>
78
#include <OpenImageIO/imageio.h>
9+
#include <OpenImageIO/platform.h>
810
#include <OpenImageIO/tiffutils.h>
911

1012
#include <libheif/heif_cxx.h>
@@ -26,7 +28,11 @@ class HeifOutput final : public ImageOutput {
2628
const char* format_name(void) const override { return "heif"; }
2729
int supports(string_view feature) const override
2830
{
29-
return feature == "alpha" || feature == "exif" || feature == "tiles";
31+
return feature == "alpha" || feature == "exif" || feature == "tiles"
32+
#if LIBHEIF_HAVE_VERSION(1, 9, 0)
33+
|| feature == "cicp"
34+
#endif
35+
;
3036
}
3137
bool open(const std::string& name, const ImageSpec& spec,
3238
OpenMode mode) override;
@@ -45,6 +51,7 @@ class HeifOutput final : public ImageOutput {
4551
heif::Encoder m_encoder { heif_compression_HEVC };
4652
std::vector<unsigned char> scratch;
4753
std::vector<unsigned char> m_tilebuffer;
54+
int m_bitdepth = 0;
4855
};
4956

5057

@@ -104,19 +111,33 @@ HeifOutput::open(const std::string& name, const ImageSpec& newspec,
104111

105112
m_filename = name;
106113

107-
m_spec.set_format(TypeUInt8); // Only uint8 for now
114+
m_bitdepth = m_spec.format.size() > TypeUInt8.size() ? 10 : 8;
115+
m_bitdepth = m_spec.get_int_attribute("oiio:BitsPerSample", m_bitdepth);
116+
if (m_bitdepth == 10 || m_bitdepth == 12) {
117+
m_spec.set_format(TypeUInt16);
118+
} else if (m_bitdepth == 8) {
119+
m_spec.set_format(TypeUInt8);
120+
} else {
121+
errorfmt("Unsupported bit depth {}", m_bitdepth);
122+
return false;
123+
}
108124

109125
try {
110126
m_ctx.reset(new heif::Context);
111127
m_himage = heif::Image();
112128
static heif_chroma chromas[/*nchannels*/]
113129
= { heif_chroma_undefined, heif_chroma_monochrome,
114-
heif_chroma_undefined, heif_chroma_interleaved_RGB,
115-
heif_chroma_interleaved_RGBA };
130+
heif_chroma_undefined,
131+
(m_bitdepth == 8) ? heif_chroma_interleaved_RGB
132+
: littleendian() ? heif_chroma_interleaved_RRGGBB_LE
133+
: heif_chroma_interleaved_RRGGBB_BE,
134+
(m_bitdepth == 8) ? heif_chroma_interleaved_RGBA
135+
: littleendian() ? heif_chroma_interleaved_RRGGBBAA_LE
136+
: heif_chroma_interleaved_RRGGBBAA_BE };
116137
m_himage.create(newspec.width, newspec.height, heif_colorspace_RGB,
117138
chromas[m_spec.nchannels]);
118139
m_himage.add_plane(heif_channel_interleaved, newspec.width,
119-
newspec.height, 8 * m_spec.nchannels /*bit depth*/);
140+
newspec.height, m_bitdepth);
120141

121142
m_encoder = heif::Encoder(heif_compression_HEVC);
122143
auto compqual = m_spec.decode_compression_metadata("", 75);
@@ -161,7 +182,22 @@ HeifOutput::write_scanline(int y, int /*z*/, TypeDesc format, const void* data,
161182
uint8_t* hdata = m_himage.get_plane(heif_channel_interleaved, &hystride);
162183
#endif
163184
hdata += hystride * (y - m_spec.y);
164-
memcpy(hdata, data, hystride);
185+
if (m_bitdepth == 10 || m_bitdepth == 12) {
186+
const uint16_t* data16 = static_cast<const uint16_t*>(data);
187+
uint16_t* hdata16 = reinterpret_cast<uint16_t*>(hdata);
188+
const size_t num_values = m_spec.width * m_spec.nchannels;
189+
if (m_bitdepth == 10) {
190+
for (size_t i = 0; i < num_values; ++i) {
191+
hdata16[i] = bit_range_convert<16, 10>(data16[i]);
192+
}
193+
} else {
194+
for (size_t i = 0; i < num_values; ++i) {
195+
hdata16[i] = bit_range_convert<16, 12>(data16[i]);
196+
}
197+
}
198+
} else {
199+
memcpy(hdata, data, hystride);
200+
}
165201
return true;
166202
}
167203

@@ -207,8 +243,30 @@ HeifOutput::close()
207243
} else if (compqual.first == "none") {
208244
m_encoder.set_lossless(true);
209245
}
246+
heif::Context::EncodingOptions options;
247+
#if LIBHEIF_HAVE_VERSION(1, 9, 0)
248+
// Write CICP. we can only set output_nclx_profile with the C API.
249+
std::unique_ptr<heif_color_profile_nclx,
250+
void (*)(heif_color_profile_nclx*)>
251+
nclx(heif_nclx_color_profile_alloc(), heif_nclx_color_profile_free);
252+
const ParamValue* p = m_spec.find_attribute("CICP",
253+
TypeDesc(TypeDesc::INT, 4));
254+
if (p) {
255+
const int* cicp = static_cast<const int*>(p->data());
256+
nclx->color_primaries = heif_color_primaries(cicp[0]);
257+
nclx->transfer_characteristics = heif_transfer_characteristics(
258+
cicp[1]);
259+
nclx->matrix_coefficients = heif_matrix_coefficients(cicp[2]);
260+
nclx->full_range_flag = cicp[3];
261+
options.output_nclx_profile = nclx.get();
262+
// Chroma subsampling is incompatible with RGB.
263+
if (nclx->matrix_coefficients == heif_matrix_coefficients_RGB_GBR) {
264+
m_encoder.set_string_parameter("chroma", "444");
265+
}
266+
}
267+
#endif
210268
encode_exif(m_spec, exifblob, endian::big);
211-
m_ihandle = m_ctx->encode_image(m_himage, m_encoder);
269+
m_ihandle = m_ctx->encode_image(m_himage, m_encoder, options);
212270
std::vector<char> head { 'E', 'x', 'i', 'f', 0, 0 };
213271
exifblob.insert(exifblob.begin(), head.begin(), head.end());
214272
try {

testsuite/heif/ref/out-libheif1.12-orient.txt

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,34 @@ ref/IMG_7702_small.heic : 512 x 300, 3 channel, uint8 heif
3939
Exif:SubsecTimeOriginal: "006"
4040
Exif:WhiteBalance: 0 (auto)
4141
oiio:ColorSpace: "srgb_rec709_scene"
42+
Reading ref/Chimera-AV1-8bit-162.avif
43+
ref/Chimera-AV1-8bit-162.avif : 480 x 270, 3 channel, uint8 heif
44+
SHA-1: F8FDAF1BD56A21E3AF99CF8EE7FA45434D2826C7
45+
channel list: R, G, B
46+
oiio:ColorSpace: "srgb_rec709_scene"
47+
Reading ref/test-10bit.avif
48+
ref/test-10bit.avif : 16 x 16, 4 channel, uint10 heif
49+
SHA-1: A217653C4E10FEBF080E26F9FC78F572184B1FDA
50+
channel list: R, G, B, A
51+
Software: "OpenImageIO 3.2.0.0dev : B4BD496D92983E84F1FD621682CAB821C1E2126C"
52+
Exif:ExifVersion: "0230"
53+
Exif:FlashPixVersion: "0100"
54+
Exif:ImageHistory: "oiiotool --pattern fill:topleft=1,0,0,1:topright=0,1,0,1:bottomleft=0,0,1,1:bottomright=1,1,1,1 16x16 4 -d uint16 -o test16.png"
55+
heif:UnassociatedAlpha: 1
56+
oiio:BitsPerSample: 10
57+
oiio:ColorSpace: "srgb_rec709_scene"
58+
Reading cicp_pq.avif
59+
cicp_pq.avif : 16 x 16, 4 channel, uint10 heif
60+
SHA-1: 0F3CAB52D479BC23E9C981DBADDFEF1F792E5540
61+
channel list: R, G, B, A
62+
CICP: 9, 16, 9, 1
63+
Software: "OpenImageIO 3.2.0.0dev : A50DC799B2B4CA667217608C0F82302455E5D32A"
64+
Exif:ExifVersion: "0230"
65+
Exif:FlashPixVersion: "0100"
66+
Exif:ImageHistory: "oiiotool --pattern fill:topleft=1,0,0,1:topright=0,1,0,1:bottomleft=0,0,1,1:bottomright=1,1,1,1 16x16 4 -d uint16 -o test16.png"
67+
heif:UnassociatedAlpha: 1
68+
oiio:BitsPerSample: 10
69+
oiio:ColorSpace: "srgb_rec709_scene"
4270
Reading ../oiio-images/heif/greyhounds-looking-for-a-table.heic
4371
../oiio-images/heif/greyhounds-looking-for-a-table.heic : 3024 x 4032, 3 channel, uint8 heif
4472
SHA-1: 8211F56BBABDC7615CCAF67CBF49741D1A292D2E

testsuite/heif/ref/out-libheif1.4.txt

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,29 @@ ref/Chimera-AV1-8bit-162.avif : 480 x 270, 3 channel, uint8 heif
4444
SHA-1: F8FDAF1BD56A21E3AF99CF8EE7FA45434D2826C7
4545
channel list: R, G, B
4646
oiio:ColorSpace: "srgb_rec709_scene"
47+
Reading ref/test-10bit.avif
48+
ref/test-10bit.avif : 16 x 16, 4 channel, uint10 heif
49+
SHA-1: A217653C4E10FEBF080E26F9FC78F572184B1FDA
50+
channel list: R, G, B, A
51+
Software: "OpenImageIO 3.2.0.0dev : B4BD496D92983E84F1FD621682CAB821C1E2126C"
52+
Exif:ExifVersion: "0230"
53+
Exif:FlashPixVersion: "0100"
54+
Exif:ImageHistory: "oiiotool --pattern fill:topleft=1,0,0,1:topright=0,1,0,1:bottomleft=0,0,1,1:bottomright=1,1,1,1 16x16 4 -d uint16 -o test16.png"
55+
heif:UnassociatedAlpha: 1
56+
oiio:BitsPerSample: 10
57+
oiio:ColorSpace: "srgb_rec709_scene"
58+
Reading cicp_pq.avif
59+
cicp_pq.avif : 16 x 16, 4 channel, uint10 heif
60+
SHA-1: 0F3CAB52D479BC23E9C981DBADDFEF1F792E5540
61+
channel list: R, G, B, A
62+
CICP: 9, 16, 9, 1
63+
Software: "OpenImageIO 3.2.0.0dev : A50DC799B2B4CA667217608C0F82302455E5D32A"
64+
Exif:ExifVersion: "0230"
65+
Exif:FlashPixVersion: "0100"
66+
Exif:ImageHistory: "oiiotool --pattern fill:topleft=1,0,0,1:topright=0,1,0,1:bottomleft=0,0,1,1:bottomright=1,1,1,1 16x16 4 -d uint16 -o test16.png"
67+
heif:UnassociatedAlpha: 1
68+
oiio:BitsPerSample: 10
69+
oiio:ColorSpace: "srgb_rec709_scene"
4770
Reading ../oiio-images/heif/greyhounds-looking-for-a-table.heic
4871
../oiio-images/heif/greyhounds-looking-for-a-table.heic : 3024 x 4032, 3 channel, uint8 heif
4972
SHA-1: 8211F56BBABDC7615CCAF67CBF49741D1A292D2E

testsuite/heif/ref/out-libheif1.5.txt

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,29 @@ ref/Chimera-AV1-8bit-162.avif : 480 x 270, 3 channel, uint8 heif
4444
SHA-1: F8FDAF1BD56A21E3AF99CF8EE7FA45434D2826C7
4545
channel list: R, G, B
4646
oiio:ColorSpace: "srgb_rec709_scene"
47+
Reading ref/test-10bit.avif
48+
ref/test-10bit.avif : 16 x 16, 4 channel, uint10 heif
49+
SHA-1: A217653C4E10FEBF080E26F9FC78F572184B1FDA
50+
channel list: R, G, B, A
51+
Software: "OpenImageIO 3.2.0.0dev : B4BD496D92983E84F1FD621682CAB821C1E2126C"
52+
Exif:ExifVersion: "0230"
53+
Exif:FlashPixVersion: "0100"
54+
Exif:ImageHistory: "oiiotool --pattern fill:topleft=1,0,0,1:topright=0,1,0,1:bottomleft=0,0,1,1:bottomright=1,1,1,1 16x16 4 -d uint16 -o test16.png"
55+
heif:UnassociatedAlpha: 1
56+
oiio:BitsPerSample: 10
57+
oiio:ColorSpace: "srgb_rec709_scene"
58+
Reading cicp_pq.avif
59+
cicp_pq.avif : 16 x 16, 4 channel, uint10 heif
60+
SHA-1: 0F3CAB52D479BC23E9C981DBADDFEF1F792E5540
61+
channel list: R, G, B, A
62+
CICP: 9, 16, 9, 1
63+
Software: "OpenImageIO 3.2.0.0dev : A50DC799B2B4CA667217608C0F82302455E5D32A"
64+
Exif:ExifVersion: "0230"
65+
Exif:FlashPixVersion: "0100"
66+
Exif:ImageHistory: "oiiotool --pattern fill:topleft=1,0,0,1:topright=0,1,0,1:bottomleft=0,0,1,1:bottomright=1,1,1,1 16x16 4 -d uint16 -o test16.png"
67+
heif:UnassociatedAlpha: 1
68+
oiio:BitsPerSample: 10
69+
oiio:ColorSpace: "srgb_rec709_scene"
4770
Reading ../oiio-images/heif/greyhounds-looking-for-a-table.heic
4871
../oiio-images/heif/greyhounds-looking-for-a-table.heic : 3024 x 4032, 3 channel, uint8 heif
4972
SHA-1: 8211F56BBABDC7615CCAF67CBF49741D1A292D2E

0 commit comments

Comments
 (0)