From 1c17f9fd0a004ce8d465d1277026f9df97f564eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20M=C3=BCller?= Date: Tue, 7 Jul 2026 16:17:29 +0200 Subject: [PATCH] feat(imageloader): dedicated thread pool for blocking IO This change is more involved than it sounds: by being loaded in a separate threadpool, image data IO and decoding is now better parallelized. A few redundant copies are now also avoided. However, this change requires adapting pretty much all format-specific loaders (as well as the EXIF metadata loader) in order to avoid said unnecessary copies. The net result is faster bulk image loading (IO no longer blocks CPU-intensive work and vice versa) at the cost of a bit more RAM (all encoded images go into memory up front). --- dependencies/libexif | 2 +- include/tev/Common.h | 4 ++ include/tev/Image.h | 6 +- include/tev/ThreadPool.h | 5 ++ include/tev/imageio/BmpImageLoader.h | 6 +- include/tev/imageio/ClipboardImageLoader.h | 2 +- include/tev/imageio/DdsImageLoader.h | 2 +- include/tev/imageio/DicomImageLoader.h | 2 +- include/tev/imageio/EmptyImageLoader.h | 2 +- include/tev/imageio/ExrImageLoader.h | 2 +- include/tev/imageio/FitsImageLoader.h | 2 +- include/tev/imageio/HeifImageLoader.h | 2 +- include/tev/imageio/IcoImageLoader.h | 2 +- include/tev/imageio/ImageLoader.h | 2 +- include/tev/imageio/Jpeg2000ImageLoader.h | 2 +- include/tev/imageio/JpegTurboImageLoader.h | 2 +- include/tev/imageio/JxlImageLoader.h | 2 +- include/tev/imageio/JxrImageLoader.h | 2 +- include/tev/imageio/PfmImageLoader.h | 2 +- include/tev/imageio/PngImageLoader.h | 2 +- include/tev/imageio/QoiImageLoader.h | 2 +- include/tev/imageio/RawImageLoader.h | 2 +- include/tev/imageio/StbiImageLoader.h | 2 +- include/tev/imageio/TiffImageLoader.h | 2 +- include/tev/imageio/WebpImageLoader.h | 2 +- src/BackgroundImagesLoader.cpp | 3 +- src/Image.cpp | 27 +++++-- src/ImageViewer.cpp | 18 +++-- src/imageio/BmpImageLoader.cpp | 34 +++++---- src/imageio/ClipboardImageLoader.cpp | 9 ++- src/imageio/DdsImageLoader.cpp | 20 ++---- src/imageio/DicomImageLoader.cpp | 32 +++++---- src/imageio/EmptyImageLoader.cpp | 2 +- src/imageio/Exif.cpp | 11 +-- src/imageio/ExrImageLoader.cpp | 2 +- src/imageio/FitsImageLoader.cpp | 22 ++---- src/imageio/HeifImageLoader.cpp | 61 +++------------- src/imageio/IcoImageLoader.cpp | 22 ++---- src/imageio/Jpeg2000ImageLoader.cpp | 11 +-- src/imageio/JpegTurboImageLoader.cpp | 20 +----- src/imageio/JxlImageLoader.cpp | 25 ++----- src/imageio/JxrImageLoader.cpp | 21 ++---- src/imageio/PfmImageLoader.cpp | 2 +- src/imageio/PngImageLoader.cpp | 2 +- src/imageio/QoiImageLoader.cpp | 21 ++---- src/imageio/RawImageLoader.cpp | 83 +--------------------- src/imageio/StbiImageLoader.cpp | 30 +++----- src/imageio/TiffImageLoader.cpp | 27 ++----- src/imageio/WebpImageLoader.cpp | 15 ++-- src/main.cpp | 9 +-- 50 files changed, 190 insertions(+), 402 deletions(-) diff --git a/dependencies/libexif b/dependencies/libexif index f99967d0..6701b8b4 160000 --- a/dependencies/libexif +++ b/dependencies/libexif @@ -1 +1 @@ -Subproject commit f99967d09f1424170388cb2c0b65dcf67d6f5ac3 +Subproject commit 6701b8b49b593d069f3040fa5c9647d5ba47b4d0 diff --git a/include/tev/Common.h b/include/tev/Common.h index 94ffa8f2..9a6f18af 100644 --- a/include/tev/Common.h +++ b/include/tev/Common.h @@ -318,6 +318,10 @@ template struct visitor : Callable... { template concept trivially_copyable = std::is_trivially_copyable_v; +template std::span toSpan(const std::istringstream& stream) { + return std::span{reinterpret_cast(stream.view().data()), stream.view().size()}; +} + // Stricter version of from_chars that only returns true if the entire input was consumed and no error occurred. template bool fromChars(const char* begin, const char* end, T& value) { const auto result = std::from_chars(begin, end, value); diff --git a/include/tev/Image.h b/include/tev/Image.h index 3cbf6612..407855e8 100644 --- a/include/tev/Image.h +++ b/include/tev/Image.h @@ -32,11 +32,11 @@ #include #include #include -#include #include #include #include #include +#include #include #include @@ -344,10 +344,10 @@ Task orientToTopLeft(PixelBuffer& data, nanogui::Vector2i siz using ImageLoadResult = std::expected>, ImageLoadError>; Task tryLoadImage( - int imageId, fs::path path, std::istream& iStream, std::string_view channelSelector, const ImageLoaderSettings& settings, bool groupChannels + int imageId, fs::path path, std::istringstream& iStream, std::string_view channelSelector, const ImageLoaderSettings& settings, bool groupChannels ); Task tryLoadImage( - fs::path path, std::istream& iStream, std::string_view channelSelector, const ImageLoaderSettings& settings, bool groupChannels + fs::path path, std::istringstream& iStream, std::string_view channelSelector, const ImageLoaderSettings& settings, bool groupChannels ); Task tryLoadImage(int imageId, fs::path path, std::string_view channelSelector, const ImageLoaderSettings& settings, bool groupChannels); diff --git a/include/tev/ThreadPool.h b/include/tev/ThreadPool.h index 86189714..61b61b85 100644 --- a/include/tev/ThreadPool.h +++ b/include/tev/ThreadPool.h @@ -45,6 +45,11 @@ class ThreadPool { return pool; } + static ThreadPool& blockingIo() { + static ThreadPool pool; + return pool; + } + template auto enqueueTask(F&& f, int priority, bool stopToken = false) { using return_type = std::invoke_result_t; diff --git a/include/tev/imageio/BmpImageLoader.h b/include/tev/imageio/BmpImageLoader.h index a58ddfcc..cc8c8870 100644 --- a/include/tev/imageio/BmpImageLoader.h +++ b/include/tev/imageio/BmpImageLoader.h @@ -21,7 +21,7 @@ #include #include -#include +#include #include namespace tev { @@ -29,7 +29,7 @@ namespace tev { class BmpImageLoader final : public ImageLoader { public: Task> loadWithoutFileHeader( - std::istream& iStream, + std::istringstream& iStream, const fs::path& path, std::string_view channelSelector, const ImageLoaderSettings& settings, @@ -46,7 +46,7 @@ class BmpImageLoader final : public ImageLoader { ) const; Task> load( - std::istream& iStream, const fs::path& path, std::string_view channelSelector, const ImageLoaderSettings& settings, int priority + std::istringstream& iStream, const fs::path& path, std::string_view channelSelector, const ImageLoaderSettings& settings, int priority ) const override; std::string name() const override { return "BMP"; } diff --git a/include/tev/imageio/ClipboardImageLoader.h b/include/tev/imageio/ClipboardImageLoader.h index 1c35cc59..eafc4faf 100644 --- a/include/tev/imageio/ClipboardImageLoader.h +++ b/include/tev/imageio/ClipboardImageLoader.h @@ -28,7 +28,7 @@ namespace tev { class ClipboardImageLoader final : public ImageLoader { public: Task> load( - std::istream& iStream, const fs::path& path, std::string_view channelSelector, const ImageLoaderSettings& settings, int priority + std::istringstream& iStream, const fs::path& path, std::string_view channelSelector, const ImageLoaderSettings& settings, int priority ) const override; std::string name() const override { return "clipboard"; } diff --git a/include/tev/imageio/DdsImageLoader.h b/include/tev/imageio/DdsImageLoader.h index 4abfab16..46dabdc2 100644 --- a/include/tev/imageio/DdsImageLoader.h +++ b/include/tev/imageio/DdsImageLoader.h @@ -28,7 +28,7 @@ namespace tev { class DdsImageLoader final : public ImageLoader { public: Task> load( - std::istream& iStream, const fs::path& path, std::string_view channelSelector, const ImageLoaderSettings& settings, int priority + std::istringstream& iStream, const fs::path& path, std::string_view channelSelector, const ImageLoaderSettings& settings, int priority ) const override; std::string name() const override { return "DDS"; } diff --git a/include/tev/imageio/DicomImageLoader.h b/include/tev/imageio/DicomImageLoader.h index 24eb5c3e..34b18274 100644 --- a/include/tev/imageio/DicomImageLoader.h +++ b/include/tev/imageio/DicomImageLoader.h @@ -28,7 +28,7 @@ namespace tev { class DicomImageLoader final : public ImageLoader { public: Task> load( - std::istream& iStream, const fs::path& path, std::string_view channelSelector, const ImageLoaderSettings& settings, int priority + std::istringstream& iStream, const fs::path& path, std::string_view channelSelector, const ImageLoaderSettings& settings, int priority ) const override; std::string name() const override { return "DICOM"; } diff --git a/include/tev/imageio/EmptyImageLoader.h b/include/tev/imageio/EmptyImageLoader.h index a9061bdc..e307c7c1 100644 --- a/include/tev/imageio/EmptyImageLoader.h +++ b/include/tev/imageio/EmptyImageLoader.h @@ -28,7 +28,7 @@ namespace tev { class EmptyImageLoader final : public ImageLoader { public: Task> load( - std::istream& iStream, const fs::path& path, std::string_view channelSelector, const ImageLoaderSettings& settings, int priority + std::istringstream& iStream, const fs::path& path, std::string_view channelSelector, const ImageLoaderSettings& settings, int priority ) const override; std::string name() const override { return "IPC"; } diff --git a/include/tev/imageio/ExrImageLoader.h b/include/tev/imageio/ExrImageLoader.h index f7b7f77c..aad5a676 100644 --- a/include/tev/imageio/ExrImageLoader.h +++ b/include/tev/imageio/ExrImageLoader.h @@ -28,7 +28,7 @@ namespace tev { class ExrImageLoader final : public ImageLoader { public: Task> load( - std::istream& iStream, const fs::path& path, std::string_view channelSelector, const ImageLoaderSettings& settings, int priority + std::istringstream& iStream, const fs::path& path, std::string_view channelSelector, const ImageLoaderSettings& settings, int priority ) const override; std::string name() const override { return "OpenEXR"; } diff --git a/include/tev/imageio/FitsImageLoader.h b/include/tev/imageio/FitsImageLoader.h index e548f166..73b72e71 100644 --- a/include/tev/imageio/FitsImageLoader.h +++ b/include/tev/imageio/FitsImageLoader.h @@ -28,7 +28,7 @@ namespace tev { class FitsImageLoader final : public ImageLoader { public: Task> load( - std::istream& iStream, const fs::path& path, std::string_view channelSelector, const ImageLoaderSettings& settings, int priority + std::istringstream& iStream, const fs::path& path, std::string_view channelSelector, const ImageLoaderSettings& settings, int priority ) const override; std::string name() const override { return "FITS"; } diff --git a/include/tev/imageio/HeifImageLoader.h b/include/tev/imageio/HeifImageLoader.h index 6ae407ef..adc8a39a 100644 --- a/include/tev/imageio/HeifImageLoader.h +++ b/include/tev/imageio/HeifImageLoader.h @@ -28,7 +28,7 @@ namespace tev { class HeifImageLoader final : public ImageLoader { public: Task> load( - std::istream& iStream, const fs::path& path, std::string_view channelSelector, const ImageLoaderSettings& settings, int priority + std::istringstream& iStream, const fs::path& path, std::string_view channelSelector, const ImageLoaderSettings& settings, int priority ) const override; std::string name() const override { return "HEIF"; } diff --git a/include/tev/imageio/IcoImageLoader.h b/include/tev/imageio/IcoImageLoader.h index 88cdf314..37081e25 100644 --- a/include/tev/imageio/IcoImageLoader.h +++ b/include/tev/imageio/IcoImageLoader.h @@ -28,7 +28,7 @@ namespace tev { class IcoImageLoader final : public ImageLoader { public: Task> load( - std::istream& iStream, const fs::path& path, std::string_view channelSelector, const ImageLoaderSettings& settings, int priority + std::istringstream& iStream, const fs::path& path, std::string_view channelSelector, const ImageLoaderSettings& settings, int priority ) const override; std::string name() const override { return "ICO"; } diff --git a/include/tev/imageio/ImageLoader.h b/include/tev/imageio/ImageLoader.h index 6865a132..705cc710 100644 --- a/include/tev/imageio/ImageLoader.h +++ b/include/tev/imageio/ImageLoader.h @@ -217,7 +217,7 @@ class ImageLoader { virtual ~ImageLoader() {} virtual Task> load( - std::istream& iStream, const fs::path& path, std::string_view channelSelector, const ImageLoaderSettings& settings, int priority + std::istringstream& iStream, const fs::path& path, std::string_view channelSelector, const ImageLoaderSettings& settings, int priority ) const = 0; virtual std::string name() const = 0; diff --git a/include/tev/imageio/Jpeg2000ImageLoader.h b/include/tev/imageio/Jpeg2000ImageLoader.h index 856f3a83..8734ff31 100644 --- a/include/tev/imageio/Jpeg2000ImageLoader.h +++ b/include/tev/imageio/Jpeg2000ImageLoader.h @@ -28,7 +28,7 @@ namespace tev { class Jpeg2000ImageLoader final : public ImageLoader { public: Task> load( - std::istream& iStream, const fs::path& path, std::string_view channelSelector, const ImageLoaderSettings& settings, int priority + std::istringstream& iStream, const fs::path& path, std::string_view channelSelector, const ImageLoaderSettings& settings, int priority ) const override; Task> load( diff --git a/include/tev/imageio/JpegTurboImageLoader.h b/include/tev/imageio/JpegTurboImageLoader.h index 30299739..aa7fd70b 100644 --- a/include/tev/imageio/JpegTurboImageLoader.h +++ b/include/tev/imageio/JpegTurboImageLoader.h @@ -28,7 +28,7 @@ namespace tev { class JpegTurboImageLoader final : public ImageLoader { public: Task> load( - std::istream& iStream, const fs::path& path, std::string_view channelSelector, const ImageLoaderSettings& settings, int priority + std::istringstream& iStream, const fs::path& path, std::string_view channelSelector, const ImageLoaderSettings& settings, int priority ) const override; std::string name() const override { return "JPEG Turbo"; } diff --git a/include/tev/imageio/JxlImageLoader.h b/include/tev/imageio/JxlImageLoader.h index dff8f626..8272d066 100644 --- a/include/tev/imageio/JxlImageLoader.h +++ b/include/tev/imageio/JxlImageLoader.h @@ -30,7 +30,7 @@ namespace tev { class JxlImageLoader final : public ImageLoader { public: Task> load( - std::istream& iStream, const fs::path& path, std::string_view channelSelector, const ImageLoaderSettings& settings, int priority + std::istringstream& iStream, const fs::path& path, std::string_view channelSelector, const ImageLoaderSettings& settings, int priority ) const override; Task> load( diff --git a/include/tev/imageio/JxrImageLoader.h b/include/tev/imageio/JxrImageLoader.h index 056f3ba4..7db77525 100644 --- a/include/tev/imageio/JxrImageLoader.h +++ b/include/tev/imageio/JxrImageLoader.h @@ -28,7 +28,7 @@ namespace tev { class JxrImageLoader final : public ImageLoader { public: Task> load( - std::istream& iStream, const fs::path& path, std::string_view channelSelector, const ImageLoaderSettings& settings, int priority + std::istringstream& iStream, const fs::path& path, std::string_view channelSelector, const ImageLoaderSettings& settings, int priority ) const override; std::string name() const override { return "JXR"; } diff --git a/include/tev/imageio/PfmImageLoader.h b/include/tev/imageio/PfmImageLoader.h index 97f8860f..725e1030 100644 --- a/include/tev/imageio/PfmImageLoader.h +++ b/include/tev/imageio/PfmImageLoader.h @@ -28,7 +28,7 @@ namespace tev { class PfmImageLoader final : public ImageLoader { public: Task> load( - std::istream& iStream, const fs::path& path, std::string_view channelSelector, const ImageLoaderSettings& settings, int priority + std::istringstream& iStream, const fs::path& path, std::string_view channelSelector, const ImageLoaderSettings& settings, int priority ) const override; std::string name() const override { return "PFM/PAM"; } diff --git a/include/tev/imageio/PngImageLoader.h b/include/tev/imageio/PngImageLoader.h index 1460efbc..0f1046cd 100644 --- a/include/tev/imageio/PngImageLoader.h +++ b/include/tev/imageio/PngImageLoader.h @@ -28,7 +28,7 @@ namespace tev { class PngImageLoader final : public ImageLoader { public: Task> load( - std::istream& iStream, const fs::path& path, std::string_view channelSelector, const ImageLoaderSettings& settings, int priority + std::istringstream& iStream, const fs::path& path, std::string_view channelSelector, const ImageLoaderSettings& settings, int priority ) const override; std::string name() const override { return "PNG"; } diff --git a/include/tev/imageio/QoiImageLoader.h b/include/tev/imageio/QoiImageLoader.h index 759bc965..33eede4c 100644 --- a/include/tev/imageio/QoiImageLoader.h +++ b/include/tev/imageio/QoiImageLoader.h @@ -28,7 +28,7 @@ namespace tev { class QoiImageLoader final : public ImageLoader { public: Task> load( - std::istream& iStream, const fs::path& path, std::string_view channelSelector, const ImageLoaderSettings& settings, int priority + std::istringstream& iStream, const fs::path& path, std::string_view channelSelector, const ImageLoaderSettings& settings, int priority ) const override; std::string name() const override { return "QOI"; } diff --git a/include/tev/imageio/RawImageLoader.h b/include/tev/imageio/RawImageLoader.h index 7531ac62..759d5202 100644 --- a/include/tev/imageio/RawImageLoader.h +++ b/include/tev/imageio/RawImageLoader.h @@ -28,7 +28,7 @@ namespace tev { class RawImageLoader final : public ImageLoader { public: Task> load( - std::istream& iStream, const fs::path& path, std::string_view channelSelector, const ImageLoaderSettings& settings, int priority + std::istringstream& iStream, const fs::path& path, std::string_view channelSelector, const ImageLoaderSettings& settings, int priority ) const override; std::string name() const override { return "RAW"; } diff --git a/include/tev/imageio/StbiImageLoader.h b/include/tev/imageio/StbiImageLoader.h index e75aafb9..62cf4899 100644 --- a/include/tev/imageio/StbiImageLoader.h +++ b/include/tev/imageio/StbiImageLoader.h @@ -28,7 +28,7 @@ namespace tev { class StbiImageLoader final : public ImageLoader { public: Task> load( - std::istream& iStream, const fs::path& path, std::string_view channelSelector, const ImageLoaderSettings& settings, int priority + std::istringstream& iStream, const fs::path& path, std::string_view channelSelector, const ImageLoaderSettings& settings, int priority ) const override; std::string name() const override { return "STBI"; } diff --git a/include/tev/imageio/TiffImageLoader.h b/include/tev/imageio/TiffImageLoader.h index d08b49ce..a5cfb81c 100644 --- a/include/tev/imageio/TiffImageLoader.h +++ b/include/tev/imageio/TiffImageLoader.h @@ -28,7 +28,7 @@ namespace tev { class TiffImageLoader final : public ImageLoader { public: Task> load( - std::istream& iStream, const fs::path& path, std::string_view channelSelector, const ImageLoaderSettings& settings, int priority + std::istringstream& iStream, const fs::path& path, std::string_view channelSelector, const ImageLoaderSettings& settings, int priority ) const override; std::string name() const override { return "TIFF"; } diff --git a/include/tev/imageio/WebpImageLoader.h b/include/tev/imageio/WebpImageLoader.h index ddceae97..f3c5f5f2 100644 --- a/include/tev/imageio/WebpImageLoader.h +++ b/include/tev/imageio/WebpImageLoader.h @@ -28,7 +28,7 @@ namespace tev { class WebpImageLoader final : public ImageLoader { public: Task> load( - std::istream& iStream, const fs::path& path, std::string_view channelSelector, const ImageLoaderSettings& settings, int priority + std::istringstream& iStream, const fs::path& path, std::string_view channelSelector, const ImageLoaderSettings& settings, int priority ) const override; std::string name() const override { return "WEBP"; } diff --git a/src/BackgroundImagesLoader.cpp b/src/BackgroundImagesLoader.cpp index fa009a00..822b922c 100644 --- a/src/BackgroundImagesLoader.cpp +++ b/src/BackgroundImagesLoader.cpp @@ -73,8 +73,9 @@ void BackgroundImagesLoader::enqueue(const fs::path& path, string_view channelSe invokeTaskDetached([loadId, path, cs = string{channelSelector}, shallSelect, toReplace, this]() -> Task { const int taskPriority = -Image::drawId(); - co_await ThreadPool::global().enqueueCoroutine(taskPriority); + co_await ThreadPool::blockingIo().enqueueCoroutine(taskPriority); const auto images = co_await tryLoadImage(taskPriority, path, cs, mImageLoaderSettings, mGroupChannels); + co_await ThreadPool::global().enqueueCoroutine(taskPriority); { const lock_guard lock{mPendingLoadedImagesMutex}; diff --git a/src/Image.cpp b/src/Image.cpp index 5e2ca5cf..a2938f3f 100644 --- a/src/Image.cpp +++ b/src/Image.cpp @@ -1473,7 +1473,7 @@ Task orientToTopLeft(PixelBuffer& data, nanogui::Vector2i siz } Task tryLoadImage( - int taskPriority, fs::path path, istream& iStream, string_view channelSelector, const ImageLoaderSettings& settings, bool groupChannels + int taskPriority, fs::path path, istringstream& iStream, string_view channelSelector, const ImageLoaderSettings& settings, bool groupChannels ) { // No need to keep loading images if tev is already shutting down again. if (shuttingDown()) { @@ -1550,7 +1550,11 @@ Task tryLoadImage( vector> images = co_await awaitAll(imageTasks) | toVector; if (images.empty()) { - throw ImageLoadError{fmt::format("No parts/channels match channel selector :{}", channelSelector)}; + if (channelSelector.empty()) { + throw ImageLoadError{fmt::format("No parts/channels found in image {}.", path)}; + } else { + throw ImageLoadError{fmt::format("No parts/channels match channel selector :{}", channelSelector)}; + } } const auto end = chrono::system_clock::now(); @@ -1568,7 +1572,7 @@ Task tryLoadImage( } Task - tryLoadImage(fs::path path, istream& iStream, string_view channelSelector, const ImageLoaderSettings& settings, bool groupChannels) { + tryLoadImage(fs::path path, istringstream& iStream, string_view channelSelector, const ImageLoaderSettings& settings, bool groupChannels) { co_return co_await tryLoadImage(-Image::drawId(), path, iStream, channelSelector, settings, groupChannels); } @@ -1581,8 +1585,21 @@ Task // sure. } - ifstream fileStream{path, ios_base::binary}; - co_return co_await tryLoadImage(taskPriority, path, fileStream, channelSelector, settings, groupChannels); + istringstream iStream; + + { + co_await ThreadPool::blockingIo().enqueueCoroutine(taskPriority); + + if (!shuttingDown()) { + ostringstream oStream; + ifstream fileStream{path, ios_base::binary}; + oStream << fileStream.rdbuf(); + iStream = istringstream{std::move(oStream).str()}; // C++20 rvalue .str() overload to avoid a copy of the string + } + } + + co_await ThreadPool::global().enqueueCoroutine(taskPriority); + co_return co_await tryLoadImage(taskPriority, path, iStream, channelSelector, settings, groupChannels); } Task tryLoadImage(fs::path path, string_view channelSelector, const ImageLoaderSettings& settings, bool groupChannels) { diff --git a/src/ImageViewer.cpp b/src/ImageViewer.cpp index f0b21d29..10e1f03f 100644 --- a/src/ImageViewer.cpp +++ b/src/ImageViewer.cpp @@ -2591,7 +2591,7 @@ void ImageViewer::copyImageNameToClipboard() const { } void ImageViewer::pasteImagesFromClipboard() { - stringstream imageStream; + istringstream imageStream; if (glfwGetPlatform() == GLFW_PLATFORM_WAYLAND) { const auto data = waylandGetClipboardPngImage(); if (data.empty()) { @@ -2599,7 +2599,7 @@ void ImageViewer::pasteImagesFromClipboard() { } // TODO: use spanstream once it is available in C++23 to avoid the copy - imageStream = stringstream{string{data}, ios::in}; + imageStream = istringstream{string{data}}; } else if (glfwGetPlatform() == GLFW_PLATFORM_X11) { clip::lock l; if (!l.locked()) { @@ -2615,16 +2615,18 @@ void ImageViewer::pasteImagesFromClipboard() { string data(len, '\0'); l.get_data(f, data.data(), len); - imageStream = stringstream{std::move(data), ios::in}; + imageStream = istringstream{std::move(data)}; } else { clip::image clipImage; if (!clip::get_image(clipImage)) { throw runtime_error{"No image data found in clipboard."}; } - imageStream << "clip"; - imageStream.write(reinterpret_cast(&clipImage.spec()), sizeof(clip::image_spec)); - imageStream.write(clipImage.data(), clipImage.spec().bytes_per_row * clipImage.spec().height); + ostringstream oss; + oss << "clip"; + oss.write(reinterpret_cast(&clipImage.spec()), sizeof(clip::image_spec)); + oss.write(clipImage.data(), clipImage.spec().bytes_per_row * clipImage.spec().height); + imageStream = istringstream{std::move(oss).str()}; } tlog::info("Loading image from clipboard..."); @@ -2646,7 +2648,9 @@ void ImageViewer::showErrorDialog(string_view message, bool logToConsole) { tlog::error(message); } - new ErrorDialog(this, MessageDialog::Type::Warning, "Error", message, "OK", "Cancel", false, (int)(m_size.x() * 0.75f), (int)(m_size.y() * 0.75f)); + new ErrorDialog( + this, MessageDialog::Type::Warning, "Error", message, "OK", "Cancel", false, (int)(m_size.x() * 0.75f), (int)(m_size.y() * 0.75f) + ); } chroma_t ImageViewer::inspectionChroma() const { diff --git a/src/imageio/BmpImageLoader.cpp b/src/imageio/BmpImageLoader.cpp index 1c2a01d6..62f4641f 100644 --- a/src/imageio/BmpImageLoader.cpp +++ b/src/imageio/BmpImageLoader.cpp @@ -785,7 +785,7 @@ HeapArray decode_huffman1d(const uint8_t* src, size_t src_size, int wid } Task> BmpImageLoader::load( - istream& iStream, const fs::path& path, string_view channelSelector, const ImageLoaderSettings& settings, int priority + istringstream& iStream, const fs::path& path, string_view channelSelector, const ImageLoaderSettings& settings, int priority ) const { const bool reverseEndianness = endian::native == endian::big; @@ -888,7 +888,7 @@ Task> BmpImageLoader::load( } Task> BmpImageLoader::loadWithoutFileHeader( - istream& iStream, + istringstream& iStream, const fs::path& path, string_view channelSelector, const ImageLoaderSettings& settings, @@ -1356,7 +1356,9 @@ Task> BmpImageLoader::loadWithoutFileHeader( iStream.read((char*)palette.data(), palette.size()); if (!iStream) { - throw ImageLoadError{fmt::format("Failed to read BMP palette with {} entries and entry size {}", numPaletteEntries, paletteEntrySize)}; + throw ImageLoadError{ + fmt::format("Failed to read BMP palette with {} entries and entry size {}", numPaletteEntries, paletteEntrySize) + }; } if (palette.size() == 0) { @@ -1421,20 +1423,28 @@ Task> BmpImageLoader::loadWithoutFileHeader( )}; } - HeapArray pixelData(bytesToRead); - iStream.read((char*)pixelData.data(), pixelData.size()); - if (!iStream) { - throw ImageLoadError{fmt::format("Failed to read BMP pixel data of size {}", pixelData.size())}; + auto pixelData = toSpan(iStream).subspan(iStream.tellg()); + if (pixelData.size() < bytesToRead) { + throw ImageLoadError{fmt::format("Failed to read BMP pixel data of size {}", bytesToRead)}; } + // Required by IcoImageLoader if an AND-mask is stored right after the BMP pixel data. Ico files don't store metadata about where the + // BMP data ends and where the AND-mask begins, so has to rely on the BMP loader advancing iStream. + iStream.seekg(bytesToRead, ios_base::cur); + + HeapArray pixelDataCopy; switch (compression) { - case ECompression::Rle8: pixelData = decode_rle8(pixelData.data(), pixelData.size(), dib.width, dib.height); break; - case ECompression::Rle4: pixelData = decode_rle4(pixelData.data(), pixelData.size(), dib.width, dib.height); break; - case ECompression::Rle24: pixelData = decode_rle24(pixelData.data(), pixelData.size(), dib.width, dib.height); break; - case ECompression::Huffman: pixelData = decode_huffman1d(pixelData.data(), pixelData.size(), dib.width, dib.height); break; + case ECompression::Rle8: pixelDataCopy = decode_rle8(pixelData.data(), pixelData.size(), dib.width, dib.height); break; + case ECompression::Rle4: pixelDataCopy = decode_rle4(pixelData.data(), pixelData.size(), dib.width, dib.height); break; + case ECompression::Rle24: pixelDataCopy = decode_rle24(pixelData.data(), pixelData.size(), dib.width, dib.height); break; + case ECompression::Huffman: pixelDataCopy = decode_huffman1d(pixelData.data(), pixelData.size(), dib.width, dib.height); break; default: break; } + if (pixelDataCopy) { + pixelData = pixelDataCopy; + } + if (pixelData.size() < pixelDataSize) { throw ImageLoadError{fmt::format("Decoded BMP pixel data size {} is smaller than expected {}", pixelData.size(), pixelDataSize)}; } @@ -1479,7 +1489,7 @@ Task> BmpImageLoader::loadWithoutFileHeader( const size_t pixelBit = (size_t)x * dib.bitsPerPixel; const size_t pixelByte = pixelBit / 8; - uint8_t* pixelPtr = pixelData.data() + rowStart + pixelByte; + const uint8_t* pixelPtr = pixelData.data() + rowStart + pixelByte; if (hasPalette) { const size_t byte = *pixelPtr; diff --git a/src/imageio/ClipboardImageLoader.cpp b/src/imageio/ClipboardImageLoader.cpp index 93bd997e..a9e048ef 100644 --- a/src/imageio/ClipboardImageLoader.cpp +++ b/src/imageio/ClipboardImageLoader.cpp @@ -28,7 +28,7 @@ using namespace std; namespace tev { Task> - ClipboardImageLoader::load(istream& iStream, const fs::path&, string_view, const ImageLoaderSettings&, int priority) const { + ClipboardImageLoader::load(istringstream& iStream, const fs::path&, string_view, const ImageLoaderSettings&, int priority) const { char magic[4]; clip::image_spec spec; @@ -72,10 +72,9 @@ Task> const auto outView = MultiChannelView{resultData.channels}; - HeapArray data(numBytes); - iStream.read(data.data(), numBytes); - if ((size_t)iStream.gcount() < numBytes) { - throw ImageLoadError{fmt::format("Insufficient bytes to read image data ({} vs {}).", iStream.gcount(), numBytes)}; + const auto data = toSpan(iStream).subspan(iStream.tellg()); + if (data.size() < numBytes) { + throw ImageLoadError{fmt::format("Insufficient bytes to read image data ({} vs {}).", data.size(), numBytes)}; } const size_t shifts[4] = { diff --git a/src/imageio/DdsImageLoader.cpp b/src/imageio/DdsImageLoader.cpp index bb624283..66947528 100644 --- a/src/imageio/DdsImageLoader.cpp +++ b/src/imageio/DdsImageLoader.cpp @@ -152,24 +152,12 @@ static size_t getDxgiChannelCount(DXGI_FORMAT fmt) { } } -Task> DdsImageLoader::load(istream& iStream, const fs::path&, string_view, const ImageLoaderSettings&, int priority) const { - iStream.seekg(0, iStream.end); - const size_t dataSize = iStream.tellg(); - if (dataSize < 4) { - throw FormatNotSupported{"File is too small."}; - } - - iStream.clear(); - iStream.seekg(0); - - HeapArray data{dataSize}; - iStream.read(data.data(), 4); - if (data[0] != 'D' || data[1] != 'D' || data[2] != 'S' || data[3] != ' ') { +Task> DdsImageLoader::load(istringstream& iStream, const fs::path&, string_view, const ImageLoaderSettings&, int priority) const { + const auto data = toSpan(iStream).subspan(iStream.tellg()); + if (data.size() < 4 || data[0] != 'D' || data[1] != 'D' || data[2] != 'S' || data[3] != ' ') { throw FormatNotSupported{"File is not a DDS file."}; } - iStream.read(data.data() + 4, dataSize - 4); - // COM must be initialized on the thread executing the following DirectX calls. Thus: when editing this file *make sure* that no // co_await calls are made before the last DirectX call! Note that it is not a problem that CoInitializeEx will potentially get called // multiple times across different threads, or even multiple times by the same thread pool thread. Both situations are explicitly @@ -188,7 +176,7 @@ Task> DdsImageLoader::load(istream& iStream, const fs::path&, DirectX::ScratchImage scratchImage; DirectX::TexMetadata metadata; - if (DirectX::LoadFromDDSMemory(data.data(), dataSize, DirectX::DDS_FLAGS_NONE, &metadata, scratchImage) != S_OK) { + if (DirectX::LoadFromDDSMemory(data.data(), data.size(), DirectX::DDS_FLAGS_NONE, &metadata, scratchImage) != S_OK) { throw ImageLoadError{"Failed to read DDS file."}; } diff --git a/src/imageio/DicomImageLoader.cpp b/src/imageio/DicomImageLoader.cpp index f7107730..0f22b84b 100644 --- a/src/imageio/DicomImageLoader.cpp +++ b/src/imageio/DicomImageLoader.cpp @@ -723,7 +723,7 @@ Task> readDicomImage(const gdcm::ImageReader& reader, con co_return resultData; } -Task> readDicomImage(istream& iStream, const ImageLoaderSettings& settings, int priority) { +Task> readDicomImage(istringstream& iStream, const ImageLoaderSettings& settings, int priority) { gdcm::ImageReader reader; reader.SetStream(iStream); if (!reader.Read()) { @@ -789,18 +789,25 @@ Task> readDicomDir(const gdcm::File& dirFile, const fs::p const auto loadFile = [&](size_t fi) -> Task> { const fs::path& refPath = referencedFiles[fi]; - ifstream refStream{refPath, ios::binary}; - if (!refStream) { + istringstream data; + + { + co_await ThreadPool::blockingIo().enqueueCoroutine(priority); + + ifstream refStream{refPath, ios::binary}; + ostringstream oss; + oss << refStream.rdbuf(); + data = istringstream{std::move(oss).str()}; + } + + co_await ThreadPool::global().enqueueCoroutine(priority); + + if (!data) { tlog::warning("Failed to open DICOMDIR-referenced file: {}", refPath); co_return {}; } - // Copy data into a string stream because GDCM does a lot of back and forth seeking and reading. On Linux, GDCM is actually quite - // performant when applied directly to a file stream, but on macOS, the copy is strongly needed. - stringstream data; - data << refStream.rdbuf(); - try { co_return co_await readDicomImage(data, settings, priority); } catch (const ImageLoadError& e) { @@ -876,7 +883,7 @@ void generatePartNames(vector& dicomData) { } Task> - DicomImageLoader::load(istream& iStream, const fs::path& path, string_view, const ImageLoaderSettings& settings, int priority) const { + DicomImageLoader::load(istringstream& iStream, const fs::path& path, string_view, const ImageLoaderSettings& settings, int priority) const { char header[132] = {0}; iStream.read(reinterpret_cast(header), sizeof(header)); const bool hasMagic = header[128] == 'D' && header[129] == 'I' && header[130] == 'C' && header[131] == 'M'; @@ -893,16 +900,11 @@ Task> iStream.clear(); iStream.seekg(0); - // Copy data into a string stream because GDCM does a lot of back and forth seeking and reading. On Linux, GDCM is actually quite - // performant when applied directly to a file stream, but on macOS, the copy is strongly needed. - stringstream data; - data << iStream.rdbuf(); - vector result; try { gdcm::ImageReader reader; - reader.SetStream(data); + reader.SetStream(iStream); if (reader.Read()) { result = co_await readDicomImage(reader, settings, priority); } else { diff --git a/src/imageio/EmptyImageLoader.cpp b/src/imageio/EmptyImageLoader.cpp index 1ccde864..c378a397 100644 --- a/src/imageio/EmptyImageLoader.cpp +++ b/src/imageio/EmptyImageLoader.cpp @@ -25,7 +25,7 @@ using namespace std; namespace tev { -Task> EmptyImageLoader::load(istream& iStream, const fs::path&, string_view, const ImageLoaderSettings&, int priority) const { +Task> EmptyImageLoader::load(istringstream& iStream, const fs::path&, string_view, const ImageLoaderSettings&, int priority) const { char magic[6]; iStream.read(magic, 6); string magicString(magic, 6); diff --git a/src/imageio/Exif.cpp b/src/imageio/Exif.cpp index 95e21b3f..bf02c704 100644 --- a/src/imageio/Exif.cpp +++ b/src/imageio/Exif.cpp @@ -82,15 +82,6 @@ Exif::Exif() { Exif::Exif(span exifData, bool autoPrependFourcc) : Exif() { auto guard = ScopeGuard{[this]() { reset(); }}; - // If data doesn't already start with fourcc, prepend - HeapArray newExifData; - if (autoPrependFourcc && (exifData.size() < 6 || memcmp(exifData.data(), Exif::FOURCC.data(), 6) != 0)) { - newExifData = HeapArray{exifData.size() + FOURCC.size()}; - memcpy(newExifData.data(), FOURCC.data(), FOURCC.size()); - memcpy(newExifData.data() + FOURCC.size(), exifData.data(), exifData.size()); - exifData = newExifData; - } - if (exifData.size() > numeric_limits::max()) { throw invalid_argument{"EXIF data size exceeds maximum supported size."}; } @@ -221,6 +212,8 @@ AttributeNode Exif::toAttributes() const { return result; } + tlog::info("FOUND MAKERNOTE"); + AttributeNode& mdNode = result.children.emplace_back(); mdNode.name = "MakerNote"; mdNode.type = "MakerNote"; diff --git a/src/imageio/ExrImageLoader.cpp b/src/imageio/ExrImageLoader.cpp index 1610c5a7..07404e80 100644 --- a/src/imageio/ExrImageLoader.cpp +++ b/src/imageio/ExrImageLoader.cpp @@ -456,7 +456,7 @@ class RawChannel { }; Task> - ExrImageLoader::load(istream& iStream, const fs::path& path, string_view channelSelector, const ImageLoaderSettings&, int priority) const { + ExrImageLoader::load(istringstream& iStream, const fs::path& path, string_view channelSelector, const ImageLoaderSettings&, int priority) const { try { if (!isExrImage(iStream)) { throw FormatNotSupported{"File is not an EXR image."}; diff --git a/src/imageio/FitsImageLoader.cpp b/src/imageio/FitsImageLoader.cpp index d233a7f1..9fa6424e 100644 --- a/src/imageio/FitsImageLoader.cpp +++ b/src/imageio/FitsImageLoader.cpp @@ -311,28 +311,16 @@ Task> decodeImageHdu(fitsfile* fp, int hduIndex, int priorit } // anonymous namespace Task> - FitsImageLoader::load(istream& iStream, const fs::path& path, string_view, const ImageLoaderSettings&, int priority) const { - char magic[9] = {}; - iStream.read(magic, sizeof(magic)); - if (!iStream || string_view{magic, sizeof(magic)} != "SIMPLE =") { + FitsImageLoader::load(istringstream& iStream, const fs::path& path, string_view, const ImageLoaderSettings&, int priority) const { + const auto data = toSpan(iStream).subspan(iStream.tellg()); + if (data.size() < 9 || string_view{data.data(), 9} != "SIMPLE =") { throw FormatNotSupported{"File is not a FITS image."}; } - iStream.clear(); - iStream.seekg(0, iStream.end); - const auto dataSize = iStream.tellg(); - iStream.seekg(0, iStream.beg); - - HeapArray data(dataSize); - iStream.read(data.data(), dataSize); - if (!iStream) { - throw ImageLoadError{fmt::format("Failed to read FITS data of size {}", (size_t)dataSize)}; - } - fitsfile* fp = nullptr; int status = 0; - void* bufferPtr = data.data(); - size_t bufferSize = (size_t)dataSize; + void* bufferPtr = (void*)data.data(); // casting away constness, trusting cfitsio not to modify the buffer + size_t bufferSize = data.size(); const string memName = fmt::format("{}.mem", path.filename().string()); fits_open_memfile(&fp, memName.c_str(), READONLY, &bufferPtr, &bufferSize, 0, nullptr, &status); throwOnCfitsioError(status, "open_memfile"); diff --git a/src/imageio/HeifImageLoader.cpp b/src/imageio/HeifImageLoader.cpp index bf4ba0f6..e5dfdcf6 100644 --- a/src/imageio/HeifImageLoader.cpp +++ b/src/imageio/HeifImageLoader.cpp @@ -41,21 +41,19 @@ using namespace std; namespace tev { Task> HeifImageLoader::load( - istream& iStream, const fs::path&, string_view channelSelector, const ImageLoaderSettings& settings, int priority + istringstream& iStream, const fs::path&, string_view channelSelector, const ImageLoaderSettings& settings, int priority ) const { // libheif's spec says it needs the first 12 bytes to determine whether the image can be read. - uint8_t header[12]; - iStream.read((char*)header, 12); - - if (!iStream || iStream.gcount() != 12) { + const auto data = toSpan(iStream).subspan(iStream.tellg()); + if (data.size() < 12) { throw FormatNotSupported{"File is too short to be an HEIF image."}; } - if (header[4] != 'f' || header[5] != 't' || header[6] != 'y' || header[7] != 'p') { + if (data[4] != 'f' || data[5] != 't' || data[6] != 'y' || data[7] != 'p') { throw FormatNotSupported{"Invalid HEIF file: missing 'ftyp' box."}; } - const heif_brand2 brand = heif_read_main_brand(header, 12); + const heif_brand2 brand = heif_read_main_brand(data.data(), 12); const unordered_set supportedFormats = { // HEIC @@ -93,43 +91,6 @@ Task> HeifImageLoader::load( throw FormatNotSupported{fmt::format("HEIF format {:08X} is not supported.", brand)}; } - iStream.seekg(0, ios_base::end); - const int64_t fileSize = iStream.tellg(); - iStream.clear(); - iStream.seekg(0); - - struct ReaderContext { - istream& stream; - int64_t size; - } readerContext = {iStream, fileSize}; - - static constexpr heif_reader reader = { - .reader_api_version = 1, - .get_position = [](void* context) { return (int64_t)static_cast(context)->stream.tellg(); }, - .read = - [](void* data, size_t size, void* context) { - auto& stream = static_cast(context)->stream; - stream.read((char*)data, size); - return stream.good() ? 0 : -1; - }, - .seek = - [](int64_t pos, void* context) { - auto& stream = static_cast(context)->stream; - stream.seekg(pos); - return stream.good() ? 0 : -1; - }, - .wait_for_file_size = - [](int64_t target_size, void* context) { - return static_cast(context)->size < target_size ? heif_reader_grow_status_size_beyond_eof : - heif_reader_grow_status_size_reached; - }, - // Not used by API version 1 - .request_range = {}, - .preload_range_hint = {}, - .release_file_range = {}, - .release_error_msg = {}, - }; - static constexpr auto getIccProfileFromImgAndHandle = [](const heif_image* img, const heif_image_handle* handle) -> optional> { if (handle) { @@ -231,7 +192,7 @@ Task> HeifImageLoader::load( }; }; - const auto [bytesPerRow, data] = getPlaneData(); + const auto [bytesPerRow, planeData] = getPlaneData(); if (heif_image_has_content_light_level(img)) { heif_content_light_level cll; @@ -288,10 +249,10 @@ Task> HeifImageLoader::load( if (bitDepth == 16) { // libheif returns 16-byte aligned uint8_t* data, regardless of the actual bit depth. The alignment and uint8_t type mean it's // well-defined behavior to reinterpret the data as uint16_t. - const auto uint16Data = span{reinterpret_cast(data.data()), data.size() / sizeof(uint16_t)}; + const auto uint16Data = span{reinterpret_cast(planeData.data()), planeData.size() / sizeof(uint16_t)}; co_await toFloat32(uint16Data, numChannels, outView, alphaKind, priority, channelScale, 0.0f, bytesPerRow / sizeof(uint16_t)); } else { - co_await toFloat32(data, numChannels, outView, alphaKind, priority, channelScale, 0.0f, bytesPerRow / sizeof(uint8_t)); + co_await toFloat32(planeData, numChannels, outView, alphaKind, priority, channelScale, 0.0f, bytesPerRow / sizeof(uint8_t)); } // If we've got an ICC color profile, apply that because it's the most detailed / standardized. @@ -552,7 +513,7 @@ Task> HeifImageLoader::load( throw ImageLoadError{"Failed to allocate libheif context."}; } - if (const auto error = heif_context_read_from_reader(ctx.get(), &reader, &readerContext, nullptr); error.code != heif_error_Ok) { + if (const auto error = heif_context_read_from_memory_without_copy(ctx.get(), data.data(), data.size(), nullptr); error.code != heif_error_Ok) { throw ImageLoadError{fmt::format("Failed to read image: {}", error.message)}; } @@ -683,9 +644,9 @@ Task> HeifImageLoader::load( co_return nullopt; } - auto data = co_await decodeImageHandle(auxImgHandle, isGainmap, auxLayerName, partName); + auto auxImageData = co_await decodeImageHandle(auxImgHandle, isGainmap, auxLayerName, partName); co_return AuxImageData{ - .data = std::move(data), + .data = std::move(auxImageData), .isIsoGainmap = isIsoGainmap, .isAppleGainmap = isAppleGainmap, .retain = retainAuxLayer, diff --git a/src/imageio/IcoImageLoader.cpp b/src/imageio/IcoImageLoader.cpp index c18a4cb1..72fb6ed4 100644 --- a/src/imageio/IcoImageLoader.cpp +++ b/src/imageio/IcoImageLoader.cpp @@ -36,7 +36,7 @@ template static T read(const uint8_t* data, bool reverseE } Task> IcoImageLoader::load( - istream& iStream, const fs::path& path, string_view channelSelector, const ImageLoaderSettings& settings, int priority + istringstream& iStream, const fs::path& path, string_view channelSelector, const ImageLoaderSettings& settings, int priority ) const { const size_t initialPos = iStream.tellg(); const bool reverseEndianness = endian::native == endian::big; @@ -181,24 +181,12 @@ Task> IcoImageLoader::load( const auto bytesPerRow = (size_t)nextMultiple(entry.width, 32) / 8; const size_t andMaskSize = bytesPerRow * entry.height; - const size_t maskDataPos = iStream.tellg(); - iStream.seekg(0, ios::end); - const size_t maskDataEnd = iStream.tellg(); - iStream.seekg(maskDataPos, ios_base::beg); - - if (maskDataEnd - maskDataPos < andMaskSize) { - throw ImageLoadError{fmt::format( - "BMP file is too small to contain expected AND mask: {} bytes available, {} bytes expected", - maskDataEnd - maskDataPos, - andMaskSize - )}; + const auto andMaskData = toSpan(iStream).subspan(iStream.tellg(), andMaskSize); + if (andMaskData.size() != andMaskSize) { + throw ImageLoadError{fmt::format("Failed to read AND mask of size {}", andMaskSize)}; } - HeapArray andMaskData(andMaskSize); - iStream.read((char*)andMaskData.data(), andMaskData.size()); - if (!iStream) { - throw ImageLoadError{fmt::format("Failed to read AND mask of size {}", andMaskData.size())}; - } + iStream.seekg(andMaskSize, ios_base::cur); vector> alphaChannels; for (auto& image : imageData) { diff --git a/src/imageio/Jpeg2000ImageLoader.cpp b/src/imageio/Jpeg2000ImageLoader.cpp index 63475515..80034a0b 100644 --- a/src/imageio/Jpeg2000ImageLoader.cpp +++ b/src/imageio/Jpeg2000ImageLoader.cpp @@ -508,7 +508,7 @@ Task> Jpeg2000ImageLoader::load( } Task> Jpeg2000ImageLoader::load( - istream& iStream, const fs::path& path, string_view channelSelector, const ImageLoaderSettings& settings, int priority + istringstream& iStream, const fs::path& path, string_view channelSelector, const ImageLoaderSettings& settings, int priority ) const { const size_t initialPos = iStream.tellg(); @@ -519,14 +519,7 @@ Task> Jpeg2000ImageLoader::load( throw FormatNotSupported{"File is not a JPEG 2000 image or codestream."}; } - iStream.clear(); - iStream.seekg(0, iStream.end); - const auto dataSize = (size_t)iStream.tellg() - initialPos; - iStream.seekg(initialPos, iStream.beg); - - HeapArray data(dataSize); - iStream.read((char*)data.data(), dataSize); - + span data{(const uint8_t*)iStream.view().data() + initialPos, iStream.view().size() - initialPos}; co_return co_await load(data, path, channelSelector, settings, priority, false); } diff --git a/src/imageio/JpegTurboImageLoader.cpp b/src/imageio/JpegTurboImageLoader.cpp index 358df49e..c05f8bad 100644 --- a/src/imageio/JpegTurboImageLoader.cpp +++ b/src/imageio/JpegTurboImageLoader.cpp @@ -39,26 +39,12 @@ using namespace std; namespace tev { Task> - JpegTurboImageLoader::load(istream& iStream, const fs::path&, string_view, const ImageLoaderSettings& settings, int priority) const { - const auto initialPos = iStream.tellg(); - - unsigned char header[2] = {0}; - iStream.read(reinterpret_cast(header), 2); - if (header[0] != 0xFF || header[1] != 0xD8) { + JpegTurboImageLoader::load(istringstream& iStream, const fs::path&, string_view, const ImageLoaderSettings& settings, int priority) const { + const auto buffer = toSpan(iStream).subspan(iStream.tellg()); + if (buffer.size() < 2 || buffer[0] != 0xFF || buffer[1] != 0xD8) { throw FormatNotSupported{"File is not a JPEG image."}; } - iStream.clear(); - iStream.seekg(initialPos, ios::beg); - - // Read the entire stream into memory and decompress from there. JPEG does not support streaming decompression from iostreams. - iStream.seekg(0, ios::end); - const size_t fileSize = iStream.tellg() - initialPos; - iStream.seekg(initialPos, ios::beg); - - HeapArray buffer(fileSize); - iStream.read(reinterpret_cast(buffer.data()), fileSize); - unordered_set seenOffsets; // to avoid processing the same data multiple times struct IsoGainmapInfo { diff --git a/src/imageio/JxlImageLoader.cpp b/src/imageio/JxlImageLoader.cpp index 8e1abf50..e723c4f3 100644 --- a/src/imageio/JxlImageLoader.cpp +++ b/src/imageio/JxlImageLoader.cpp @@ -54,17 +54,6 @@ bool isJxlImage(span data) { return sig == JxlSignature::JXL_SIG_CODESTREAM || sig == JxlSignature::JXL_SIG_CONTAINER; } -bool isJxlImage(istream& iStream) { - uint8_t signature[16]; - iStream.read(reinterpret_cast(signature), sizeof(signature)); - - const bool result = !!iStream && iStream.gcount() == sizeof(signature) && isJxlImage(signature); - - iStream.clear(); - iStream.seekg(0); - return result; -} - string jxlToString(JxlExtraChannelType type) { switch (type) { case JXL_CHANNEL_ALPHA: return "alpha"; @@ -140,20 +129,14 @@ string jxlToString(JxlTransferFunction type) { } // namespace Task> JxlImageLoader::load( - istream& iStream, const fs::path& path, string_view channelSelector, const ImageLoaderSettings& settings, int priority + istringstream& iStream, const fs::path& path, string_view channelSelector, const ImageLoaderSettings& settings, int priority ) const { - if (!isJxlImage(iStream)) { + const auto data = toSpan(iStream).subspan(iStream.tellg()); + if (!isJxlImage(data)) { throw FormatNotSupported{"File is not a JPEG XL image."}; } - iStream.seekg(0, ios::end); - const auto fileSize = iStream.tellg(); - iStream.seekg(0, ios::beg); - - HeapArray fileData(fileSize); - iStream.read(reinterpret_cast(fileData.data()), fileSize); - - co_return co_await load(fileData, path, channelSelector, settings, priority, false); + co_return co_await load(data, path, channelSelector, settings, priority, false); } Task> JxlImageLoader::load( diff --git a/src/imageio/JxrImageLoader.cpp b/src/imageio/JxrImageLoader.cpp index 0e9e041d..01a7cea3 100644 --- a/src/imageio/JxrImageLoader.cpp +++ b/src/imageio/JxrImageLoader.cpp @@ -300,28 +300,17 @@ optional describeFormat(const PKPixelFormatGUID& guid) { } // namespace -Task> JxrImageLoader::load(istream& iStream, const fs::path&, string_view, const ImageLoaderSettings&, int priority) const { - const auto initialPos = iStream.tellg(); +Task> + JxrImageLoader::load(istringstream& iStream, const fs::path&, string_view, const ImageLoaderSettings&, int priority) const { + const auto buffer = toSpan(iStream).subspan(iStream.tellg()); // JXR/HD Photo files start with the TIFF-like magic "II" followed by 0x00BC (little endian) or the big-endian variant. Sniff the first // bytes and bail early if this clearly isn't a JXR so other loaders get a chance. - char magic[4] = {0}; - iStream.read(magic, sizeof(magic)); - iStream.clear(); - iStream.seekg(initialPos, ios::beg); - - const bool isJxrLE = magic[0] == 'I' && magic[1] == 'I' && (uint8_t)magic[2] == 0xBC; + const bool isJxrLE = buffer.size() >= 4 && buffer[0] == 'I' && buffer[1] == 'I' && buffer[2] == 0xBC; if (!isJxrLE) { throw FormatNotSupported{"File is not a JPEG XR image."}; } - iStream.seekg(0, ios::end); - const size_t fileSize = iStream.tellg() - initialPos; - iStream.seekg(initialPos, ios::beg); - - HeapArray buffer(fileSize); - iStream.read(reinterpret_cast(buffer.data()), fileSize); - PKImageDecode* decoder = nullptr; const auto decoderGuard = ScopeGuard{[&]() { if (decoder) { @@ -348,7 +337,7 @@ Task> JxrImageLoader::load(istream& iStream, const fs::path&, }}; WMPStream* stream = nullptr; - JXR_CHECK(factory->CreateStreamFromMemory(&stream, buffer.data(), static_cast(buffer.size()))); + JXR_CHECK(factory->CreateStreamFromMemory(&stream, const_cast(buffer.data()), static_cast(buffer.size()))); JXR_CHECK(decoder->Initialize(decoder, stream)); int32_t width = 0, height = 0; diff --git a/src/imageio/PfmImageLoader.cpp b/src/imageio/PfmImageLoader.cpp index 70cb09ce..425346ea 100644 --- a/src/imageio/PfmImageLoader.cpp +++ b/src/imageio/PfmImageLoader.cpp @@ -96,7 +96,7 @@ static uint8_t version(const PamType pamType) { return (uint8_t)pamType; } -Task> PfmImageLoader::load(istream& iStream, const fs::path&, string_view, const ImageLoaderSettings&, int priority) const { +Task> PfmImageLoader::load(istringstream& iStream, const fs::path&, string_view, const ImageLoaderSettings&, int priority) const { size_t frameIdx = 0; const auto loadPam = [&iStream, &frameIdx, priority]() -> Task> { diff --git a/src/imageio/PngImageLoader.cpp b/src/imageio/PngImageLoader.cpp index 9a9f03fe..15767c59 100644 --- a/src/imageio/PngImageLoader.cpp +++ b/src/imageio/PngImageLoader.cpp @@ -31,7 +31,7 @@ using namespace std; namespace tev { -Task> PngImageLoader::load(istream& iStream, const fs::path&, string_view, const ImageLoaderSettings&, int priority) const { +Task> PngImageLoader::load(istringstream& iStream, const fs::path&, string_view, const ImageLoaderSettings&, int priority) const { png_byte header[8] = {0}; iStream.read(reinterpret_cast(header), sizeof(header)); if (png_sig_cmp(header, 0, sizeof(header))) { diff --git a/src/imageio/QoiImageLoader.cpp b/src/imageio/QoiImageLoader.cpp index bf08152a..9cafe795 100644 --- a/src/imageio/QoiImageLoader.cpp +++ b/src/imageio/QoiImageLoader.cpp @@ -29,27 +29,16 @@ using namespace std; namespace tev { -Task> QoiImageLoader::load(istream& iStream, const fs::path&, string_view, const ImageLoaderSettings&, int priority) const { - char magic[4]; - iStream.read(magic, 4); - string magicString(magic, 4); - - if (magicString != "qoif") { - throw FormatNotSupported{fmt::format("Invalid magic QOI string {}.", magicString)}; +Task> QoiImageLoader::load(istringstream& iStream, const fs::path&, string_view, const ImageLoaderSettings&, int priority) const { + const auto data = toSpan(iStream).subspan(iStream.tellg()); + if (data.size() < 4 || string_view{(const char*)data.data(), 4} != "qoif") { + throw FormatNotSupported{"Invalid magic QOI string."}; } - iStream.clear(); - iStream.seekg(0, iStream.end); - const auto dataSize = iStream.tellg(); - iStream.seekg(0, iStream.beg); - - HeapArray data(dataSize); - iStream.read(data.data(), dataSize); - qoi_desc desc; using DataPtr = unique_ptr; - const auto decodedData = DataPtr{static_cast(qoi_decode(data.data(), (int)dataSize, &desc, 0)), &free}; + const auto decodedData = DataPtr{static_cast(qoi_decode(data.data(), (int)data.size(), &desc, 0)), &free}; if (!decodedData) { throw ImageLoadError{"Failed to decode data from the QOI format."}; } diff --git a/src/imageio/RawImageLoader.cpp b/src/imageio/RawImageLoader.cpp index 3142f38a..2bf1b2eb 100644 --- a/src/imageio/RawImageLoader.cpp +++ b/src/imageio/RawImageLoader.cpp @@ -33,83 +33,6 @@ using namespace std; namespace tev { -class LibRawDataStream final : public LibRaw_abstract_datastream { -public: - LibRawDataStream(istream& stream, const fs::path& path) : mStream{stream} { - const auto pathStr = toString(path); - strncpy(mPath, pathStr.c_str(), sizeof(mPath) - 1); - -#ifdef LIBRAW_WIN32_UNICODEPATHS - const auto wPathStr = path.wstring(); - wcsncpy(mWPath, wPathStr.c_str(), sizeof(mWPath) / sizeof(mWPath[0]) - 1); -#endif - } - - ~LibRawDataStream() override = default; - - int valid() override { return mStream.good(); } - - int read(void* ptr, size_t size, size_t nmemb) override { - mStream.read((char*)ptr, size * nmemb); - return mStream.gcount() / size; - } - - int seek(INT64 o, int whence) override { - ios_base::seekdir dir; - switch (whence) { - case SEEK_SET: dir = ios_base::beg; break; - case SEEK_CUR: dir = ios_base::cur; break; - case SEEK_END: dir = ios_base::end; break; - default: return -1; - } - mStream.clear(); // Clear any eof flags - mStream.seekg(o, dir); - return mStream.good() ? 0 : -1; - } - - INT64 tell() override { return mStream.tellg(); } - - INT64 size() override { - auto currentPos = mStream.tellg(); - mStream.seekg(0, ios_base::end); - auto size = mStream.tellg(); - mStream.seekg(currentPos, ios_base::beg); - return size; - } - - int get_char() override { return mStream.get(); } - - char* gets(char* str, int sz) override { - mStream.getline(str, sz); - return mStream.good() ? str : nullptr; - } - - int scanf_one(const char* fmt, void* val) override { - // Not implemented - return -1; - } - - int eof() override { return mStream.eof(); } - - int jpeg_src(void* /*jpegdata*/) override { - // Not implemented - return -1; - } - - const char* fname() override { return mPath; }; -#ifdef LIBRAW_WIN32_UNICODEPATHS - const wchar_t* wfname() override { return mWPath; }; -#endif - -private: - istream& mStream; - - char mPath[1024] = {}; -#ifdef LIBRAW_WIN32_UNICODEPATHS - wchar_t mWPath[1024] = {}; -#endif -}; - Vector2i flip_index(Vector2i idx, const Vector2i size, const int flip) { if (flip & 4) { swap(idx.x(), idx.y()); @@ -140,7 +63,7 @@ Box2i maskToBox(const int mask[4]) { }; } -Task> RawImageLoader::load(istream& iStream, const fs::path& path, string_view, const ImageLoaderSettings&, int priority) const { +Task> RawImageLoader::load(istringstream& iStream, const fs::path& path, string_view, const ImageLoaderSettings&, int priority) const { if (toLower(toString(path.extension())) == ".dng") { throw FormatNotSupported{"DNG files will be handled by TiffImageLoader."}; } @@ -216,8 +139,8 @@ Task> RawImageLoader::load(istream& iStream, const fs::path& p iProcessor.imgdata.params.use_camera_matrix = true; iProcessor.imgdata.params.use_camera_wb = true; - auto librawStream = LibRawDataStream{iStream, path}; - if (const int error = iProcessor.open_datastream(&librawStream); error != LIBRAW_SUCCESS) { + const auto buffer = toSpan(iStream).subspan(iStream.tellg()); + if (const int error = iProcessor.open_buffer(buffer.data(), buffer.size()); error != LIBRAW_SUCCESS) { throw FormatNotSupported{fmt::format("Could not open raw image: {}", libraw_strerror(error))}; } diff --git a/src/imageio/StbiImageLoader.cpp b/src/imageio/StbiImageLoader.cpp index c207031a..29bbb72f 100644 --- a/src/imageio/StbiImageLoader.cpp +++ b/src/imageio/StbiImageLoader.cpp @@ -28,19 +28,9 @@ using namespace std; namespace tev { -Task> StbiImageLoader::load(istream& iStream, const fs::path&, string_view, const ImageLoaderSettings&, int priority) const { - static constexpr stbi_io_callbacks callbacks = { - // Read - [](void* context, char* data, int size) { - auto stream = static_cast(context); - stream->read(data, size); - return (int)stream->gcount(); - }, - // Seek - [](void* context, int size) { static_cast(context)->seekg(size, ios_base::cur); }, - // EOF - [](void* context) { return (int)!!(*static_cast(context)); }, - }; +Task> + StbiImageLoader::load(istringstream& iStream, const fs::path&, string_view, const ImageLoaderSettings&, int priority) const { + const auto buffer = toSpan(iStream).subspan(iStream.tellg()); using DataPtr = unique_ptr; @@ -48,28 +38,24 @@ Task> StbiImageLoader::load(istream& iStream, const fs::path&, int numChannels; int numFrames = 1; Vector2i size; - const bool isHdr = stbi_is_hdr_from_callbacks(&callbacks, &iStream) != 0; - iStream.clear(); - iStream.seekg(0); + const bool isHdr = stbi_is_hdr_from_memory(buffer.data(), (int)buffer.size()) != 0; if (isHdr) { // Returns a float*, but we type erase to void* to unify the code below - data.reset(stbi_loadf_from_callbacks(&callbacks, &iStream, &size.x(), &size.y(), &numChannels, 0)); + data.reset(stbi_loadf_from_memory(buffer.data(), (int)buffer.size(), &size.x(), &size.y(), &numChannels, 0)); } else { stbi__context s; - stbi__start_callbacks(&s, (stbi_io_callbacks*)&callbacks, &iStream); + stbi__start_mem(&s, buffer.data(), (int)buffer.size()); bool isGif = stbi__gif_test(&s); - iStream.clear(); - iStream.seekg(0); if (isGif) { - stbi__start_callbacks(&s, (stbi_io_callbacks*)&callbacks, &iStream); + stbi__start_mem(&s, buffer.data(), (int)buffer.size()); int* delays; // We don't care about gif frame delays. Read and discard. // Returns a void*, but, looking at the implementation, it was originally uint8_t*, so is fine to cast back to that later data.reset(stbi__load_gif_main(&s, &delays, &size.x(), &size.y(), &numFrames, &numChannels, 0)); } else { // Returns a uint8_t*, but we type erase to void* to unify the code below - data.reset(stbi_load_from_callbacks(&callbacks, &iStream, &size.x(), &size.y(), &numChannels, 0)); + data.reset(stbi_load_from_memory(buffer.data(), (int)buffer.size(), &size.x(), &size.y(), &numChannels, 0)); } } diff --git a/src/imageio/TiffImageLoader.cpp b/src/imageio/TiffImageLoader.cpp index 538ae690..75fe2af4 100644 --- a/src/imageio/TiffImageLoader.cpp +++ b/src/imageio/TiffImageLoader.cpp @@ -309,7 +309,6 @@ static void tiffWarningHandler(const char* module, const char* fmt, va_list args ); } -// Custom TIFF I/O functions for reading from an istream struct TiffData { const uint8_t* data; toff_t offset; @@ -2703,19 +2702,16 @@ Task readTiffImage( } Task> - TiffImageLoader::load(istream& iStream, const fs::path& path, string_view, const ImageLoaderSettings& settings, int priority) const { - // This function tries to implement the most relevant parts of the TIFF 6.0 spec: - // https://www.itu.int/itudoc/itu-t/com16/tiff-fx/docs/tiff6.pdf - char magic[4] = {0}; - iStream.read(magic, sizeof(magic)); - if (!iStream || (magic[0] != 'I' && magic[0] != 'M') || magic[1] != magic[0]) { + TiffImageLoader::load(istringstream& iStream, const fs::path& path, string_view, const ImageLoaderSettings& settings, int priority) const { + const auto buffer = toSpan(iStream).subspan(iStream.tellg()); + if (buffer.size() < 4 || (buffer[0] != 'I' && buffer[0] != 'M') || buffer[1] != buffer[0]) { throw FormatNotSupported{"File is not a TIFF image."}; } - const auto fileEndianness = magic[0] == 'I' ? endian::little : endian::big; + const auto fileEndianness = buffer[0] == 'I' ? endian::little : endian::big; const bool reverseEndian = fileEndianness != endian::native; - const uint16_t answer = reverseEndian ? (magic[2] << 8 | magic[3]) : (magic[3] << 8 | magic[2]); + const uint16_t answer = reverseEndian ? (buffer[2] << 8 | buffer[3]) : (buffer[3] << 8 | buffer[2]); if (answer != 42) { throw FormatNotSupported{"File is not a TIFF image."}; } @@ -2723,24 +2719,13 @@ Task> TIFFSetErrorHandler(tiffErrorHandler); TIFFSetWarningHandler(tiffWarningHandler); - // Read the entire stream into memory and decompress from there. Technically, we can progressively decode TIFF images, but we want - // to additionally load the TIFF image via our EXIF library, which requires the file to be in memory. For the same reason, we also - // prepend the EXIF FOURCC to the data ahead of the TIFF header. - iStream.seekg(0, ios::end); - const size_t fileSize = iStream.tellg(); - iStream.seekg(0, ios::beg); - - HeapArray buffer(fileSize + sizeof(Exif::FOURCC)); - copy(Exif::FOURCC.begin(), Exif::FOURCC.end(), buffer.data()); - iStream.read((char*)buffer.data() + sizeof(Exif::FOURCC), fileSize); - optional exifAttributes; try { const auto exif = Exif{buffer}; exifAttributes = exif.toAttributes(); } catch (const invalid_argument& e) { tlog::warning("Failed to read EXIF metadata: {}", e.what()); } - TiffData tiffData{buffer.data() + sizeof(Exif::FOURCC), 0, (tsize_t)fileSize}; + TiffData tiffData{buffer.data(), 0, (tsize_t)buffer.size()}; TIFF* tif = TIFFClientOpen( toString(path).c_str(), "rMc", // read-only w/ memory mapping; no strip chopping diff --git a/src/imageio/WebpImageLoader.cpp b/src/imageio/WebpImageLoader.cpp index ffb79ba9..4fc29523 100644 --- a/src/imageio/WebpImageLoader.cpp +++ b/src/imageio/WebpImageLoader.cpp @@ -32,20 +32,13 @@ using namespace std; namespace tev { -Task> WebpImageLoader::load(istream& iStream, const fs::path&, string_view, const ImageLoaderSettings&, int priority) const { - char magic[16] = {0}; - iStream.read(magic, sizeof(magic)); - if (!iStream || strncmp(magic, "RIFF", 4) != 0 || strncmp(magic + 8, "WEBP", 4) != 0) { +Task> + WebpImageLoader::load(istringstream& iStream, const fs::path&, string_view, const ImageLoaderSettings&, int priority) const { + const auto buffer = toSpan(iStream).subspan(iStream.tellg()); + if (buffer.size() < 16 || strncmp((const char*)buffer.data(), "RIFF", 4) != 0 || strncmp((const char*)buffer.data() + 8, "WEBP", 4) != 0) { throw FormatNotSupported{"File is not a webp image."}; } - iStream.seekg(0, ios::end); - const auto fileSize = iStream.tellg(); - iStream.seekg(0, ios::beg); - - HeapArray buffer(fileSize); - iStream.read((char*)buffer.data(), fileSize); - WebPData data = {buffer.data(), buffer.size()}; using DemuxPtr = unique_ptr; diff --git a/src/main.cpp b/src/main.cpp index f9f7bc2f..3c1bd0e7 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -111,17 +111,18 @@ static void handleIpcPacket(const IpcPacket& packet, const shared_ptrscheduleToUiThread([&, info] { - stringstream imageStream; - imageStream << "empty" << " " << info.width << " " << info.height << " " << info.nChannels << " "; + ostringstream oss; + oss << "empty" << " " << info.width << " " << info.height << " " << info.nChannels << " "; for (int i = 0; i < info.nChannels; ++i) { // The following lines encode strings by prefixing their length. The reason for using this encoding is to allow // arbitrary characters, including whitespaces, in the channel names. - imageStream << info.channelNames[i].length() << info.channelNames[i]; + oss << info.channelNames[i].length() << info.channelNames[i]; } + istringstream iStream{std::move(oss).str()}; auto imagesLoadTask = tryLoadImage( toPath(info.imageName), - imageStream, + iStream, "", sImageViewer->imagesLoader().imageLoaderSettings(), sImageViewer->imagesLoader().groupChannels()