Files
2026-06-14 19:09:18 +01:00

694 lines
26 KiB
C++

// -*- tab-width: 4; -*-
// vi: set sw=2 ts=4 expandtab:
// Copyright 2022 The Khronos Group Inc.
// SPDX-License-Identifier: Apache-2.0
//!
//! @internal
//! @~English
//! @file
//!
//! @brief Base classes for image input and output plugins
//!
//! The API for these classes is inspired by that of OpenImageIO. We don't use
//! OIIO because
//! - the total size, with all its dependencies, is 128 Mb. There is no easy
//! way, i.e. via cmake comnfiguration, to omit plugins that are of no
//! interest.
//! - it takes between 40m and 1hr on the CI services to build it and all
//! its dependencies
//! - I have consistently been unable to build the vcpkg version of it for
//! all the platforms we need. vcpkg is the only package manager whose
//! installed products are redistributable.
//!
#pragma once
#include "stdafx.h"
#include <algorithm>
#include <cctype>
#include <iostream>
#include <fstream>
#include <functional>
#include <limits>
#include <map>
#include <string>
#include <sstream>
#include <vector>
#include <memory>
#include <fmt/format.h>
#include <math.h>
#include "formatdesc.h"
using stride_t = int64_t;
const stride_t AutoStride = std::numeric_limits<stride_t>::min();
typedef bool (*ProgressCallback)(void *opaque_data, float portion_done);
class ImageSpec {
public:
struct Origin {
uint8_t x;
uint8_t y;
uint8_t z;
static const uint8_t eLeft = 0;
static const uint8_t eRight = 1;
static const uint8_t eTop = 0;
static const uint8_t eBottom = 1;
static const uint8_t eFront = 0;
static const uint8_t eBack = 1;
static const uint8_t eUnspecified = 0xff;
// This is the most common origin among image file formats, hence
// the no arg constructor sets it. If unspecified, use the 3 arg ctor.
Origin() : x(eLeft), y(eTop), z(eFront) { };
Origin(uint8_t _x, uint8_t _y) : x(_x), y(_y), z(eFront) { }
Origin(uint8_t _x, uint8_t _y, uint8_t _z) : x(_x), y(_y), z(_z) { }
bool unspecified() {
return x == eUnspecified || y == eUnspecified || z == eUnspecified;
}
};
protected:
FormatDescriptor formatDesc;
uint32_t imageWidth; ///< width of the pixel data
uint32_t imageHeight; ///< height of the pixel data
uint32_t imageDepth; ///< depth of pixel data, >1 indicates a "volume"
Origin imageOrigin; ///< logical corner of image that is the first pixel in the data stream
public:
ImageSpec() : imageWidth(0), imageHeight(0),
imageDepth(0), imageOrigin() { }
ImageSpec(uint32_t w, uint32_t h, uint32_t d, FormatDescriptor& formatDesc)
: ImageSpec(w, h, d, Origin(), formatDesc) { }
ImageSpec(uint32_t w, uint32_t h, uint32_t d, Origin&& o,
FormatDescriptor formatDesc)
: formatDesc(std::move(formatDesc)),
imageWidth(w), imageHeight(h), imageDepth(d), imageOrigin(o) { }
ImageSpec(uint32_t w, uint32_t h, uint32_t d,
uint32_t channelCount, uint32_t channelBitCount,
khr_df_sample_datatype_qualifiers_e dt
= static_cast<khr_df_sample_datatype_qualifiers_e>(0),
khr_df_transfer_e t = KHR_DF_TRANSFER_UNSPECIFIED,
khr_df_primaries_e p = KHR_DF_PRIMARIES_BT709,
khr_df_model_e m = KHR_DF_MODEL_RGBSDA,
khr_df_flags_e f = KHR_DF_FLAG_ALPHA_STRAIGHT)
: ImageSpec(w, h, d, Origin(), channelCount,
channelBitCount, dt, t, p, m, f) { }
ImageSpec(uint32_t w, uint32_t h, uint32_t d, Origin&& o,
uint32_t channelCount, uint32_t channelBitCount,
khr_df_sample_datatype_qualifiers_e dt
= static_cast<khr_df_sample_datatype_qualifiers_e>(0),
khr_df_transfer_e t = KHR_DF_TRANSFER_UNSPECIFIED,
khr_df_primaries_e p = KHR_DF_PRIMARIES_BT709,
khr_df_model_e m = KHR_DF_MODEL_RGBSDA,
khr_df_flags_e f = KHR_DF_FLAG_ALPHA_STRAIGHT)
: formatDesc(channelCount, channelBitCount, dt, t, p, m, f),
imageWidth(w), imageHeight(h), imageDepth(d), imageOrigin(o) { }
ImageSpec(uint32_t w, uint32_t h, uint32_t d,
uint32_t channelCount, uint32_t channelBitCount,
uint32_t channelLower, uint32_t channelUpper,
khr_df_sample_datatype_qualifiers_e dt
= static_cast<khr_df_sample_datatype_qualifiers_e>(0),
khr_df_transfer_e t = KHR_DF_TRANSFER_UNSPECIFIED,
khr_df_primaries_e p = KHR_DF_PRIMARIES_BT709,
khr_df_model_e m = KHR_DF_MODEL_RGBSDA,
khr_df_flags_e f = KHR_DF_FLAG_ALPHA_STRAIGHT)
: ImageSpec(w, h, d, Origin(), channelCount,
channelBitCount,channelLower, channelUpper,
dt, t, p, m, f) { }
ImageSpec(uint32_t w, uint32_t h, uint32_t d, Origin&& o,
uint32_t channelCount, uint32_t channelBitCount,
uint32_t channelLower, uint32_t channelUpper,
khr_df_sample_datatype_qualifiers_e dt
= static_cast<khr_df_sample_datatype_qualifiers_e>(0),
khr_df_transfer_e t = KHR_DF_TRANSFER_UNSPECIFIED,
khr_df_primaries_e p = KHR_DF_PRIMARIES_BT709,
khr_df_model_e m = KHR_DF_MODEL_RGBSDA,
khr_df_flags_e f = KHR_DF_FLAG_ALPHA_STRAIGHT)
: formatDesc(channelCount, channelBitCount,
channelLower, channelUpper,
dt, t, p, m, f),
imageWidth(w), imageHeight(h), imageDepth(d), imageOrigin(o) { }
ImageSpec(uint32_t w, uint32_t h, uint32_t d,
uint32_t channelCount, std::vector<uint32_t>& channelBitLengths,
std::vector<khr_df_model_channels_e>& channelTypes,
khr_df_sample_datatype_qualifiers_e dt
= static_cast<khr_df_sample_datatype_qualifiers_e>(0),
khr_df_transfer_e t = KHR_DF_TRANSFER_UNSPECIFIED,
khr_df_primaries_e p = KHR_DF_PRIMARIES_BT709,
khr_df_model_e m = KHR_DF_MODEL_RGBSDA,
khr_df_flags_e f = KHR_DF_FLAG_ALPHA_STRAIGHT)
: ImageSpec(w, h, d, Origin(), channelCount,
channelBitLengths, channelTypes, dt, t, p, m, f) { }
ImageSpec(uint32_t w, uint32_t h, uint32_t d, Origin&& o,
uint32_t channelCount, std::vector<uint32_t>& channelBitLengths,
std::vector<khr_df_model_channels_e>& channelTypes,
khr_df_sample_datatype_qualifiers_e dt
= static_cast<khr_df_sample_datatype_qualifiers_e>(0),
khr_df_transfer_e t = KHR_DF_TRANSFER_UNSPECIFIED,
khr_df_primaries_e p = KHR_DF_PRIMARIES_BT709,
khr_df_model_e m = KHR_DF_MODEL_RGBSDA,
khr_df_flags_e f = KHR_DF_FLAG_ALPHA_STRAIGHT)
: formatDesc(channelCount, channelBitLengths,
channelTypes, dt, t, p, m, f),
imageWidth(w), imageHeight(h), imageDepth(d), imageOrigin(o) { }
FormatDescriptor& format() { return formatDesc; }
const FormatDescriptor& format() const { return formatDesc; }
uint32_t width() const noexcept { return imageWidth; }
uint32_t height() const noexcept { return imageHeight; }
uint32_t depth() const noexcept { return imageDepth; }
const Origin& origin() const noexcept { return imageOrigin; }
void setWidth(uint32_t w) { imageWidth = w; }
void setHeight(uint32_t h) { imageHeight = h; }
void setDepth(uint32_t d) { imageDepth = d; }
void setOrigin(const Origin& o) { imageOrigin = o; }
size_t imagePixelCount() const noexcept {
return depth() * width() * height();
};
size_t imageChannelCount() const noexcept {
return imagePixelCount() * format().channelCount();
};
size_t imageByteCount() const noexcept {
return imagePixelCount() * format().pixelByteCount();
};
size_t scanlineByteCount() const noexcept {
return width() * format().pixelByteCount();
}
size_t scanlineChannelCount() const noexcept {
return width() * format().channelCount();
}
};
constexpr bool operator==(const ImageSpec::Origin& lhs, const ImageSpec::Origin& rhs) {
return lhs.x == rhs.x && lhs.y == rhs.y && lhs.z == rhs.z;
}
constexpr bool operator!=(const ImageSpec::Origin& lhs, const ImageSpec::Origin& rhs) {
return lhs.x != rhs.x || lhs.y != rhs.y || lhs.z != rhs.z;
}
[[nodiscard]] inline std::string toString(const ImageSpec::Origin& o) noexcept {
std::string str;
switch (o.x) {
case ImageSpec::Origin::eLeft: str = "left"; break;
case ImageSpec::Origin::eRight: str = "right"; break;
case ImageSpec::Origin::eUnspecified: str = "unspecified"; break;
default: assert(false && "Invalid origin.x");
}
str += ",";
switch (o.y) {
case ImageSpec::Origin::eTop: str += "top"; break;
case ImageSpec::Origin::eBottom: str += "bottom"; break;
case ImageSpec::Origin::eUnspecified: str += "unspecified"; break;
default: assert(false && "Invalid origin.y");
}
str += ",";
switch (o.z) {
case ImageSpec::Origin::eFront: str += "front"; break;
case ImageSpec::Origin::eBack: str += "back"; break;
case ImageSpec::Origin::eUnspecified: str += "unspecified"; break;
default: assert(false && "Invalid origin.z");
}
return str;
}
typedef std::function<void(const std::string&)> WarningCallbackFunction;
enum class ImageInputFormatType {
png_l,
png_la,
png_rgb,
png_rgba,
exr_uint,
exr_float,
npbm,
jpg,
};
inline const char* toString(ImageInputFormatType type) {
switch (type) {
case ImageInputFormatType::png_l:
return "png_l";
case ImageInputFormatType::png_la:
return "png_la";
case ImageInputFormatType::png_rgb:
return "png_rgb";
case ImageInputFormatType::png_rgba:
return "png_rgba";
case ImageInputFormatType::exr_uint:
return "exr_uint";
case ImageInputFormatType::exr_float:
return "exr_float";
case ImageInputFormatType::npbm:
return "npbm";
case ImageInputFormatType::jpg:
return "jpg";
}
assert(false && "Invalid ImageInputFormatType enum value");
return "<<invalid>>";
}
class ImageInput {
protected:
std::ifstream file;
std::unique_ptr<std::stringstream> buffer;
std::istream* isp = nullptr;
std::string name;
std::string _filename;
std::vector<uint16_t> nativeBuffer16;
std::vector<uint8_t> nativeBuffer8;
struct imageInfo {
ImageSpec spec;
ImageInputFormatType formatType;
size_t filepos;
imageInfo(ImageSpec&& is, ImageInputFormatType formatType, size_t pos = 0)
: spec(is), formatType(formatType), filepos(pos) { }
};
std::vector<imageInfo> images; ///<
uint32_t curSubimage = std::numeric_limits<uint32_t>::max();
uint32_t curMiplevel = std::numeric_limits<uint32_t>::max();
WarningCallbackFunction sendWarning = nullptr;
public:
using unique_ptr = std::unique_ptr<ImageInput>;
/// @brief Create an ImageInput subclass instance that is able to read the
/// given file and open it.
///
/// The `config`, if not nullptr, points to an ImageSpec giving hints,
/// requests, or special instructions. ImageInput implementations are
/// free to not respond to any such requests, so the default
/// implementation is just to ignore `config`.
///
/// `open()` will first try to make an ImageInput corresponding to
/// the format implied by the file extension (for example, `"foo.tif"`
/// will try the TIFF plugin), but if one is not found or if the
/// inferred one does not open the file, every known ImageInput type
/// will be tried until one is found that will open the file.
///
/// @param[in] filename The name of the file to open.
///
/// @param[in] config Optional pointer to an ImageSpec whose metadata
/// contains "configuration hints."
///
/// @returns
/// A `unique_ptr` that will close and free the ImageInput when
/// it exits scope or is reset. The pointer will be empty if the
/// required writer was not able to be created. If the open fails,
/// the `unique_ptr` will be empty. An error can be retrieved by
/// ImageInput::geterror().
static unique_ptr open(const std::string& filename,
const ImageSpec *config=nullptr,
WarningCallbackFunction wcb=nullptr);
//Filesystem::IOProxy* ioproxy = nullptr,
//string_view plugin_searchpath="");
virtual ~ImageInput() { close(); }
// TODO: is config necessary?
virtual void open (const std::string& filename, ImageSpec& newspec);
virtual void open (const std::string& filename, ImageSpec& newspec,
const ImageSpec& /*config*/) {
return open(filename, newspec);
}
virtual void close() {
if (isp == buffer.get()) {
buffer.reset();
} else if (file.is_open()) {
file.close();
}
isp = nullptr;
}
void connectCallback(WarningCallbackFunction wcb) {
sendWarning = wcb;
}
protected:
ImageInput(std::string&& name) : name(name) { }
virtual void open (std::ifstream&& ifs, ImageSpec& newspec) {
file = std::move(ifs);
isp = &file;
open(newspec);
}
virtual void open (std::ifstream& ifs, ImageSpec& newspec,
const ImageSpec& /*config*/) {
return open(ifs, newspec);
}
virtual void open (std::unique_ptr<std::stringstream>&& iss,
ImageSpec& newspec) {
buffer = std::move(iss);
isp = buffer.get();
open(newspec);
}
virtual void open (std::unique_ptr<std::stringstream>&& iss,
ImageSpec& newspec,
const ImageSpec& /*config*/) {
return open(std::move(iss), newspec);
}
virtual void open (std::istream& cin_, ImageSpec& newspec) {
isp = &cin_;
open(newspec);
}
virtual void open (std::istream& cin_, ImageSpec& newspec,
const ImageSpec& /*config*/) {
return open(cin_, newspec);
}
virtual void open(ImageSpec& newspec) = 0;
//virtual void open(ImageSpec& newspec, const ImageSpec& config) = 0;
std::ifstream& getFile() { return file; }
std::unique_ptr<std::stringstream>& getBuffer() { return buffer; }
void throwOnReadFailure();
void warning(const std::string& wmsg);
void fwarning(const std::string& wmsg);
private:
/** @internal
* @brief Open file or stringstream.
*
* This is solely to support the static open(). Subclasses should have no need
* to override.
*
* std::move is used because if the open is successful the class object needs to retain the
* fstream or stringstream that would otherwise disappear when open() exits. The catch
* clauses std::move the stream info back to the caller so it can keep searching for a plugin.
*/
void open(const std::string& filename, std::ifstream& ifs,
std::unique_ptr<std::stringstream>& bufferIn,
ImageSpec& newspec) {
_filename = filename; // Purely so warnings can include the file name.
if (ifs.is_open()) {
try {
open(std::move(ifs), newspec);
} catch (...) {
ifs = std::move(getFile());
ifs.clear();
ifs.seekg(0);
throw;
}
} else if (bufferIn.get() != nullptr) {
try {
open(std::move(bufferIn), newspec);
} catch (...) {
bufferIn = std::move(getBuffer());
bufferIn->clear();
bufferIn.get()->seekg(0);
throw;
}
} else {
try {
open(std::cin, newspec);
} catch (...) {
std::cin.clear();
std::cin.seekg(0);
throw;
}
}
}
public:
virtual const std::string& formatName(void) const { return name; }
virtual const std::string& filename(void) const { return _filename; }
// Return a reference to the ImageSpec of the current image.
// This default method assumes no subimages.
virtual const ImageSpec& spec (void) const {
return images[0].spec;
}
// Return the FormatType of the current image.
// This default method assumes no subimages.
virtual ImageInputFormatType formatType (void) const {
return images[0].formatType;
}
// Return a full copy of the ImageSpec of the designated subimage & level.
// If there is no such subimage and miplevel it returns an ImageSpec
// whose format returns true for isUnknown().
// This default method assumes no subimages.
virtual ImageSpec spec (uint32_t /*subimage*/, uint32_t /*miplevel=0*/) {
ImageSpec ret;
if (curSubimage < images.size()) {
ret = images[curSubimage].spec;
}
return ret;
}
// Return a copy of the ImageSpec but only the dimension and type fields.
// TODO: Determine if this is necessary.
virtual ImageSpec spec_dimensions (uint32_t /*subimage*/, uint32_t /*miplevel=0*/) {
return spec();
}
virtual uint32_t currentSubimage(void) const { return curSubimage; }
virtual uint32_t currentMiplevel(void) const { return curMiplevel; }
virtual uint32_t subimageCount(void) const { return 1; }
virtual uint32_t miplevelCount(void) const { return 1; }
virtual bool seekSubimage(uint32_t subimage, uint32_t miplevel = 0) {
// Default implementation assumes no support for subimages or
// mipmaps, so there is no work to do.
return subimage == currentSubimage() && miplevel == currentMiplevel();
}
/// Read an entire image into contiguous memory performing conversions to
/// @a requestFormat.
///
/// @TODO @a requestFormat allows callers to request almost unlimited
/// possible conversions compared to the original format. The current
/// plug-ins only provide a handful of conversions and those available
/// vary by plug-in. Plug-ins must throw an exception when an
/// unsupported conversion is requested. As a work in progress this is
/// okay but we need to rationalize all this such as
///
/// 1. a subset of all possible conversions supported by every plug-in
/// 2. conversion-specific exceptions so caller can tell what didn't work.
///
/// Commonly supported transformations are bit scaling and changing the
/// channel count, both adding and removing channels. See the derived
/// classes for the specific coversions supported.
///
virtual void readImage(void* buffer, size_t bufferByteCount,
uint32_t subimage = 0, uint32_t miplevel = 0,
const FormatDescriptor& requestFormat = FormatDescriptor());
/// @brief Read a scanline into contiguous memory performing conversions to
/// @a requestFormat.
///
/// Supported conversions in the default implementation are uint->uint for
/// 8- & 16-bit values.
///
/// @sa See readImage for information about handling of requestFormat.
virtual void readScanline(void* buffer, size_t bufferByteCount,
uint32_t y, uint32_t z,
uint32_t subimage, uint32_t miplevel,
const FormatDescriptor& requestFormat = FormatDescriptor());
/// Read a single scanline (all channels) of native data into contiguous
/// memory.
virtual void readNativeScanline(void* buffer, size_t bufferByteCount,
uint32_t y, uint32_t z = 0,
uint32_t subimage = 0, uint32_t miplevel = 0) = 0;
template<class Tr, class Tw>
inline static void
rescale(Tw* write, const Tr* read, size_t nvals, Tr max)
{
if (max) {
float multiplier = static_cast<float>(std::numeric_limits<Tw>::max()) / max;
for (size_t i = 0; i < nvals; i++) {
write[i] = static_cast<Tw>(roundf(read[i] * multiplier));
}
}
}
template<class Tr, class Tw>
inline static void
rescale(Tw* write, Tw maxw, const Tr* read, Tr maxr, size_t nvals)
{
if (maxr) {
float multiplier = static_cast<float>(maxw) / maxr;
for (size_t i = 0; i < nvals; i++) {
write[i] = static_cast<Tw>(roundf(read[i] * multiplier));
}
}
}
class different_format : public std::runtime_error {
public:
different_format() : std::runtime_error("") { }
};
class invalid_file : public std::runtime_error {
public:
invalid_file(std::string error)
: std::runtime_error("Invalid file: " + error) { }
};
class buffer_too_small : public std::runtime_error {
public:
buffer_too_small() : std::runtime_error("Image buffer too small.") { }
};
typedef ImageInput* (*Creator)();
};
class ImageOutput {
public:
/// unique_ptr to an ImageOutput.
using unique_ptr = std::unique_ptr<ImageOutput>;
static unique_ptr create (const std::string& name);
protected:
ImageOutput(std::string&& name) : name(name) { }
public:
virtual ~ImageOutput () { };
/// Return the name of the format implemented by this class.
virtual const std::string& formatName(void) const { return name; }
/// Query if feature is supported.
virtual int supports (std::string /*feature*/) const {
return false;
}
/// Modes passed to the `open()` call.
enum OpenMode { Create, AppendSubimage, AppendMIPLevel };
///
/// @param name The name of the image file to open.
/// @param newspec The ImageSpec describing the resolution, data
/// types, etc.
/// @param mode Specifies whether the purpose of the `open` is
/// to create/truncate the file (default: `Create`),
/// append another subimage (`AppendSubimage`), or
/// append another MIP level (`AppendMIPLevel`).
/// @returns `true` upon success, or `false` upon failure.
virtual void open (const std::string& name, const ImageSpec& newspec,
OpenMode mode=Create) = 0;
/// Return a reference to the image format specification of the current
/// subimage. Note that the contents of the spec are invalid before
/// `open()` or after `close()`.
const ImageSpec &spec (void) const { return imageSpec; }
/// Closes the currently open file associated with this ImageOutput and
/// frees any memory or resources associated with it.
virtual void close () = 0;
///
/// @param y/z The y & z coordinates of the scanline.
/// @param format A FormatDescriptor describing @a data.
/// @param data Pointer to the pixel data.
/// @param xstride The distance in bytes between successive
/// pixels in @a data (or `AutoStride`).
virtual void writeScanline (int y, int z, const FormatDescriptor& format,
const void *data, stride_t xstride=AutoStride);
///
/// @param format A FormatDescriptor describing @a data.
/// @param data Pointer to the pixel data.
/// @param xstride/ystride/zstride
/// The distance in bytes between successive pixels,
/// scanlines, and image planes (or `AutoStride`).
/// @param progress_callback/progress_callback_data
/// Optional progress callback.
/// @returns `true` upon success, or `false` upon failure.
virtual void writeImage (const FormatDescriptor& format, const void *data,
stride_t xstride=AutoStride,
stride_t ystride=AutoStride,
stride_t zstride=AutoStride,
ProgressCallback progress_callback=nullptr,
void *progress_callback_data=nullptr);
/// Specify a reduced-resolution ("thumbnail") version of the image.
/// Note that many image formats may require the thumbnail to be
/// specified prior to writing the pixels.
///
//virtual void setThumbnail(const Image& thumb) { return false; }
/// Read the pixels of the current subimage of @a in, and write it as the
/// next subimage of `*this`, in a way that is efficient and does not
/// alter pixel values, if at all possible. Both @a in and `this` must
/// be a properly-opened `ImageInput and `ImageOutput`, respectively,
/// and their current images must match in size and number of channels.
///
/// If a particular ImageOutput implementation does not supply a
/// `copy_image` method, it will inherit the default implementation,
/// which is to simply read scanlines from @a in and write them
/// to `*this`.
///
/// @param in A pointer to the open `ImageInput` to read from.
virtual void copyImage (ImageInput *in);
/// Call signature of a function that creates and returns an
/// `ImageOutput*`.
typedef ImageOutput* (*Creator)();
protected:
ImageSpec imageSpec; ///< format spec of the currently open image
std::string name;
};
namespace Imageio {
class string : public std::string {
public:
using std::string::string;
string tolower() {
std::transform(begin(), end(), begin(),
[](unsigned char c) {
return static_cast<char>(std::tolower(c));
});
return *this;
}
string (const std::string& s) : std::string(s) {}
};
typedef std::map<std::string, ImageInput::Creator> InputPluginMap;
extern InputPluginMap inputFormats;
typedef std::map<std::string, ImageOutput::Creator> OutputPluginMap;
extern OutputPluginMap outputFormats;
void catalogBuiltinPlugins();
}