Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 24 additions & 3 deletions src/jpeg.imageio/jpeginput.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,21 @@ is_icc_profile_marker(jpeg_saved_marker_ptr marker)
sizeof(icc_marker_prefix));
}

// Ultra HDR files using the ISO 21496-1 gain map format (as written by
// libultrahdr 1.x) signal the gain map metadata in an APP2 marker whose
// payload begins with "urn:iso:std:". This is distinct from the legacy Adobe
// gain map format, which is detected via the "hdrgm:Version" XMP attribute.
static const char iso_gainmap_marker_prefix[] = "urn:iso:std:";

static bool
is_iso_gainmap_marker(jpeg_saved_marker_ptr marker)
{
return marker->marker == (JPEG_APP0 + 2)
&& marker->data_length >= sizeof(iso_gainmap_marker_prefix) - 1
&& !memcmp(marker->data, iso_gainmap_marker_prefix,
sizeof(iso_gainmap_marker_prefix) - 1);
}


// For explanations of the error handling, see the "example.c" in the
// libjpeg distribution.
Expand Down Expand Up @@ -292,7 +307,10 @@ JpgInput::open(const std::string& name, ImageSpec& newspec)
if (!subsampling.empty())
m_spec.attribute(JPEG_SUBSAMPLING_ATTR, subsampling);

bool iso_gainmap_signal = false;
for (jpeg_saved_marker_ptr m = m_cinfo.marker_list; m; m = m->next) {
if (is_iso_gainmap_marker(m))
iso_gainmap_signal = true;
if (is_exif_marker(m)) {
// The block starts with "Exif\0\0", so skip 6 bytes to get
// to the start of the actual Exif data TIFF directory
Expand Down Expand Up @@ -393,10 +411,13 @@ JpgInput::open(const std::string& name, ImageSpec& newspec)

// Try to interpret as Ultra HDR image.
// The libultrahdr API requires to load the whole file content in memory
// therefore we first check for the presence of the "hdrgm:Version" metadata
// to avoid this costly process when not necessary.
// therefore we first check for a cheap signal of the format to avoid this
// costly process when not necessary. Two gain map formats are recognized:
// the legacy Adobe format, signaled by the "hdrgm:Version" XMP attribute,
// and the ISO 21496-1 format (written by libultrahdr 1.x), signaled by an
// APP2 "urn:iso:std:" marker.
// https://developer.android.com/media/platform/hdr-image-format#signal_of_the_format
if (m_spec.find_attribute("hdrgm:Version"))
if (m_spec.find_attribute("hdrgm:Version") || iso_gainmap_signal)
m_is_uhdr = read_uhdr(m_io);

newspec = m_spec;
Expand Down
217 changes: 215 additions & 2 deletions src/jpeg.imageio/jpegoutput.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

#include <OpenImageIO/filesystem.h>
#include <OpenImageIO/fmath.h>
#include <OpenImageIO/half.h>
#include <OpenImageIO/imageio.h>
#include <OpenImageIO/tiffutils.h>

Expand All @@ -34,7 +35,11 @@ class JpgOutput final : public ImageOutput {
const char* format_name(void) const override { return "jpeg"; }
int supports(string_view feature) const override
{
return (feature == "exif" || feature == "iptc" || feature == "ioproxy");
return (feature == "exif" || feature == "iptc" || feature == "ioproxy"
#if defined(USE_UHDR)
|| feature == "uhdr"
#endif
);
}
bool open(const std::string& name, const ImageSpec& spec,
OpenMode mode = Create) override;
Expand All @@ -46,7 +51,8 @@ class JpgOutput final : public ImageOutput {
private:
std::string m_filename;
unsigned int m_dither;
int m_next_scanline; // Which scanline is the next to write?
int m_next_scanline; // Which scanline is the next to write?
bool m_compressor_created; // Has jpeg_create_compress() been called?
std::vector<unsigned char> m_scratch;
struct jpeg_compress_struct m_cinfo;
struct jpeg_error_mgr c_jerr;
Expand All @@ -65,10 +71,29 @@ class JpgOutput final : public ImageOutput {
unsigned long m_outsize = 0;
#endif

#if defined(USE_UHDR)
// Ultra HDR output. When m_write_uhdr is true, we bypass the libjpeg
// compressor entirely: scanlines are accumulated into m_uhdr_pixels (a
// full-frame packed RGBA half-float buffer) during write_scanline(), and
// the actual encode via libultrahdr happens in close(). libultrahdr needs
// the whole raw image at once, so we cannot stream it.
bool m_write_uhdr = false;
std::vector<half> m_uhdr_pixels; // Full-frame packed RGBA, 4 halfs/pixel
uhdr_color_gamut_t m_uhdr_cg = UHDR_CG_UNSPECIFIED;
int m_uhdr_quality = 95;
#endif

void init(void)
{
m_copy_coeffs = NULL;
m_copy_decompressor = NULL;
m_compressor_created = false;
#if defined(USE_UHDR)
m_write_uhdr = false;
m_uhdr_pixels.clear();
m_uhdr_cg = UHDR_CG_UNSPECIFIED;
m_uhdr_quality = 95;
#endif
ioproxy_clear();
clear_outbuffer();
}
Expand Down Expand Up @@ -96,6 +121,18 @@ class JpgOutput final : public ImageOutput {
// Read the XResolution/YResolution and PixelAspectRatio metadata, store
// in density fields m_cinfo.X_density,Y_density.
void resmeta_to_density();

#if defined(USE_UHDR)
// Validate the spec for Ultra HDR output, infer the color gamut, and
// allocate the full-frame pixel buffer. Called from open() when the
// "jpeg:ultrahdr" attribute is set. Returns false (with an error set) if
// the spec is not suitable for Ultra HDR encoding.
bool setup_uhdr_output();

// Encode the accumulated m_uhdr_pixels buffer with libultrahdr and write
// the resulting JPEG stream to the IOProxy. Called from close().
bool encode_uhdr();
#endif
};


Expand Down Expand Up @@ -142,8 +179,19 @@ JpgOutput::open(const std::string& name, const ImageSpec& newspec,
if (!ioproxy_use_or_open(name))
return false;

// Ultra HDR output. If requested, we take a completely different path that
// buffers the whole image and encodes it with libultrahdr in close(),
// rather than streaming scanlines through libjpeg.
if (m_spec.get_int_attribute("jpeg:ultrahdr")) {
if (!setup_uhdr_output())
return false;
m_next_scanline = 0;
return true;
}

m_cinfo.err = jpeg_std_error(&c_jerr); // set error handler
jpeg_create_compress(&m_cinfo); // create compressor
m_compressor_created = true;
Filesystem::IOProxy* m_io = ioproxy();
if (!strcmp(m_io->proxytype(), "file")) {
auto fd = reinterpret_cast<Filesystem::IOFile*>(m_io)->handle();
Expand Down Expand Up @@ -458,6 +506,129 @@ JpgOutput::resmeta_to_density()



#if defined(USE_UHDR)
bool
JpgOutput::setup_uhdr_output()
{
// libultrahdr's half-float HDR intent requires linear pixel data. Only
// float-based OIIO formats can carry HDR values, so reject integer specs.
if (m_spec.format != TypeDesc::HALF && m_spec.format != TypeDesc::FLOAT) {
errorfmt(
"JPEG Ultra HDR output requires a half or float pixel data type, "
"not \"{}\"",
m_spec.format);
return false;
}
if (m_spec.nchannels < 3) {
errorfmt("JPEG Ultra HDR output requires at least 3 (RGB) channels, "
"not {}",
m_spec.nchannels);
return false;
}

// The encoder mandates a known color gamut (BT.709 / Display-P3 / BT.2100)
// and linear transfer for the half-float HDR intent. Infer the gamut from
// the image's linear scene-referred color space; anything else (non-linear
// transfer, or primaries we can't map, e.g. ACEScg) is an error.
string_view cs = m_spec.get_string_attribute("oiio:ColorSpace");
if (equivalent_colorspace(cs, "lin_rec709_scene"))
m_uhdr_cg = UHDR_CG_BT_709;
else if (equivalent_colorspace(cs, "lin_p3d65_scene"))
m_uhdr_cg = UHDR_CG_DISPLAY_P3;
else if (equivalent_colorspace(cs, "lin_rec2020_scene"))
m_uhdr_cg = UHDR_CG_BT_2100;
else {
errorfmt(
"JPEG Ultra HDR output requires a linear Rec.709, P3-D65, or "
"Rec.2020 color space, but the image color space is \"{}\"",
cs.size() ? cs : string_view("unknown"));
return false;
}

auto compqual = m_spec.decode_compression_metadata("jpeg", 95);
if (Strutil::iequals(compqual.first, "jpeg"))
m_uhdr_quality = clamp(compqual.second, 1, 100);
else
m_uhdr_quality = 95;

// Allocate the full-frame packed RGBA half buffer. write_scanline() fills
// it row by row; encode_uhdr() consumes it in close().
m_uhdr_pixels.assign(size_t(m_spec.width) * size_t(m_spec.height) * 4,
half(0.0f));
m_write_uhdr = true;
return true;
}



bool
JpgOutput::encode_uhdr()
{
uhdr_raw_image_t img {};
img.fmt = UHDR_IMG_FMT_64bppRGBAHalfFloat;
img.cg = m_uhdr_cg;
img.ct = UHDR_CT_LINEAR;
img.range = UHDR_CR_FULL_RANGE;
img.w = m_spec.width;
img.h = m_spec.height;
img.planes[UHDR_PLANE_PACKED] = m_uhdr_pixels.data();
img.planes[UHDR_PLANE_U] = nullptr;
img.planes[UHDR_PLANE_V] = nullptr;
img.stride[UHDR_PLANE_PACKED] = m_spec.width; // in pixels
img.stride[UHDR_PLANE_U] = 0;
img.stride[UHDR_PLANE_V] = 0;

uhdr_codec_private_t* enc = uhdr_create_encoder();
if (!enc) {
errorfmt("Could not create Ultra HDR encoder");
return false;
}

// Helper to check a libultrahdr call result, emit a useful error, and
// release the encoder on failure.
auto check = [&](const uhdr_error_info_t& err, const char* what) -> bool {
if (err.error_code == UHDR_CODEC_OK)
return true;
if (err.has_detail)
errorfmt("Ultra HDR {} failed (code {}): {}", what,
int(err.error_code), err.detail);
else
errorfmt("Ultra HDR {} failed (code {})", what, int(err.error_code));
uhdr_release_encoder(enc);
return false;
};

if (!check(uhdr_enc_set_raw_image(enc, &img, UHDR_HDR_IMG), "set_raw_image"))
return false;
if (!check(uhdr_enc_set_quality(enc, m_uhdr_quality, UHDR_BASE_IMG),
"set_quality"))
return false;
if (!check(uhdr_enc_set_output_format(enc, UHDR_CODEC_JPG),
"set_output_format"))
return false;
if (!check(uhdr_encode(enc), "encode"))
return false;

uhdr_compressed_image_t* stream = uhdr_get_encoded_stream(enc);
if (!stream || !stream->data) {
errorfmt("Ultra HDR encoder produced no output stream");
uhdr_release_encoder(enc);
return false;
}

if (ioproxy()->write(stream->data, stream->data_sz) != stream->data_sz) {
errorfmt("Error writing Ultra HDR data to \"{}\"", m_filename);
uhdr_release_encoder(enc);
return false;
}

uhdr_release_encoder(enc);
return true;
}
#endif



bool
JpgOutput::write_scanline(int y, int z, TypeDesc format, const void* data,
stride_t xstride)
Expand All @@ -467,6 +638,30 @@ JpgOutput::write_scanline(int y, int z, TypeDesc format, const void* data,
errorfmt("Attempt to write scanlines out of order to {}", m_filename);
return false;
}

#if defined(USE_UHDR)
if (m_write_uhdr) {
if (y >= m_spec.height) {
errorfmt("Attempt to write too many scanlines to {}", m_filename);
return false;
}
m_spec.auto_stride(xstride, format, m_spec.nchannels);
// Destination row: packed RGBA half. Pre-fill alpha to 1.0 so that
// RGB (3-channel) inputs get an opaque alpha; RGBA inputs overwrite it.
half* row = m_uhdr_pixels.data() + size_t(y) * m_spec.width * 4;
for (int x = 0; x < m_spec.width; ++x)
row[x * 4 + 3] = half(1.0f);
// Copy up to 4 channels (RGB, or RGBA) into the packed buffer,
// converting to half. Any channels beyond 4 are dropped.
int ncopy = std::min(m_spec.nchannels, 4);
convert_image(ncopy, m_spec.width, 1, 1, data, format, xstride,
AutoStride, AutoStride, row, TypeDesc::HALF,
4 * sizeof(half), AutoStride, AutoStride);
++m_next_scanline;
return true;
}
#endif

if (y >= (int)m_cinfo.image_height) {
errorfmt("Attempt to write too many scanlines to {}", m_filename);
return false;
Expand Down Expand Up @@ -517,6 +712,24 @@ JpgOutput::close()
return true;
}

#if defined(USE_UHDR)
if (m_write_uhdr) {
// If the caller wrote fewer scanlines than the full height, the tail of
// m_uhdr_pixels is left at its initialized value (0 with alpha 1.0),
// which is a reasonable fill. Encode whatever we have.
bool ok = encode_uhdr();
init();
return ok;
}
#endif

if (!m_compressor_created) {
// open() failed before the libjpeg compressor was created (e.g. a
// rejected Ultra HDR request). Nothing to finalize; just reset.
init();
return true;
}

if (m_next_scanline < spec().height && m_copy_coeffs == NULL) {
// But if we've only written some scanlines, write the rest to avoid
// errors
Expand Down
Loading