Writing ImageIO Plugins#
Plugin Introduction#
As explained in Chapters ImageInput: Reading Images and ImageOutput: Writing Images, the ImageIO library does not know how to read or write any particular image formats, but rather relies on plugins located and loaded dynamically at run-time. This set of plugins, and therefore the set of image file formats that OpenImageIO or its clients can read and write, is extensible without needing to modify OpenImageIO itself.
This chapter explains how to write your own OpenImageIO plugins. We will first explain separately how to write image file readers and writers, then tie up the loose ends of how to build the plugins themselves.
Image Reader Plugins#
A plugin that reads a particular image file format must implement a subclass of ImageInput (described in Chapter ImageInput: Reading Images). This is actually very straightforward and consists of the following steps, which we will illustrate with a real-world example of writing a JPEG/JFIF plug-in.
Read the base class definition from
imageio.h
. It may also be helpful to enclose the contents of your plugin in the same namespace that the OpenImageIO library uses:#include <OpenImageIO/imageio.h> OIIO_PLUGIN_NAMESPACE_BEGIN // ... everything else ... OIIO_PLUGIN_NAMESPACE_END
Declare these public items:
An integer called
name_imageio_version
that identifies the version of the ImageIO protocol implemented by the plugin, defined inimageio.h
as the constantOIIO_PLUGIN_VERSION
. This allows the library to be sure it is not loading a plugin that was compiled against an incompatible version of OpenImageIO.An function named
name_imageio_library_version
that identifies the underlying dependent library that is responsible for reading or writing the format (it may returnnullptr
to indicate that there is no dependent library being used for this format).A function named
name_input_imageio_create
that takes no arguments and returns anImageInput *
constructed from a new instance of your ImageInput subclass and a deleter. (Note that name is the name of your format, and must match the name of the plugin itself.)An array of
char *
calledname_input_extensions
that contains the list of file extensions that are likely to indicate a file of the right format. The list is terminated by anullptr
.
All of these items must be inside an
extern "C"
block in order to avoid name mangling by the C++ compiler, and we provide handy macrosOIIO_PLUGIN_EXPORTS_BEGIN
andOIIO_PLUGIN_EXPORTS_END
to make this easy. Depending on your compiler, you may need to use special commands to dictate that the symbols will be exported in the DSO; we provide a specialOIIO_EXPORT
macro for this purpose, defined inexport.h
.Putting this all together, we get the following for our JPEG example:
OIIO_PLUGIN_EXPORTS_BEGIN OIIO_EXPORT int jpeg_imageio_version = OIIO_PLUGIN_VERSION; OIIO_EXPORT ImageInput *jpeg_input_imageio_create () { return new JpgInput; } OIIO_EXPORT const char *jpeg_input_extensions[] = { "jpg", "jpe", "jpeg", "jif", "jfif", "jfi", nullptr }; OIIO_EXPORT const char* jpeg_imageio_library_version () { #define STRINGIZE2(a) #a #define STRINGIZE(a) STRINGIZE2(a) #ifdef LIBJPEG_TURBO_VERSION return "jpeg-turbo " STRINGIZE(LIBJPEG_TURBO_VERSION); #else return "jpeglib " STRINGIZE(JPEG_LIB_VERSION_MAJOR) "." STRINGIZE(JPEG_LIB_VERSION_MINOR); #endif } OIIO_PLUGIN_EXPORTS_END
The definition and implementation of an ImageInput subclass for this file format. It must publicly inherit ImageInput, and must overload the following methods which are “pure virtual” in the ImageInput base class:
format_name()
should return the name of the format, which ought to match the name of the plugin and by convention is strictly lower-case and contains no whitespace.open()
should open the file and return true, or should return false if unable to do so (including if the file was found but turned out not to be in the format that your plugin is trying to implement).close()
should close the file, if open.read_native_scanline()
should read a single scanline from the file into the address provided, uncompressing it but keeping it in its naive data format without any translation.The virtual destructor, which should
close()
if the file is still open, addition to performing any other tear-down activities.
Additionally, your ImageInput subclass may optionally choose to overload any of the following methods, which are defined in the ImageInput base class and only need to be overloaded if the default behavior is not appropriate for your plugin:
supports()
, only if your format supports any of the optional features described in the section describingImageInput::supports
.valid_file()
, if your format has a way to determine if a file is of the given format in a way that is less expensive than a fullopen()
.seek_subimage()
, only if your format supports reading multiple subimages within a single file.read_native_scanlines()
, only if your format has a speed advantage when reading multiple scanlines at once. If you do not supply this function, the default implementation will simply callread_scanline()
for each scanline in the range.read_native_tile()
, only if your format supports reading tiled images.read_native_tiles()
, only if your format supports reading tiled images and there is a speed advantage when reading multiple tiles at once. If you do not supply this function, the default implementation will simply callread_native_tile()
for each tile in the range.Channel subset'' versions of ``read_native_scanlines()
and/orread_native_tiles()
, only if your format has a more efficient means of reading a subset of channels. If you do not supply these methods, the default implementation will simply useread_native_scanlines()
orread_native_tiles()
to read into a temporary all-channel buffer and then copy the channel subset into the user’s buffer.read_native_deep_scanlines()
and/orread_native_deep_tiles()
, only if your format supports “deep” data images.
Here is how the class definition looks for our JPEG example. Note that the JPEG/JFIF file format does not support multiple subimages or tiled images.
class JpgInput final : public ImageInput { public: JpgInput () { init(); } ~JpgInput () override { close(); } const char * format_name (void) const override { return "jpeg"; } bool open (const std::string &name, ImageSpec &spec) override; bool read_native_scanline (int y, int z, void *data) override; bool close () override; private: FILE *m_fd; bool m_first_scanline; struct jpeg_decompress_struct m_cinfo; struct jpeg_error_mgr m_jerr; void init () { m_fd = NULL; } };
Your subclass implementation of open()
, close()
, and
read_native_scanline()
are the heart of an ImageInput implementation.
(Also read_native_tile()
and seek_subimage()
, for those image
formats that support them.)
The remainder of this section simply lists the full implementation of our
JPEG reader, which relies heavily on the open source jpeg-6b
library
to perform the actual JPEG decoding.
// Copyright Contributors to the OpenImageIO project.
// SPDX-License-Identifier: Apache-2.0
// https://github.com/AcademySoftwareFoundation/OpenImageIO
#include <algorithm>
#include <cassert>
#include <cstdio>
#include <OpenImageIO/filesystem.h>
#include <OpenImageIO/fmath.h>
#include <OpenImageIO/imageio.h>
#include <OpenImageIO/strutil.h>
#include <OpenImageIO/tiffutils.h>
#include "jpeg_pvt.h"
OIIO_PLUGIN_NAMESPACE_BEGIN
// N.B. The class definition for JpgInput is in jpeg_pvt.h.
// Export version number and create function symbols
OIIO_PLUGIN_EXPORTS_BEGIN
OIIO_EXPORT int jpeg_imageio_version = OIIO_PLUGIN_VERSION;
OIIO_EXPORT const char*
jpeg_imageio_library_version()
{
#ifdef LIBJPEG_TURBO_VERSION
return "jpeg-turbo " OIIO_STRINGIZE(
LIBJPEG_TURBO_VERSION) "/jp" OIIO_STRINGIZE(JPEG_LIB_VERSION);
#else
return "jpeglib " OIIO_STRINGIZE(JPEG_LIB_VERSION_MAJOR) "." OIIO_STRINGIZE(
JPEG_LIB_VERSION_MINOR);
#endif
}
OIIO_EXPORT ImageInput*
jpeg_input_imageio_create()
{
return new JpgInput;
}
OIIO_EXPORT const char* jpeg_input_extensions[]
= { "jpg", "jpe", "jpeg", "jif", "jfif", "jfi", nullptr };
OIIO_PLUGIN_EXPORTS_END
static const uint8_t JPEG_MAGIC1 = 0xff;
static const uint8_t JPEG_MAGIC2 = 0xd8;
// For explanations of the error handling, see the "example.c" in the
// libjpeg distribution.
static void
my_error_exit(j_common_ptr cinfo)
{
/* cinfo->err really points to a my_error_mgr struct, so coerce pointer */
JpgInput::my_error_ptr myerr = (JpgInput::my_error_ptr)cinfo->err;
/* Always display the message. */
/* We could postpone this until after returning, if we chose. */
// (*cinfo->err->output_message) (cinfo);
myerr->jpginput->jpegerror(myerr, true);
/* Return control to the setjmp point */
longjmp(myerr->setjmp_buffer, 1);
}
static void
my_output_message(j_common_ptr cinfo)
{
JpgInput::my_error_ptr myerr = (JpgInput::my_error_ptr)cinfo->err;
// Create the message
char buffer[JMSG_LENGTH_MAX];
(*cinfo->err->format_message)(cinfo, buffer);
myerr->jpginput->jpegerror(myerr, false);
// This function is called only for non-fatal problems, so we don't
// need to do the longjmp.
// longjmp(myerr->setjmp_buffer, 1);
}
static std::string
comp_info_to_attr(const jpeg_decompress_struct& cinfo)
{
// Compare the current 6 samples with our known definitions
// to determine the corresponding subsampling attr
std::vector<int> comp;
comp.push_back(cinfo.comp_info[0].h_samp_factor);
comp.push_back(cinfo.comp_info[0].v_samp_factor);
comp.push_back(cinfo.comp_info[1].h_samp_factor);
comp.push_back(cinfo.comp_info[1].v_samp_factor);
comp.push_back(cinfo.comp_info[2].h_samp_factor);
comp.push_back(cinfo.comp_info[2].v_samp_factor);
size_t size = comp.size();
if (std::equal(JPEG_444_COMP, JPEG_444_COMP + size, comp.begin()))
return JPEG_444_STR;
else if (std::equal(JPEG_422_COMP, JPEG_422_COMP + size, comp.begin()))
return JPEG_422_STR;
else if (std::equal(JPEG_420_COMP, JPEG_420_COMP + size, comp.begin()))
return JPEG_420_STR;
else if (std::equal(JPEG_411_COMP, JPEG_411_COMP + size, comp.begin()))
return JPEG_411_STR;
return "";
}
void
JpgInput::jpegerror(my_error_ptr /*myerr*/, bool fatal)
{
// Send the error message to the ImageInput
char errbuf[JMSG_LENGTH_MAX];
(*m_cinfo.err->format_message)((j_common_ptr)&m_cinfo, errbuf);
errorfmt("JPEG error: {} (\"{}\")", errbuf, filename());
// Shut it down and clean it up
if (fatal) {
m_fatalerr = true;
close();
m_fatalerr = true; // because close() will reset it
}
}
bool
JpgInput::valid_file(Filesystem::IOProxy* ioproxy) const
{
// Check magic number to assure this is a JPEG file
if (!ioproxy || ioproxy->mode() != Filesystem::IOProxy::Read)
return false;
uint8_t magic[2] {};
const size_t numRead = ioproxy->pread(magic, sizeof(magic), 0);
return numRead == sizeof(magic) && magic[0] == JPEG_MAGIC1
&& magic[1] == JPEG_MAGIC2;
}
bool
JpgInput::open(const std::string& name, ImageSpec& newspec,
const ImageSpec& config)
{
auto p = config.find_attribute("_jpeg:raw", TypeInt);
m_raw = p && *(int*)p->data();
ioproxy_retrieve_from_config(config);
m_config.reset(new ImageSpec(config)); // save config spec
return open(name, newspec);
}
bool
JpgInput::open(const std::string& name, ImageSpec& newspec)
{
m_filename = name;
if (!ioproxy_use_or_open(name))
return false;
// If an IOProxy was passed, it had better be a File or a
// MemReader, that's all we know how to use with jpeg.
Filesystem::IOProxy* m_io = ioproxy();
std::string proxytype = m_io->proxytype();
if (proxytype != "file" && proxytype != "memreader") {
errorfmt("JPEG reader can't handle proxy type {}", proxytype);
return false;
}
// Check magic number to assure this is a JPEG file
uint8_t magic[2] = { 0, 0 };
if (m_io->pread(magic, sizeof(magic), 0) != sizeof(magic)) {
errorfmt("Empty file \"{}\"", name);
close_file();
return false;
}
if (magic[0] != JPEG_MAGIC1 || magic[1] != JPEG_MAGIC2) {
close_file();
errorfmt(
"\"{}\" is not a JPEG file, magic number doesn't match (was 0x{:x}{:x})",
name, int(magic[0]), int(magic[1]));
return false;
}
// Set up the normal JPEG error routines, then override error_exit and
// output_message so we intercept all the errors.
m_cinfo.err = jpeg_std_error((jpeg_error_mgr*)&m_jerr);
m_jerr.pub.error_exit = my_error_exit;
m_jerr.pub.output_message = my_output_message;
if (setjmp(m_jerr.setjmp_buffer)) {
// Jump to here if there's a libjpeg internal error
// Prevent memory leaks, see example.c in jpeg distribution
jpeg_destroy_decompress(&m_cinfo);
close_file();
return false;
}
// initialize decompressor
jpeg_create_decompress(&m_cinfo);
m_decomp_create = true;
// specify the data source
if (proxytype == "file") {
auto fd = reinterpret_cast<Filesystem::IOFile*>(m_io)->handle();
jpeg_stdio_src(&m_cinfo, fd);
} else {
auto buffer = reinterpret_cast<Filesystem::IOMemReader*>(m_io)->buffer();
jpeg_mem_src(&m_cinfo, const_cast<unsigned char*>(buffer.data()),
buffer.size());
}
// Request saving of EXIF and other special tags for later spelunking
for (int mark = 0; mark < 16; ++mark)
jpeg_save_markers(&m_cinfo, JPEG_APP0 + mark, 0xffff);
jpeg_save_markers(&m_cinfo, JPEG_COM, 0xffff); // comment marker
// read the file parameters
if (jpeg_read_header(&m_cinfo, FALSE) != JPEG_HEADER_OK || m_fatalerr) {
errorfmt("Bad JPEG header for \"{}\"", filename());
return false;
}
int nchannels = m_cinfo.num_components;
if (m_cinfo.jpeg_color_space == JCS_CMYK
|| m_cinfo.jpeg_color_space == JCS_YCCK) {
// CMYK jpegs get converted by us to RGB
m_cinfo.out_color_space = JCS_CMYK; // pre-convert YCbCrK->CMYK
nchannels = 3;
m_cmyk = true;
}
if (m_raw)
m_coeffs = jpeg_read_coefficients(&m_cinfo);
else
jpeg_start_decompress(&m_cinfo); // start working
if (m_fatalerr)
return false;
m_next_scanline = 0; // next scanline we'll read
m_spec = ImageSpec(m_cinfo.output_width, m_cinfo.output_height, nchannels,
TypeDesc::UINT8);
if (!check_open(m_spec, { 0, 1 << 16, 0, 1 << 16, 0, 1, 0, 3 }))
return false;
// Assume JPEG is in sRGB unless the Exif or XMP tags say otherwise.
m_spec.set_colorspace("sRGB");
if (m_cinfo.jpeg_color_space == JCS_CMYK)
m_spec.attribute("jpeg:ColorSpace", "CMYK");
else if (m_cinfo.jpeg_color_space == JCS_YCCK)
m_spec.attribute("jpeg:ColorSpace", "YCbCrK");
// If the chroma subsampling is detected and matches something
// we expect, then set an attribute so that it can be preserved
// in future operations.
std::string subsampling = comp_info_to_attr(m_cinfo);
if (!subsampling.empty())
m_spec.attribute(JPEG_SUBSAMPLING_ATTR, subsampling);
for (jpeg_saved_marker_ptr m = m_cinfo.marker_list; m; m = m->next) {
if (m->marker == (JPEG_APP0 + 1)
&& !strcmp((const char*)m->data, "Exif")) {
// The block starts with "Exif\0\0", so skip 6 bytes to get
// to the start of the actual Exif data TIFF directory
decode_exif(string_view((char*)m->data + 6, m->data_length - 6),
m_spec);
} else if (m->marker == (JPEG_APP0 + 1)
&& !strcmp((const char*)m->data,
"http://ns.adobe.com/xap/1.0/")) { //NOSONAR
std::string xml((const char*)m->data, m->data_length);
decode_xmp(xml, m_spec);
} else if (m->marker == (JPEG_APP0 + 13)
&& !strcmp((const char*)m->data, "Photoshop 3.0"))
jpeg_decode_iptc((unsigned char*)m->data);
else if (m->marker == JPEG_COM) {
std::string data((const char*)m->data, m->data_length);
// Additional string metadata can be stored in JPEG files as
// comment markers in the form "key:value" or "ident:key:value".
// If the string contains a single colon, we assume key:value.
// If there's multiple, we try splitting as ident:key:value and
// check if ident and key are reasonable (in particular, whether
// ident is a C-style identifier and key is not surrounded by
// whitespace). If ident passes but key doesn't, assume key:value.
auto separator = data.find(':');
if (OIIO::get_int_attribute("jpeg:com_attributes")
&& (separator != std::string::npos && separator > 0)) {
std::string left = data.substr(0, separator);
std::string right = data.substr(separator + 1);
separator = right.find(':');
if (separator != std::string::npos && separator > 0) {
std::string mid = right.substr(0, separator);
std::string value = right.substr(separator + 1);
if (Strutil::string_is_identifier(left)
&& (mid == Strutil::trimmed_whitespace(mid))) {
// Valid parsing: left is ident, mid is key
std::string attribute = left + ":" + mid;
if (!m_spec.find_attribute(attribute, TypeDesc::STRING))
m_spec.attribute(attribute, value);
continue;
}
}
if (left == Strutil::trimmed_whitespace(left)) {
// Valid parsing: left is key, right is value
if (!m_spec.find_attribute(left, TypeDesc::STRING))
m_spec.attribute(left, right);
continue;
}
}
// If we made it this far, treat the comment as a description
if (!m_spec.find_attribute("ImageDescription", TypeDesc::STRING))
m_spec.attribute("ImageDescription", data);
}
}
// Handle density/pixelaspect. We need to do this AFTER the exif is
// decoded, in case it contains useful information.
float xdensity = m_spec.get_float_attribute("XResolution");
float ydensity = m_spec.get_float_attribute("YResolution");
if (m_cinfo.X_density && m_cinfo.Y_density) {
xdensity = float(m_cinfo.X_density);
ydensity = float(m_cinfo.Y_density);
if (xdensity > 1 && ydensity > 1) {
m_spec.attribute("XResolution", xdensity);
m_spec.attribute("YResolution", ydensity);
// We're kind of assuming that if either cinfo.X_density or
// Y_density is 1, then those fields are only used to indicate
// pixel aspect ratio, but don't override [XY]Resolution that may
// have come from the Exif.
}
}
if (xdensity && ydensity) {
// Pixel aspect ratio SHOULD be computed like this:
// float aspect = ydensity / xdensity;
// But Nuke and Photoshop do it backwards, and so we do, too, because
// we are lemmings.
float aspect = xdensity / ydensity;
if (aspect != 1.0f)
m_spec.attribute("PixelAspectRatio", aspect);
if (m_spec.extra_attribs.contains("XResolution")) {
switch (m_cinfo.density_unit) {
case 0: m_spec.attribute("ResolutionUnit", "none"); break;
case 1: m_spec.attribute("ResolutionUnit", "in"); break;
case 2: m_spec.attribute("ResolutionUnit", "cm"); break;
}
}
}
read_icc_profile(&m_cinfo, m_spec); /// try to read icc profile
// 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.
// https://developer.android.com/media/platform/hdr-image-format#signal_of_the_format
if (m_spec.find_attribute("hdrgm:Version"))
m_is_uhdr = read_uhdr(m_io);
newspec = m_spec;
return true;
}
bool
JpgInput::read_icc_profile(j_decompress_ptr cinfo, ImageSpec& spec)
{
int num_markers = 0;
std::vector<uint8_t> icc_buf;
unsigned int total_length = 0;
const int MAX_SEQ_NO = 255;
unsigned char marker_present
[MAX_SEQ_NO
+ 1]; // one extra is used to store the flag if marker is found, set to one if marker is found
unsigned int data_length[MAX_SEQ_NO + 1]; // store the size of each marker
unsigned int data_offset[MAX_SEQ_NO + 1]; // store the offset of each marker
memset(marker_present, 0, (MAX_SEQ_NO + 1));
for (jpeg_saved_marker_ptr m = cinfo->marker_list; m; m = m->next) {
if (m->marker == (JPEG_APP0 + 2)
&& !strcmp((const char*)m->data, "ICC_PROFILE")) {
if (num_markers == 0)
num_markers = GETJOCTET(m->data[13]);
else if (num_markers != GETJOCTET(m->data[13]))
return false;
int seq_no = GETJOCTET(m->data[12]);
if (seq_no <= 0 || seq_no > num_markers)
return false;
if (marker_present[seq_no]) // duplicate marker
return false;
marker_present[seq_no] = 1; // flag found marker
data_length[seq_no] = m->data_length - ICC_HEADER_SIZE;
}
}
if (num_markers == 0)
return false;
// checking for missing markers
for (int seq_no = 1; seq_no <= num_markers; seq_no++) {
if (marker_present[seq_no] == 0)
return false; // missing sequence number
data_offset[seq_no] = total_length;
total_length += data_length[seq_no];
}
if (total_length == 0)
return false; // found only empty markers
icc_buf.resize(total_length * sizeof(JOCTET));
// and fill it in
for (jpeg_saved_marker_ptr m = cinfo->marker_list; m; m = m->next) {
if (m->marker == (JPEG_APP0 + 2)
&& !strcmp((const char*)m->data, "ICC_PROFILE")) {
int seq_no = GETJOCTET(m->data[12]);
memcpy(icc_buf.data() + data_offset[seq_no],
m->data + ICC_HEADER_SIZE, data_length[seq_no]);
}
}
spec.attribute("ICCProfile", TypeDesc(TypeDesc::UINT8, total_length),
icc_buf.data());
std::string errormsg;
bool ok = decode_icc_profile(icc_buf, spec, errormsg);
if (!ok) {
errorfmt("Possible corrupt file, could not decode ICC profile: {}\n",
errormsg);
return false;
}
return true;
}
bool
JpgInput::read_uhdr(Filesystem::IOProxy* ioproxy)
{
#if defined(USE_UHDR)
// Read entire file content into buffer.
const size_t buffer_size = ioproxy->size();
std::vector<unsigned char> buffer(buffer_size);
ioproxy->pread(buffer.data(), buffer_size, 0);
// Check if this is an actual Ultra HDR image.
const bool detect_uhdr = is_uhdr_image(buffer.data(), buffer.size());
if (!detect_uhdr)
return false;
// Create Ultra HDR decoder.
// Do not forget to release it once we don't need it,
// i.e if this function returns false
// or when we call close().
m_uhdr_dec = uhdr_create_decoder();
// Prepare decoder input.
// Note: we currently do not override any of the
// default settings.
uhdr_compressed_image_t uhdr_compressed;
uhdr_compressed.data = buffer.data();
uhdr_compressed.data_sz = buffer.size();
uhdr_compressed.capacity = buffer.size();
uhdr_dec_set_image(m_uhdr_dec, &uhdr_compressed);
// Decode Ultra HDR image
// and check for decoding errors.
uhdr_error_info_t err_info = uhdr_decode(m_uhdr_dec);
if (err_info.error_code != UHDR_CODEC_OK) {
errorfmt("Ultra HDR decoding failed with error code {}",
int(err_info.error_code));
if (err_info.has_detail != 0)
errorfmt("Additional error details: {}", err_info.detail);
uhdr_release_decoder(m_uhdr_dec);
return false;
}
// Update spec with decoded image properties.
// Note: we currently only support a subset of all possible
// Ultra HDR image formats.
uhdr_raw_image_t* uhdr_raw = uhdr_get_decoded_image(m_uhdr_dec);
int nchannels;
TypeDesc desc;
switch (uhdr_raw->fmt) {
case UHDR_IMG_FMT_32bppRGBA8888:
nchannels = 4;
desc = TypeDesc::UINT8;
break;
case UHDR_IMG_FMT_64bppRGBAHalfFloat:
nchannels = 4;
desc = TypeDesc::HALF;
break;
case UHDR_IMG_FMT_24bppRGB888:
nchannels = 3;
desc = TypeDesc::UINT8;
break;
default:
errorfmt("Unsupported Ultra HDR image format: {}", int(uhdr_raw->fmt));
uhdr_release_decoder(m_uhdr_dec);
return false;
}
ImageSpec newspec = ImageSpec(uhdr_raw->w, uhdr_raw->h, nchannels, desc);
newspec.extra_attribs = std::move(m_spec.extra_attribs);
m_spec = newspec;
return true;
#else
return false;
#endif
}
static void
cmyk_to_rgb(int n, const unsigned char* cmyk, size_t cmyk_stride,
unsigned char* rgb, size_t rgb_stride)
{
for (; n; --n, cmyk += cmyk_stride, rgb += rgb_stride) {
// JPEG seems to store CMYK as 1-x
float C = convert_type<unsigned char, float>(cmyk[0]);
float M = convert_type<unsigned char, float>(cmyk[1]);
float Y = convert_type<unsigned char, float>(cmyk[2]);
float K = convert_type<unsigned char, float>(cmyk[3]);
float R = C * K;
float G = M * K;
float B = Y * K;
rgb[0] = convert_type<float, unsigned char>(R);
rgb[1] = convert_type<float, unsigned char>(G);
rgb[2] = convert_type<float, unsigned char>(B);
}
}
bool
JpgInput::read_native_scanline(int subimage, int miplevel, int y, int /*z*/,
void* data)
{
lock_guard lock(*this);
if (!seek_subimage(subimage, miplevel))
return false;
if (m_raw)
return false;
if (y < 0 || y >= (int)m_cinfo.output_height) // out of range scanline
return false;
if (m_next_scanline > y) {
// User is trying to read an earlier scanline than the one we're
// up to. Easy fix: close the file and re-open.
// Don't forget to save and restore any configuration settings.
ImageSpec configsave;
if (m_config)
configsave = *m_config;
ImageSpec dummyspec;
int subimage = current_subimage();
if (!close() || !open(m_filename, dummyspec, configsave)
|| !seek_subimage(subimage, 0))
return false; // Somehow, the re-open failed
OIIO_DASSERT(m_next_scanline == 0 && current_subimage() == subimage);
}
#if defined(USE_UHDR)
if (m_is_uhdr) {
uhdr_raw_image_t* uhdr_raw = uhdr_get_decoded_image(m_uhdr_dec);
unsigned int nbytes;
switch (uhdr_raw->fmt) {
case UHDR_IMG_FMT_32bppRGBA8888: nbytes = 4; break;
case UHDR_IMG_FMT_64bppRGBAHalfFloat: nbytes = 8; break;
case UHDR_IMG_FMT_24bppRGB888: nbytes = 3; break;
default: return false;
}
const size_t row_size = uhdr_raw->stride[UHDR_PLANE_PACKED] * nbytes;
unsigned char* top_left = static_cast<unsigned char*>(
uhdr_raw->planes[UHDR_PLANE_PACKED]);
unsigned char* row_data_start = top_left + row_size * y;
memcpy(data, row_data_start, row_size);
return true;
}
#endif
// Set up our custom error handler
if (setjmp(m_jerr.setjmp_buffer)) {
// Jump to here if there's a libjpeg internal error
return false;
}
void* readdata = data;
if (m_cmyk) {
// If the file's data is CMYK, read into a 4-channel buffer, then
// we'll have to convert.
m_cmyk_buf.resize(m_spec.width * 4);
readdata = &m_cmyk_buf[0];
OIIO_DASSERT(m_spec.nchannels == 3);
}
for (; m_next_scanline <= y; ++m_next_scanline) {
// Keep reading until we've read the scanline we really need
if (jpeg_read_scanlines(&m_cinfo, (JSAMPLE**)&readdata, 1) != 1
|| m_fatalerr) {
errorfmt("JPEG failed scanline read (\"{}\")", filename());
return false;
}
}
if (m_cmyk)
cmyk_to_rgb(m_spec.width, (unsigned char*)readdata, 4,
(unsigned char*)data, 3);
return true;
}
bool
JpgInput::close()
{
if (ioproxy_opened()) {
// unnecessary? jpeg_abort_decompress (&m_cinfo);
if (m_decomp_create)
jpeg_destroy_decompress(&m_cinfo);
m_decomp_create = false;
#if defined(USE_UHDR)
if (m_is_uhdr)
uhdr_release_decoder(m_uhdr_dec);
m_is_uhdr = false;
#endif
close_file();
}
init(); // Reset to initial state
return true;
}
void
JpgInput::jpeg_decode_iptc(const unsigned char* buf)
{
// APP13 blob doesn't have to be IPTC info. Look for the IPTC marker,
// which is the string "Photoshop 3.0" followed by a null character.
if (strcmp((const char*)buf, "Photoshop 3.0"))
return;
buf += strlen("Photoshop 3.0") + 1;
// Next are the 4 bytes "8BIM"
if (strncmp((const char*)buf, "8BIM", 4))
return;
buf += 4;
// Next two bytes are the segment type, in big endian.
// We expect 1028 to indicate IPTC data block.
if (((buf[0] << 8) + buf[1]) != 1028)
return;
buf += 2;
// Next are 4 bytes of 0 padding, just skip it.
buf += 4;
// Next is 2 byte (big endian) giving the size of the segment
int segmentsize = (buf[0] << 8) + buf[1];
buf += 2;
decode_iptc_iim(buf, segmentsize, m_spec);
}
OIIO_PLUGIN_NAMESPACE_END
Image Writers#
A plugin that writes a particular image file format must implement a subclass of ImageOutput (described in Chapter ImageOutput: Writing Images). This is actually very straightforward and consists of the following steps, which we will illustrate with a real-world example of writing a JPEG/JFIF plug-in.
Read the base class definition from
imageio.h
, just as with an image reader (see Section Image Reader Plugins).Declare four public items:
An integer called
name_imageio_version
that identifies the version of the ImageIO protocol implemented by the plugin, defined inimageio.h
as the constantOIIO_PLUGIN_VERSION
. This allows the library to be sure it is not loading a plugin that was compiled against an incompatible version of OpenImageIO. Note that if your plugin has both a reader and writer and they are compiled as separate modules (C++ source files), you don’t want to declare this in both modules; either one is fine.A function named
name_output_imageio_create
that takes no arguments and returns anImageOutput *
constructed from a new instance of your ImageOutput subclass and a deleter. (Note that name is the name of your format, and must match the name of the plugin itself.)An array of
char *
calledname_output_extensions
that contains the list of file extensions that are likely to indicate a file of the right format. The list is terminated by anullptr
pointer.
All of these items must be inside an
extern "C"
block in order to avoid name mangling by the C++ compiler, and we provide handy macrosOIIO_PLUGIN_EXPORTS_BEGIN
andOIIO_PLUGIN_EXPORTS_END
to mamke this easy. Depending on your compiler, you may need to use special commands to dictate that the symbols will be exported in the DSO; we provide a specialOIIO_EXPORT
macro for this purpose, defined inexport.h
.Putting this all together, we get the following for our JPEG example:
OIIO_PLUGIN_EXPORTS_BEGIN OIIO_EXPORT int jpeg_imageio_version = OIIO_PLUGIN_VERSION; OIIO_EXPORT ImageOutput *jpeg_output_imageio_create () { return new JpgOutput; } OIIO_EXPORT const char *jpeg_input_extensions[] = { "jpg", "jpe", "jpeg", nullptr }; OIIO_PLUGIN_EXPORTS_END
The definition and implementation of an ImageOutput subclass for this file format. It must publicly inherit ImageOutput, and must overload the following methods which are “pure virtual” in the ImageOutput base class:
format_name()
should return the name of the format, which ought to match the name of the plugin and by convention is strictly lower-case and contains no whitespace.supports()
should returntrue
if its argument names a feature supported by your format plugin,false
if it names a feature not supported by your plugin. See the description ofImageOutput::supports()
for the list of feature names.open()
should open the file and return true, or should return false if unable to do so (including if the file was found but turned out not to be in the format that your plugin is trying to implement).close()
should close the file, if open.write_scanline()
should write a single scanline to the file, translating from internal to native data format and handling strides properly.The virtual destructor, which should
close()
if the file is still open, addition to performing any other tear-down activities.
Additionally, your ImageOutput subclass may optionally choose to overload any of the following methods, which are defined in the ImageOutput base class and only need to be overloaded if the default behavior is not appropriate for your plugin:
write_scanlines()
, only if your format supports writing scanlines and you can get a performance improvement when outputting multiple scanlines at once. If you don’t supplywrite_scanlines()
, the default implementation will simply callwrite_scanline()
separately for each scanline in the range.write_tile()
, only if your format supports writing tiled images.write_tiles()
, only if your format supports writing tiled images and you can get a performance improvement when outputting multiple tiles at once. If you don’t supplywrite_tiles()
, the default implementation will simply callwrite_tile()
separately for each tile in the range.write_rectangle()
, only if your format supports writing arbitrary rectangles.write_image()
, only if you have a more clever method of doing so than the default implementation that callswrite_scanline()
orwrite_tile()
repeatedly.write_deep_scanlines()
and/orwrite_deep_tiles()
, only if your format supports “deep” data images.
It is not strictly required, but certainly appreciated, if a file format does not support tiles, to nonetheless accept an ImageSpec that specifies tile sizes by allocating a full-image buffer in
open()
, providing an implementation ofwrite_tile()
that copies the tile of data to the right spots in the buffer, and havingclose()
then callwrite_scanlines()
to process the buffer now that the image has been fully sent.Here is how the class definition looks for our JPEG example. Note that the JPEG/JFIF file format does not support multiple subimages or tiled images.
class JpgOutput final : public ImageOutput { public: JpgOutput () { init(); } ~JpgOutput () override { close(); } const char * format_name (void) const override { return "jpeg"; } int supports (string_view property) const override { return false; } bool open (const std::string &name, const ImageSpec &spec, bool append=false) override; bool write_scanline (int y, int z, TypeDesc format, const void *data, stride_t xstride) override; bool close (); private: FILE *m_fd; std::vector<unsigned char> m_scratch; struct jpeg_compress_struct m_cinfo; struct jpeg_error_mgr m_jerr; void init () { m_fd = NULL; } };
Your subclass implementation of open()
, close()
, and
write_scanline()
are the heart of an ImageOutput implementation.
(Also write_tile()
, for those image formats that support tiled
output.)
An ImageOutput implementation must properly handle all data formats and
strides passed to write_scanline()
or write_tile()
, unlike
an ImageInput implementation, which only needs to read scanlines or
tiles in their native format and then have the super-class handle the
translation. But don’t worry, all the heavy lifting can be accomplished
with the following helper functions provided as protected member
functions of ImageOutput that convert a scanline, tile, or rectangular
array of values from one format to the native format(s) of the file.
-
const void *to_native_scanline(TypeDesc format, const void *data, stride_t xstride, std::vector<unsigned char> &scratch, unsigned int dither = 0, int yorigin = 0, int zorigin = 0)#
Convert a full scanline of pixels (pointed to by
data
with the givenformat
and strides into contiguous pixels in the native format (described by the ImageSpec returned by thespec()
member function). The location of the newly converted data is returned, which may either be the originaldata
itself if no data conversion was necessary and the requested layout was contiguous (thereby avoiding unnecessary memory copies), or may point into memory allocated within thescratch
vector passed by the user. In either case, the caller doesn’t need to worry about thread safety or freeing any allocated memory (other than eventually destroying the scratch vector).
-
const void *to_native_tile(TypeDesc format, const void *data, stride_t xstride, stride_t ystride, stride_t zstride, std::vector<unsigned char> &scratch, unsigned int dither = 0, int xorigin = 0, int yorigin = 0, int zorigin = 0)#
Convert a full tile of pixels (pointed to by
data
with the givenformat
and strides into contiguous pixels in the native format (described by the ImageSpec returned by thespec()
member function). The location of the newly converted data is returned, which may either be the originaldata
itself if no data conversion was necessary and the requested layout was contiguous (thereby avoiding unnecessary memory copies), or may point into memory allocated within thescratch
vector passed by the user. In either case, the caller doesn’t need to worry about thread safety or freeing any allocated memory (other than eventually destroying the scratch vector).
-
const void *to_native_rectangle(int xbegin, int xend, int ybegin, int yend, int zbegin, int zend, TypeDesc format, const void *data, stride_t xstride, stride_t ystride, stride_t zstride, std::vector<unsigned char> &scratch, unsigned int dither = 0, int xorigin = 0, int yorigin = 0, int zorigin = 0)#
Convert a rectangle of pixels (pointed to by
data
with the givenformat
, dimensions, and strides into contiguous pixels in the native format (described by the ImageSpec returned by thespec()
member function). The location of the newly converted data is returned, which may either be the originaldata
itself if no data conversion was necessary and the requested layout was contiguous (thereby avoiding unnecessary memory copies), or may point into memory allocated within thescratch
vector passed by the user. In either case, the caller doesn’t need to worry about thread safety or freeing any allocated memory (other than eventually destroying the scratch vector).
For float
to 8 bit integer conversions only, if dither
parameter is
nonzero, random dither will be added to reduce quantization banding
artifacts; in this case, the specific nonzero dither
value is used as a
seed for the hash function that produces the per-pixel dither amounts, and
the optional origin
parameters help it to align the pixels to the right
position in the dither pattern.
The remainder of this section simply lists the full implementation of
our JPEG writer, which relies heavily on the open source jpeg-6b
library to perform the actual JPEG encoding.
// Copyright Contributors to the OpenImageIO project.
// SPDX-License-Identifier: BSD-3-Clause and Apache-2.0
// https://github.com/AcademySoftwareFoundation/OpenImageIO
#include <cassert>
#include <cstdio>
#include <set>
#include <vector>
#include <OpenImageIO/filesystem.h>
#include <OpenImageIO/fmath.h>
#include <OpenImageIO/imageio.h>
#include <OpenImageIO/tiffutils.h>
#include "jpeg_pvt.h"
OIIO_PLUGIN_NAMESPACE_BEGIN
#define DBG if (0)
// References:
// * JPEG library documentation: /usr/share/doc/libjpeg-devel-6b
// * JFIF spec: https://www.w3.org/Graphics/JPEG/jfif3.pdf
// * ITU T.871 (aka ISO/IEC 10918-5):
// https://www.itu.int/rec/T-REC-T.871-201105-I/en
class JpgOutput final : public ImageOutput {
public:
JpgOutput() { init(); }
~JpgOutput() override { close(); }
const char* format_name(void) const override { return "jpeg"; }
int supports(string_view feature) const override
{
return (feature == "exif" || feature == "iptc" || feature == "ioproxy");
}
bool open(const std::string& name, const ImageSpec& spec,
OpenMode mode = Create) override;
bool write_scanline(int y, int z, TypeDesc format, const void* data,
stride_t xstride) override;
bool write_tile(int x, int y, int z, TypeDesc format, const void* data,
stride_t xstride, stride_t ystride,
stride_t zstride) override;
bool close() override;
bool copy_image(ImageInput* in) override;
private:
std::string m_filename;
unsigned int m_dither;
int m_next_scanline; // Which scanline is the next to write?
std::vector<unsigned char> m_scratch;
struct jpeg_compress_struct m_cinfo;
struct jpeg_error_mgr c_jerr;
jvirt_barray_ptr* m_copy_coeffs;
struct jpeg_decompress_struct* m_copy_decompressor;
std::vector<unsigned char> m_tilebuffer;
// m_outbuffer/m_outsize are used for jpeg-to-memory
unsigned char* m_outbuffer = nullptr;
#if OIIO_JPEG_LIB_VERSION >= 94
// libjpeg switched jpeg_mem_dest() from accepting a `unsigned long*`
// to a `size_t*` in version 9d.
size_t m_outsize = 0;
#else
// libjpeg < 9d, and so far all libjpeg-turbo releases, have a
// jpeg_mem_dest() declaration that needs this to be unsigned long.
unsigned long m_outsize = 0;
#endif
void init(void)
{
m_copy_coeffs = NULL;
m_copy_decompressor = NULL;
ioproxy_clear();
clear_outbuffer();
}
void clear_outbuffer()
{
if (m_outbuffer) {
free(m_outbuffer);
m_outbuffer = nullptr;
}
m_outsize = 0;
}
void set_subsampling(const int components[])
{
jpeg_set_colorspace(&m_cinfo, JCS_YCbCr);
m_cinfo.comp_info[0].h_samp_factor = components[0];
m_cinfo.comp_info[0].v_samp_factor = components[1];
m_cinfo.comp_info[1].h_samp_factor = components[2];
m_cinfo.comp_info[1].v_samp_factor = components[3];
m_cinfo.comp_info[2].h_samp_factor = components[4];
m_cinfo.comp_info[2].v_samp_factor = components[5];
}
// Read the XResolution/YResolution and PixelAspectRatio metadata, store
// in density fields m_cinfo.X_density,Y_density.
void resmeta_to_density();
};
OIIO_PLUGIN_EXPORTS_BEGIN
OIIO_EXPORT ImageOutput*
jpeg_output_imageio_create()
{
return new JpgOutput;
}
OIIO_EXPORT const char* jpeg_output_extensions[]
= { "jpg", "jpe", "jpeg", "jif", "jfif", "jfi", nullptr };
OIIO_PLUGIN_EXPORTS_END
static std::set<std::string> metadata_include { "oiio:ConstantColor",
"oiio:AverageColor",
"oiio:SHA-1" };
static std::set<std::string> metadata_exclude {
"XResolution", "YResolution", "PixelAspectRatio",
"ResolutionUnit", "Orientation", "ImageDescription"
};
bool
JpgOutput::open(const std::string& name, const ImageSpec& newspec,
OpenMode mode)
{
// Save name and spec for later use
m_filename = name;
if (!check_open(mode, newspec,
{ 0, JPEG_MAX_DIMENSION, 0, JPEG_MAX_DIMENSION, 0, 1, 0,
256 }))
return false;
// NOTE: we appear to let a large number of channels be allowed, but
// that's only because we robustly truncate to only RGB no matter what we
// are handed.
ioproxy_retrieve_from_config(m_spec);
if (!ioproxy_use_or_open(name))
return false;
m_cinfo.err = jpeg_std_error(&c_jerr); // set error handler
jpeg_create_compress(&m_cinfo); // create compressor
Filesystem::IOProxy* m_io = ioproxy();
if (!strcmp(m_io->proxytype(), "file")) {
auto fd = reinterpret_cast<Filesystem::IOFile*>(m_io)->handle();
jpeg_stdio_dest(&m_cinfo, fd); // set output stream
} else {
clear_outbuffer();
jpeg_mem_dest(&m_cinfo, &m_outbuffer, &m_outsize);
}
// Set image and compression parameters
m_cinfo.image_width = m_spec.width;
m_cinfo.image_height = m_spec.height;
// JFIF can only handle grayscale and RGB. Do the best we can with this
// limited format by switching to 1 or 3 channels.
if (m_spec.nchannels >= 3) {
// For 3 or more channels, write the first 3 as RGB and drop any
// additional channels.
m_cinfo.input_components = 3;
m_cinfo.in_color_space = JCS_RGB;
} else if (m_spec.nchannels == 2) {
// Two channels are tricky. If the first channel name is "Y", assume
// it's a luminance image and write it as a single-channel grayscale.
// Otherwise, punt, write it as an RGB image with third channel black.
if (m_spec.channel_name(0) == "Y") {
m_cinfo.input_components = 1;
m_cinfo.in_color_space = JCS_GRAYSCALE;
} else {
m_cinfo.input_components = 3;
m_cinfo.in_color_space = JCS_RGB;
}
} else {
// One channel, assume it's grayscale
m_cinfo.input_components = 1;
m_cinfo.in_color_space = JCS_GRAYSCALE;
}
resmeta_to_density();
m_cinfo.write_JFIF_header = TRUE;
if (m_copy_coeffs) {
// Back door for copy()
jpeg_copy_critical_parameters(m_copy_decompressor, &m_cinfo);
DBG std::cout << "out open: copy_critical_parameters\n";
jpeg_write_coefficients(&m_cinfo, m_copy_coeffs);
DBG std::cout << "out open: write_coefficients\n";
} else {
// normal write of scanlines
jpeg_set_defaults(&m_cinfo); // default compression
// Careful -- jpeg_set_defaults overwrites density
resmeta_to_density();
DBG std::cout << "out open: set_defaults\n";
auto compqual = m_spec.decode_compression_metadata("jpeg", 98);
if (Strutil::iequals(compqual.first, "jpeg"))
jpeg_set_quality(&m_cinfo, clamp(compqual.second, 1, 100), TRUE);
else
jpeg_set_quality(&m_cinfo, 98, TRUE); // not jpeg? default qual
if (m_cinfo.input_components == 3) {
std::string subsampling = m_spec.get_string_attribute(
JPEG_SUBSAMPLING_ATTR);
if (subsampling == JPEG_444_STR)
set_subsampling(JPEG_444_COMP);
else if (subsampling == JPEG_422_STR)
set_subsampling(JPEG_422_COMP);
else if (subsampling == JPEG_420_STR)
set_subsampling(JPEG_420_COMP);
else if (subsampling == JPEG_411_STR)
set_subsampling(JPEG_411_COMP);
}
DBG std::cout << "out open: set_colorspace\n";
// Save as a progressive jpeg if requested by the user
if (m_spec.get_int_attribute("jpeg:progressive")) {
jpeg_simple_progression(&m_cinfo);
}
jpeg_start_compress(&m_cinfo, TRUE); // start working
DBG std::cout << "out open: start_compress\n";
}
m_next_scanline = 0; // next scanline we'll write
// Write JPEG comment, if sent an 'ImageDescription'
std::string comment = m_spec.get_string_attribute("ImageDescription");
if (comment.size()) {
jpeg_write_marker(&m_cinfo, JPEG_COM, (JOCTET*)comment.c_str(),
comment.size() + 1);
}
// Write other metadata as JPEG comments if requested
if (m_spec.get_int_attribute("jpeg:com_attributes")) {
for (const auto& p : m_spec.extra_attribs) {
std::string name = p.name().string();
auto colon = name.find(':');
if (metadata_include.count(name)) {
// Allow explicitly included metadata
} else if (metadata_exclude.count(name))
continue; // Suppress metadata that is processed separately
else if (Strutil::istarts_with(name, "ICCProfile"))
continue; // Suppress ICC profile, gets written separately
else if (colon != ustring::npos) {
auto prefix = p.name().substr(0, colon);
if (Strutil::iequals(prefix, "oiio"))
continue; // Suppress internal metadata
else if (Strutil::iequals(prefix, "exif")
|| Strutil::iequals(prefix, "GPS")
|| Strutil::iequals(prefix, "XMP"))
continue; // Suppress EXIF metadata, gets written separately
else if (Strutil::iequals(prefix, "iptc"))
continue; // Suppress IPTC metadata
else if (is_imageio_format_name(prefix))
continue; // Suppress format-specific metadata
}
auto data = p.name().string() + ":" + p.get_string();
jpeg_write_marker(&m_cinfo, JPEG_COM, (JOCTET*)data.c_str(),
data.size());
}
}
if (equivalent_colorspace(m_spec.get_string_attribute("oiio:ColorSpace"),
"sRGB"))
m_spec.attribute("Exif:ColorSpace", 1);
// Write EXIF info
std::vector<char> exif;
// Start the blob with "Exif" and two nulls. That's how it
// always is in the JPEG files I've examined.
exif.push_back('E');
exif.push_back('x');
exif.push_back('i');
exif.push_back('f');
exif.push_back(0);
exif.push_back(0);
encode_exif(m_spec, exif);
jpeg_write_marker(&m_cinfo, JPEG_APP0 + 1, (JOCTET*)exif.data(),
exif.size());
// Write IPTC IIM metadata tags, if we have anything
std::vector<char> iptc;
if (m_spec.get_int_attribute("jpeg:iptc", 1)
&& encode_iptc_iim(m_spec, iptc)) {
static char photoshop[] = "Photoshop 3.0";
std::vector<char> head(photoshop, photoshop + strlen(photoshop) + 1);
static char _8BIM[] = "8BIM";
head.insert(head.end(), _8BIM, _8BIM + 4);
head.push_back(4); // 0x0404
head.push_back(4);
head.push_back(0); // four bytes of zeroes
head.push_back(0);
head.push_back(0);
head.push_back(0);
head.push_back((char)(iptc.size() >> 8)); // size of block
head.push_back((char)(iptc.size() & 0xff));
iptc.insert(iptc.begin(), head.begin(), head.end());
jpeg_write_marker(&m_cinfo, JPEG_APP0 + 13, (JOCTET*)iptc.data(),
iptc.size());
}
// Write XMP packet, if we have anything
std::string xmp = encode_xmp(m_spec, true);
if (!xmp.empty()) {
static char prefix[] = "http://ns.adobe.com/xap/1.0/"; //NOSONAR
std::vector<char> block(prefix, prefix + strlen(prefix) + 1);
block.insert(block.end(), xmp.c_str(), xmp.c_str() + xmp.length());
jpeg_write_marker(&m_cinfo, JPEG_APP0 + 1, (JOCTET*)&block[0],
block.size());
}
m_spec.set_format(TypeDesc::UINT8); // JPG is only 8 bit
// Write ICC profile, if we have anything
if (auto icc_profile_parameter = m_spec.find_attribute(ICC_PROFILE_ATTR)) {
cspan<unsigned char> icc_profile((unsigned char*)
icc_profile_parameter->data(),
icc_profile_parameter->type().size());
if (icc_profile.size() && icc_profile.data()) {
/* Calculate the number of markers we'll need, rounding up of course */
size_t num_markers = icc_profile.size() / MAX_DATA_BYTES_IN_MARKER;
if (num_markers * MAX_DATA_BYTES_IN_MARKER
!= std::size(icc_profile))
num_markers++;
int curr_marker = 1; /* per spec, count starts at 1*/
std::vector<JOCTET> profile(MAX_DATA_BYTES_IN_MARKER
+ ICC_HEADER_SIZE);
size_t icc_profile_length = icc_profile.size();
while (icc_profile_length > 0) {
// length of profile to put in this marker
size_t length = std::min(icc_profile_length,
size_t(MAX_DATA_BYTES_IN_MARKER));
icc_profile_length -= length;
// Write the JPEG marker header (APP2 code and marker length)
strcpy((char*)profile.data(), "ICC_PROFILE"); // NOSONAR
profile[11] = 0;
profile[12] = curr_marker;
profile[13] = (JOCTET)num_markers;
OIIO_ASSERT(profile.size() >= ICC_HEADER_SIZE + length);
spancpy(make_span(profile), ICC_HEADER_SIZE, icc_profile,
length * (curr_marker - 1), length);
jpeg_write_marker(&m_cinfo, JPEG_APP0 + 2, profile.data(),
ICC_HEADER_SIZE + length);
curr_marker++;
}
}
}
m_dither = m_spec.get_int_attribute("oiio:dither", 0);
// If user asked for tiles -- which JPEG doesn't support, emulate it by
// buffering the whole image.
if (m_spec.tile_width && m_spec.tile_height)
m_tilebuffer.resize(m_spec.image_bytes());
return true;
}
void
JpgOutput::resmeta_to_density()
{
// Clear cruft from Exif that might confuse us
m_spec.erase_attribute("exif:XResolution");
m_spec.erase_attribute("exif:YResolution");
m_spec.erase_attribute("exif:ResolutionUnit");
string_view resunit = m_spec.get_string_attribute("ResolutionUnit");
if (Strutil::iequals(resunit, "none"))
m_cinfo.density_unit = 0;
else if (Strutil::iequals(resunit, "in"))
m_cinfo.density_unit = 1;
else if (Strutil::iequals(resunit, "cm"))
m_cinfo.density_unit = 2;
else
m_cinfo.density_unit = 0;
// We want to use the metadata to set the X_density and Y_density fields in
// the JPEG header, but the problem is over-constrained. What are the
// possibilities?
//
// what is set? xres yres par
// assume 72,72 par=1
// * set yres = xres (par = 1.0)
// * set xres=yres (par = 1.0)
// * * keep (par is implied)
// * set yres=72, xres based on par
// * * set yres based on par
// * * set xres based on par
// * * * par wins if they don't match
//
float XRes = m_spec.get_float_attribute("XResolution");
float YRes = m_spec.get_float_attribute("YResolution");
float aspect = m_spec.get_float_attribute("PixelAspectRatio");
if (aspect <= 0.0f) {
// PixelAspectRatio was not set in the ImageSpec. So just use the
// "resolution" values and pass them without judgment. If only one was
// set, make them equal and assume 1.0 aspect ratio. If neither were
// set, punt and set the fields to 0.
if (XRes <= 0.0f && YRes <= 0.0f) {
// No clue, set the fields to 1,1 to be valid and 1.0 aspect.
m_cinfo.X_density = 1;
m_cinfo.Y_density = 1;
return;
}
if (XRes <= 0.0f)
XRes = YRes;
if (YRes <= 0.0f)
YRes = XRes;
aspect = YRes / XRes;
} else {
// PixelAspectRatio was set in the ImageSpec. Let that trump the
// "resolution" fields, if they contradict.
//
// Here's where things get tricky. By logic and reason, as well as
// the JFIF spec and ITU T.871, the pixel aspect ratio is clearly
// ydensity/xdensity (because aspect is xlength/ylength, and density
// is 1/length). BUT... for reasons lost to history, a number of
// apps get this exactly backwards, and these include PhotoShop,
// Nuke, and RV. So, alas, we must replicate the mistake, or else
// all these common applications will misunderstand the JPEG files
// written by OIIO and vice versa. In other words, we must reverse
// the sense of how aspect ratio relates to density, contradicting
// the JFIF spec but conforming to Nuke/etc's behavior. Sigh.
if (XRes <= 0.0f && YRes <= 0.0f) {
// resolutions were not set
if (aspect >= 1.0f) {
XRes = 72.0f;
YRes = XRes / aspect;
} else {
YRes = 72.0f;
XRes = YRes * aspect;
}
} else if (XRes <= 0.0f) {
// Xres not set, but Yres was and we know aspect
// e.g., yres = 100, aspect = 2.0
// This SHOULD be the right answer:
// XRes = YRes / aspect;
// But because of the note above, reverse it:
// XRes = YRes * aspect;
XRes = YRes * aspect;
} else {
// All other cases -- XRes is set, so reset Yres to conform to
// the requested PixelAspectRatio.
// This SHOULD be the right answer:
// YRes = XRes * aspect;
// But because of the note above, reverse it:
// YRes = XRes / aspect;
YRes = XRes / aspect;
}
}
int X_density = clamp(int(XRes + 0.5f), 1, 65535);
int Y_density = clamp(int(YRes + 0.5f), 1, 65535);
m_cinfo.X_density = X_density;
m_cinfo.Y_density = Y_density;
}
bool
JpgOutput::write_scanline(int y, int z, TypeDesc format, const void* data,
stride_t xstride)
{
y -= m_spec.y;
if (y != m_next_scanline) {
errorfmt("Attempt to write scanlines out of order to {}", m_filename);
return false;
}
if (y >= (int)m_cinfo.image_height) {
errorfmt("Attempt to write too many scanlines to {}", m_filename);
return false;
}
assert(y == (int)m_cinfo.next_scanline);
// Here's where we do the dirty work of conforming to JFIF's limitation
// of 1 or 3 channels, by temporarily doctoring the spec so that
// to_native_scanline properly contiguizes the first 1 or 3 channels,
// then we restore it. The call to to_native_scanline below needs
// m_spec.nchannels to be set to the true number of channels we're
// writing, or it won't arrange the data properly. But if we doctored
// m_spec.nchannels permanently, then subsequent calls to write_scanline
// (including any surrounding call to write_image) with
// stride=AutoStride would screw up the strides since the user's stride
// is actually not 1 or 3 channels.
m_spec.auto_stride(xstride, format, m_spec.nchannels);
int save_nchannels = m_spec.nchannels;
m_spec.nchannels = m_cinfo.input_components;
if (save_nchannels == 2 && m_spec.nchannels == 3) {
// Edge case: expanding 2 channels to 3
uint8_t* tmp = OIIO_ALLOCA(uint8_t, m_spec.width * 3);
memset(tmp, 0, m_spec.width * 3);
convert_image(2, m_spec.width, 1, 1, data, format, xstride, AutoStride,
AutoStride, tmp, TypeDesc::UINT8, 3 * sizeof(uint8_t),
AutoStride, AutoStride);
data = tmp;
} else {
data = to_native_scanline(format, data, xstride, m_scratch, m_dither, y,
z);
}
m_spec.nchannels = save_nchannels;
jpeg_write_scanlines(&m_cinfo, (JSAMPLE**)&data, 1);
++m_next_scanline;
return true;
}
bool
JpgOutput::write_tile(int x, int y, int z, TypeDesc format, const void* data,
stride_t xstride, stride_t ystride, stride_t zstride)
{
// Emulate tiles by buffering the whole image
return copy_tile_to_image_buffer(x, y, z, format, data, xstride, ystride,
zstride, &m_tilebuffer[0]);
}
bool
JpgOutput::close()
{
if (!ioproxy_opened()) { // Already closed
init();
return true;
}
bool ok = true;
if (m_spec.tile_width) {
// We've been emulating tiles; now dump as scanlines.
OIIO_DASSERT(m_tilebuffer.size());
ok &= write_scanlines(m_spec.y, m_spec.y + m_spec.height, 0,
m_spec.format, &m_tilebuffer[0]);
std::vector<unsigned char>().swap(m_tilebuffer); // free it
}
if (m_next_scanline < spec().height && m_copy_coeffs == NULL) {
// But if we've only written some scanlines, write the rest to avoid
// errors
std::vector<char> buf(spec().scanline_bytes(), 0);
char* data = &buf[0];
while (m_next_scanline < spec().height) {
jpeg_write_scanlines(&m_cinfo, (JSAMPLE**)&data, 1);
++m_next_scanline;
}
}
if (m_next_scanline >= spec().height || m_copy_coeffs) {
DBG std::cout << "out close: about to finish_compress\n";
jpeg_finish_compress(&m_cinfo);
DBG std::cout << "out close: finish_compress\n";
} else {
DBG std::cout << "out close: about to abort_compress\n";
jpeg_abort_compress(&m_cinfo);
DBG std::cout << "out close: abort_compress\n";
}
DBG std::cout << "out close: about to destroy_compress\n";
jpeg_destroy_compress(&m_cinfo);
if (m_outsize) {
// We had an IOProxy of some type that was not IOFile. JPEG doesn't
// have fully general IO overloads, but it can write to memory
// buffers, we did that, so now we have to copy that in one big chunk
// to IOProxy.
ioproxy()->write(m_outbuffer, m_outsize);
}
init();
return ok;
}
bool
JpgOutput::copy_image(ImageInput* in)
{
if (in && !strcmp(in->format_name(), "jpeg")) {
JpgInput* jpg_in = dynamic_cast<JpgInput*>(in);
std::string in_name = jpg_in->filename();
DBG std::cout << "JPG copy_image from " << in_name << "\n";
// Save the original input spec and close it
ImageSpec orig_in_spec = in->spec();
in->close();
DBG std::cout << "Closed old file\n";
// Re-open the input spec, with special request that the JpgInput
// will recognize as a request to merely open, but not start the
// decompressor.
ImageSpec in_spec;
ImageSpec config_spec;
config_spec.attribute("_jpeg:raw", 1);
in->open(in_name, in_spec, config_spec);
// Re-open the output
std::string out_name = m_filename;
ImageSpec orig_out_spec = spec();
close();
m_copy_coeffs = (jvirt_barray_ptr*)jpg_in->coeffs();
m_copy_decompressor = &jpg_in->m_cinfo;
open(out_name, orig_out_spec);
// Strangeness -- the write_coefficients somehow sets things up
// so that certain writes only happen in close(), which MUST
// happen while the input file is still open. So we go ahead
// and close() now, so that the caller of copy_image() doesn't
// close the input file first and then wonder why they crashed.
close();
return true;
}
return ImageOutput::copy_image(in);
}
OIIO_PLUGIN_NAMESPACE_END
Tips and Conventions#
OpenImageIO’s main goal is to hide all the pesky details of individual file formats from the client application. This inevitably leads to various mismatches between a file format’s true capabilities and requests that may be made through the OpenImageIO APIs. This section outlines conventions, tips, and rules of thumb that we recommend for image file support.
Readers
If the file format stores images in a non-spectral color space (for example, YUV), the reader should automatically convert to RGB to pass through the OIIO APIs. In such a case, the reader should signal the file’s true color space via a
"Foo:colorspace"
attribute in the ImageSpec.“Palette” images should be automatically converted by the reader to RGB.
If the file supports thumbnail images in its header, the reader should provide an
ImageInput::get_thumbnail()
method, as well as store the thumbnail dimensions in the ImageSpec as attributes"thumbnail_width"
,"thumbnail_height"
, and"thumbnail_nchannels"
(all of which should beint
).
Writers
The overall rule of thumb is: try to always “succeed” at writing the file,
outputting the closest approximation of the user’s data as possible. But it
is permissible to fail the open()
call if it is clearly nonsensical or
there is no possible way to output a decent approximation of the user’s
data. Some tips:
If the client application requests a data format not directly supported by the file type, silently write the supported data format that will result in the least precision or range loss.
It is customary to fail a call to
open()
if the ImageSpec requested a number of color channels plainly not supported by the file format. As an exception to this rule, it is permissible for a file format that does not support alpha channels to silently drop the fourth (alpha) channel of a 4-channel output request.If the app requests a
"Compression"
not supported by the file format, you may choose as a default any lossless compression supported. Do not use a lossy compression unless you are fairly certain that the app wanted a lossy compression.If the file format is able to store images in a non-spectral color space (for example, YUV), the writer may accept a
"Foo:colorspace"
attribute in the ImageSpec as a request to automatically convert and store the data in that format (but it will always be passed as RGB through the OIIO APIs).If the file format can support thumbnail images in its header, and the ImageSpec contain attributes
"thumbnail_width"
,"thumbnail_height"
,"thumbnail_nchannels"
, and"thumbnail_image"
, the writer should attempt to store the thumbnail if possible.