// -*- 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 #include #include #include #include #include #include #include #include #include #include #include #include #include "formatdesc.h" using stride_t = int64_t; const stride_t AutoStride = std::numeric_limits::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(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(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(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(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& channelBitLengths, std::vector& channelTypes, khr_df_sample_datatype_qualifiers_e dt = static_cast(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& channelBitLengths, std::vector& channelTypes, khr_df_sample_datatype_qualifiers_e dt = static_cast(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 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 "<>"; } class ImageInput { protected: std::ifstream file; std::unique_ptr buffer; std::istream* isp = nullptr; std::string name; std::string _filename; std::vector nativeBuffer16; std::vector 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 images; ///< uint32_t curSubimage = std::numeric_limits::max(); uint32_t curMiplevel = std::numeric_limits::max(); WarningCallbackFunction sendWarning = nullptr; public: using unique_ptr = std::unique_ptr; /// @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&& iss, ImageSpec& newspec) { buffer = std::move(iss); isp = buffer.get(); open(newspec); } virtual void open (std::unique_ptr&& 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& 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& 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 inline static void rescale(Tw* write, const Tr* read, size_t nvals, Tr max) { if (max) { float multiplier = static_cast(std::numeric_limits::max()) / max; for (size_t i = 0; i < nvals; i++) { write[i] = static_cast(roundf(read[i] * multiplier)); } } } template inline static void rescale(Tw* write, Tw maxw, const Tr* read, Tr maxr, size_t nvals) { if (maxr) { float multiplier = static_cast(maxw) / maxr; for (size_t i = 0; i < nvals; i++) { write[i] = static_cast(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; 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(std::tolower(c)); }); return *this; } string (const std::string& s) : std::string(s) {} }; typedef std::map InputPluginMap; extern InputPluginMap inputFormats; typedef std::map OutputPluginMap; extern OutputPluginMap outputFormats; void catalogBuiltinPlugins(); }