├── resources ├── windows │ ├── icon.rc │ ├── icon.ico │ ├── icon.png │ ├── tev.manifest │ ├── winget.yaml │ └── patch.wxs ├── screenshot.png ├── macos │ ├── icon.icns │ └── Info.plist └── linux │ ├── icon-128.png │ ├── icon-16.png │ ├── icon-24.png │ ├── icon-256.png │ ├── icon-32.png │ ├── icon-48.png │ ├── icon-512.png │ ├── icon-64.png │ ├── icon-96.png │ ├── screenshot.png │ ├── tev.desktop │ └── tev.metainfo.xml.in ├── .envrc ├── .editorconfig ├── scripts ├── configure-winget.cmake ├── sample-colormap.py ├── create-appimage.cmake └── turbo_colormap.py ├── .gitignore ├── CITATION.cff ├── src ├── Box.cpp ├── Lazy.cpp ├── SharedQueue.cpp ├── VectorGraphics.cpp ├── imageio │ ├── StbiHdrImageSaver.cpp │ ├── ImageSaver.cpp │ ├── QoiImageSaver.cpp │ ├── StbiLdrImageSaver.cpp │ ├── EmptyImageLoader.cpp │ ├── QoiImageLoader.cpp │ ├── ExrImageSaver.cpp │ ├── GainMap.cpp │ ├── AppleMakerNote.cpp │ ├── Xmp.cpp │ ├── PfmImageLoader.cpp │ ├── ClipboardImageLoader.cpp │ ├── StbiImageLoader.cpp │ ├── JxlImageSaver.cpp │ └── Exif.cpp ├── WaylandClipboard.cpp ├── Task.cpp ├── Channel.cpp ├── ThreadPool.cpp ├── ImageInfoWindow.cpp └── MultiGraph.cpp ├── include └── tev │ ├── FalseColor.h │ ├── WaylandClipboard.h │ ├── imageio │ ├── GainMap.h │ ├── Xmp.h │ ├── DdsImageLoader.h │ ├── HeifImageLoader.h │ ├── PfmImageLoader.h │ ├── PngImageLoader.h │ ├── QoiImageLoader.h │ ├── RawImageLoader.h │ ├── StbiImageLoader.h │ ├── TiffImageLoader.h │ ├── WebpImageLoader.h │ ├── EmptyImageLoader.h │ ├── ExrImageLoader.h │ ├── JxlImageLoader.h │ ├── ClipboardImageLoader.h │ ├── UltraHdrImageLoader.h │ ├── JpegTurboImageLoader.h │ ├── ExrImageSaver.h │ ├── JxlImageSaver.h │ ├── QoiImageSaver.h │ ├── StbiHdrImageSaver.h │ ├── StbiLdrImageSaver.h │ ├── ImageSaver.h │ ├── Exif.h │ ├── ImageLoader.h │ └── AppleMakerNote.h │ ├── HelpWindow.h │ ├── SharedQueue.h │ ├── ImageInfoWindow.h │ ├── UberShader.h │ ├── MultiGraph.h │ ├── Lazy.h │ ├── ImageButton.h │ ├── Box.h │ ├── ThreadPool.h │ ├── VectorGraphics.h │ ├── Channel.h │ └── ImageCanvas.h ├── flake.nix ├── flake.lock ├── .clang-format ├── package.nix ├── .gitmodules └── .github └── workflows └── main.yml /resources/windows/icon.rc: -------------------------------------------------------------------------------- 1 | GLFW_ICON ICON "icon.ico" 2 | -------------------------------------------------------------------------------- /resources/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tom94/tev/HEAD/resources/screenshot.png -------------------------------------------------------------------------------- /resources/macos/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tom94/tev/HEAD/resources/macos/icon.icns -------------------------------------------------------------------------------- /resources/windows/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tom94/tev/HEAD/resources/windows/icon.ico -------------------------------------------------------------------------------- /resources/windows/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tom94/tev/HEAD/resources/windows/icon.png -------------------------------------------------------------------------------- /resources/linux/icon-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tom94/tev/HEAD/resources/linux/icon-128.png -------------------------------------------------------------------------------- /resources/linux/icon-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tom94/tev/HEAD/resources/linux/icon-16.png -------------------------------------------------------------------------------- /resources/linux/icon-24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tom94/tev/HEAD/resources/linux/icon-24.png -------------------------------------------------------------------------------- /resources/linux/icon-256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tom94/tev/HEAD/resources/linux/icon-256.png -------------------------------------------------------------------------------- /resources/linux/icon-32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tom94/tev/HEAD/resources/linux/icon-32.png -------------------------------------------------------------------------------- /resources/linux/icon-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tom94/tev/HEAD/resources/linux/icon-48.png -------------------------------------------------------------------------------- /resources/linux/icon-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tom94/tev/HEAD/resources/linux/icon-512.png -------------------------------------------------------------------------------- /resources/linux/icon-64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tom94/tev/HEAD/resources/linux/icon-64.png -------------------------------------------------------------------------------- /resources/linux/icon-96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tom94/tev/HEAD/resources/linux/icon-96.png -------------------------------------------------------------------------------- /resources/linux/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tom94/tev/HEAD/resources/linux/screenshot.png -------------------------------------------------------------------------------- /.envrc: -------------------------------------------------------------------------------- 1 | strict_env 2 | if [ "$(uname)" = "Linux" ]; then 3 | watch_file ./*.nix 4 | use flake 5 | fi 6 | 7 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | indent_style = space 7 | indent_size = 4 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | max_line_length = 140 11 | 12 | [*.{yml,nix,clang-format}] 13 | indent_size = 2 14 | -------------------------------------------------------------------------------- /resources/linux/tev.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Type=Application 3 | Version=1.0 4 | Name=tev 5 | GenericName=Image Viewer 6 | Comment=High dynamic range (HDR) image viewer for people who care about colors 7 | Exec=tev %F 8 | Icon=@TEV_LINUX_APP_ID@ 9 | Terminal=false 10 | MimeType=image/*; 11 | Categories=Graphics; 12 | -------------------------------------------------------------------------------- /resources/windows/tev.manifest: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | true 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /resources/windows/winget.yaml: -------------------------------------------------------------------------------- 1 | PackageIdentifier: Tom94.tev 2 | PackageVersion: @TEV_VERSION@ 3 | PackageLocale: en-US 4 | Publisher: Tom94 (Thomas Müller) 5 | PackageName: tev 6 | License: BSD-3 7 | ShortDescription: @CPACK_PACKAGE_DESCRIPTION_SUMMARY@ 8 | Installers: 9 | - Architecture: x64 10 | InstallerType: wix 11 | InstallerUrl: https://github.com/Tom94/tev/releases/download/v@TEV_VERSION@/tev-@TEV_VERSION@-win64.msi 12 | InstallerSha256: @TEV_INSTALLER_SHA256@ 13 | # InstallerSwitches: 14 | # Silent: /q 15 | # SilentWithProgress: /q 16 | ManifestType: singleton 17 | ManifestVersion: 1.0.0 18 | -------------------------------------------------------------------------------- /scripts/configure-winget.cmake: -------------------------------------------------------------------------------- 1 | include(CMakePrintHelpers) 2 | cmake_print_variables(CPACK_TEMPORARY_DIRECTORY) 3 | cmake_print_variables(CPACK_TOPLEVEL_DIRECTORY) 4 | cmake_print_variables(CPACK_PACKAGE_DIRECTORY) 5 | cmake_print_variables(CPACK_PACKAGE_FILE_NAME) 6 | cmake_print_variables(CMAKE_SYSTEM_PROCESSOR) 7 | cmake_print_variables(CPACK_INSTALL_CMAKE_PROJECTS) 8 | 9 | set(TEV_VERSION "@TEV_VERSION@") 10 | list(GET CPACK_INSTALL_CMAKE_PROJECTS 0 TEV_BUILD_DIR) 11 | 12 | file(SHA256 "${CPACK_PACKAGE_FILES}" TEV_INSTALLER_SHA256) 13 | configure_file("${TEV_BUILD_DIR}/resources/windows/winget.yaml" "${TEV_BUILD_DIR}/tev.yaml") 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Build artifacts 2 | /build 3 | /x64 4 | _CPack_Packages 5 | 6 | # Automatically generated files by IDE or env 7 | /.direnv 8 | /.vscode 9 | /.cache 10 | 11 | # Python cache 12 | __pycache__ 13 | 14 | # macOS 15 | .DS_Store 16 | 17 | # Development project files 18 | *.sublime-project 19 | 20 | # Compiled Object files 21 | *.slo 22 | *.lo 23 | *.o 24 | *.obj 25 | 26 | # Precompiled Headers 27 | *.gch 28 | *.pch 29 | 30 | # Compiled Dynamic libraries 31 | *.so 32 | *.dylib 33 | *.dll 34 | 35 | # Fortran module files 36 | *.mod 37 | 38 | # Compiled Static libraries 39 | *.lai 40 | *.la 41 | *.a 42 | *.lib 43 | 44 | # Executables 45 | *.exe 46 | *.out 47 | *.app 48 | -------------------------------------------------------------------------------- /CITATION.cff: -------------------------------------------------------------------------------- 1 | cff-version: 1.2.0 2 | title: tev --- The EDR Viewer 3 | message: >- 4 | If you use this software, please cite it using the 5 | metadata from this file. 6 | type: software 7 | authors: 8 | - given-names: Thomas 9 | email: contact@tom94.net 10 | family-names: Müller 11 | repository-code: 'https://github.com/Tom94/tev' 12 | abstract: >- 13 | High dynamic range (HDR) image viewer for people who care about colors. It is 14 | - Lightning fast: starts up instantly, loads hundreds of images in seconds. 15 | - Accurate: understands color profiles and displays HDR. 16 | - Versatile: supports many formats, histograms, pixel peeping, tonemaps, etc. 17 | keywords: 18 | - 'image, rendering, hdr, ldr, exr, openexr, comparison, screenshot' 19 | license: GPL-3.0-only 20 | version: 2.0 21 | date-released: '2017-08-26' 22 | -------------------------------------------------------------------------------- /src/Box.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * tev -- the EDR viewer 3 | * 4 | * Copyright (C) 2025 Thomas Müller 5 | * 6 | * This program is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | #include 20 | 21 | namespace tev { 22 | 23 | } 24 | -------------------------------------------------------------------------------- /src/Lazy.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * tev -- the EDR viewer 3 | * 4 | * Copyright (C) 2025 Thomas Müller 5 | * 6 | * This program is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | #include 20 | 21 | namespace tev { 22 | 23 | } 24 | -------------------------------------------------------------------------------- /src/SharedQueue.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * tev -- the EDR viewer 3 | * 4 | * Copyright (C) 2025 Thomas Müller 5 | * 6 | * This program is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | #include 20 | 21 | namespace tev { 22 | 23 | } 24 | -------------------------------------------------------------------------------- /resources/windows/patch.wxs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/VectorGraphics.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * tev -- the EDR viewer 3 | * 4 | * Copyright (C) 2025 Thomas Müller 5 | * 6 | * This program is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | #include 20 | 21 | using namespace std; 22 | 23 | namespace tev { 24 | 25 | } 26 | -------------------------------------------------------------------------------- /include/tev/FalseColor.h: -------------------------------------------------------------------------------- 1 | /* 2 | * tev -- the EDR viewer 3 | * 4 | * Copyright (C) 2025 Thomas Müller 5 | * 6 | * This program is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | #pragma once 20 | 21 | #include 22 | 23 | #include 24 | 25 | namespace tev { namespace colormap { 26 | 27 | std::span turbo(); 28 | std::span viridis(); 29 | 30 | }} // namespace tev::colormap 31 | -------------------------------------------------------------------------------- /include/tev/WaylandClipboard.h: -------------------------------------------------------------------------------- 1 | /* 2 | * tev -- the EDR viewer 3 | * 4 | * Copyright (C) 2025 Thomas Müller 5 | * 6 | * This program is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | #pragma once 20 | 21 | #include 22 | 23 | namespace tev { 24 | 25 | void waylandSetClipboardPngImage(const char* data, size_t size); 26 | const char* waylandGetClipboardPngImage(size_t* size); 27 | 28 | } // namespace tev 29 | -------------------------------------------------------------------------------- /include/tev/imageio/GainMap.h: -------------------------------------------------------------------------------- 1 | /* 2 | * tev -- the EDR viewer 3 | * 4 | * Copyright (C) 2025 Thomas Müller 5 | * 6 | * This program is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | #pragma once 20 | 21 | #include 22 | #include 23 | #include 24 | 25 | class AppleMakerNote; 26 | 27 | namespace tev { 28 | 29 | Task applyAppleGainMap(ImageData& image, const ImageData& gainMap, int priority, const AppleMakerNote* amn); 30 | 31 | } 32 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "tev — The EDR Viewer"; 3 | 4 | inputs = { 5 | nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; 6 | flake-utils.url = "github:numtide/flake-utils"; 7 | }; 8 | 9 | outputs = { self, nixpkgs, flake-utils }: 10 | flake-utils.lib.eachDefaultSystem (system: 11 | let 12 | pkgs = nixpkgs.legacyPackages.${system}; 13 | package = pkgs.callPackage ./package.nix { }; 14 | in 15 | { 16 | packages = { 17 | default = package; 18 | tev = package; 19 | }; 20 | 21 | devShells.default = pkgs.mkShell { 22 | inputsFrom = [ package ]; 23 | buildInputs = with pkgs; if stdenv.isDarwin then [ ] else [ 24 | gcc 25 | gdb 26 | binutils 27 | mesa-demos # for glxinfo, eglinfo 28 | ]; 29 | LD_LIBRARY_PATH = pkgs.lib.makeLibraryPath (with pkgs; [ 30 | wayland 31 | libxkbcommon 32 | ]); 33 | }; 34 | 35 | apps.default = flake-utils.lib.mkApp { 36 | drv = package; 37 | }; 38 | } 39 | ); 40 | } 41 | -------------------------------------------------------------------------------- /scripts/sample-colormap.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | """ 4 | Generates a list of color values which are dense samples of the 5 | colormap given as argument. The list is 1-dimensional as contains 6 | 4 floats in sequence per-sample (R,G,B,A). It can be used in C++ 7 | code to make colormap textures for use by the false-color shader. 8 | """ 9 | 10 | import sys 11 | import argparse 12 | import matplotlib.pyplot as plt 13 | import numpy as np 14 | 15 | def main(arguments): 16 | """Main function of this program.""" 17 | 18 | parser = argparse.ArgumentParser(description="Samples densely from a colormap.") 19 | parser.add_argument("name", type=str, help="The name of the colormap to sample values from.") 20 | 21 | args = parser.parse_args(arguments) 22 | 23 | xs = np.linspace(0, 1, 256) 24 | 25 | if args.name.lower() == "turbo": 26 | from turbo_colormap import interpolate, turbo_colormap_data 27 | samples = [interpolate(turbo_colormap_data, x) for x in xs] 28 | else: 29 | cmap = plt.get_cmap(args.name) 30 | samples = cmap(xs) 31 | 32 | samples = ",\n".join([", ".join([str(y) + "f" for y in x]) for x in samples]) + "," 33 | print(samples) 34 | 35 | 36 | if __name__ == "__main__": 37 | sys.exit(main(sys.argv[1:])) 38 | -------------------------------------------------------------------------------- /include/tev/imageio/Xmp.h: -------------------------------------------------------------------------------- 1 | /* 2 | * tev -- the EDR viewer 3 | * 4 | * Copyright (C) 2025 Thomas Müller 5 | * 6 | * This program is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | #pragma once 20 | 21 | #include 22 | #include 23 | 24 | #include 25 | 26 | namespace tev { 27 | 28 | class Xmp { 29 | public: 30 | Xmp(std::string_view xmpData); 31 | 32 | EOrientation orientation() const { return mOrientation; } 33 | const AttributeNode& attributes() const { return mAttributes; } 34 | 35 | private: 36 | AttributeNode mAttributes; 37 | EOrientation mOrientation = EOrientation::None; 38 | }; 39 | 40 | } // namespace tev 41 | -------------------------------------------------------------------------------- /include/tev/imageio/DdsImageLoader.h: -------------------------------------------------------------------------------- 1 | /* 2 | * tev -- the EDR viewer 3 | * 4 | * Copyright (C) 2025 Thomas Müller 5 | * 6 | * This program is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | #pragma once 20 | 21 | #include 22 | #include 23 | 24 | #include 25 | 26 | namespace tev { 27 | 28 | class DdsImageLoader : public ImageLoader { 29 | public: 30 | Task> 31 | load(std::istream& iStream, const fs::path& path, std::string_view channelSelector, int priority, bool applyGainmaps) const override; 32 | 33 | std::string name() const override { return "DDS"; } 34 | }; 35 | 36 | } // namespace tev 37 | -------------------------------------------------------------------------------- /include/tev/imageio/HeifImageLoader.h: -------------------------------------------------------------------------------- 1 | /* 2 | * tev -- the EDR viewer 3 | * 4 | * Copyright (C) 2025 Thomas Müller 5 | * 6 | * This program is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | #pragma once 20 | 21 | #include 22 | #include 23 | 24 | #include 25 | 26 | namespace tev { 27 | 28 | class HeifImageLoader : public ImageLoader { 29 | public: 30 | Task> 31 | load(std::istream& iStream, const fs::path& path, std::string_view channelSelector, int priority, bool applyGainmaps) const override; 32 | 33 | std::string name() const override { return "HEIF"; } 34 | }; 35 | 36 | } // namespace tev 37 | -------------------------------------------------------------------------------- /include/tev/imageio/PfmImageLoader.h: -------------------------------------------------------------------------------- 1 | /* 2 | * tev -- the EDR viewer 3 | * 4 | * Copyright (C) 2025 Thomas Müller 5 | * 6 | * This program is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | #pragma once 20 | 21 | #include 22 | #include 23 | 24 | #include 25 | 26 | namespace tev { 27 | 28 | class PfmImageLoader : public ImageLoader { 29 | public: 30 | Task> 31 | load(std::istream& iStream, const fs::path& path, std::string_view channelSelector, int priority, bool applyGainmaps) const override; 32 | 33 | std::string name() const override { return "PFM"; } 34 | }; 35 | 36 | } // namespace tev 37 | -------------------------------------------------------------------------------- /include/tev/imageio/PngImageLoader.h: -------------------------------------------------------------------------------- 1 | /* 2 | * tev -- the EDR viewer 3 | * 4 | * Copyright (C) 2025 Thomas Müller 5 | * 6 | * This program is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | #pragma once 20 | 21 | #include 22 | #include 23 | 24 | #include 25 | 26 | namespace tev { 27 | 28 | class PngImageLoader : public ImageLoader { 29 | public: 30 | Task> 31 | load(std::istream& iStream, const fs::path& path, std::string_view channelSelector, int priority, bool applyGainmaps) const override; 32 | 33 | std::string name() const override { return "PNG"; } 34 | }; 35 | 36 | } // namespace tev 37 | -------------------------------------------------------------------------------- /include/tev/imageio/QoiImageLoader.h: -------------------------------------------------------------------------------- 1 | /* 2 | * tev -- the EDR viewer 3 | * 4 | * Copyright (C) 2025 Thomas Müller 5 | * 6 | * This program is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | #pragma once 20 | 21 | #include 22 | #include 23 | 24 | #include 25 | 26 | namespace tev { 27 | 28 | class QoiImageLoader : public ImageLoader { 29 | public: 30 | Task> 31 | load(std::istream& iStream, const fs::path& path, std::string_view channelSelector, int priority, bool applyGainmaps) const override; 32 | 33 | std::string name() const override { return "QOI"; } 34 | }; 35 | 36 | } // namespace tev 37 | -------------------------------------------------------------------------------- /include/tev/imageio/RawImageLoader.h: -------------------------------------------------------------------------------- 1 | /* 2 | * tev -- the EDR viewer 3 | * 4 | * Copyright (C) 2025 Thomas Müller 5 | * 6 | * This program is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | #pragma once 20 | 21 | #include 22 | #include 23 | 24 | #include 25 | 26 | namespace tev { 27 | 28 | class RawImageLoader : public ImageLoader { 29 | public: 30 | Task> 31 | load(std::istream& iStream, const fs::path& path, std::string_view channelSelector, int priority, bool applyGainmaps) const override; 32 | 33 | std::string name() const override { return "RAW"; } 34 | }; 35 | 36 | } // namespace tev 37 | -------------------------------------------------------------------------------- /include/tev/imageio/StbiImageLoader.h: -------------------------------------------------------------------------------- 1 | /* 2 | * tev -- the EDR viewer 3 | * 4 | * Copyright (C) 2025 Thomas Müller 5 | * 6 | * This program is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | #pragma once 20 | 21 | #include 22 | #include 23 | 24 | #include 25 | 26 | namespace tev { 27 | 28 | class StbiImageLoader : public ImageLoader { 29 | public: 30 | Task> 31 | load(std::istream& iStream, const fs::path& path, std::string_view channelSelector, int priority, bool applyGainmaps) const override; 32 | 33 | std::string name() const override { return "STBI"; } 34 | }; 35 | 36 | } // namespace tev 37 | -------------------------------------------------------------------------------- /include/tev/imageio/TiffImageLoader.h: -------------------------------------------------------------------------------- 1 | /* 2 | * tev -- the EDR viewer 3 | * 4 | * Copyright (C) 2025 Thomas Müller 5 | * 6 | * This program is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | #pragma once 20 | 21 | #include 22 | #include 23 | 24 | #include 25 | 26 | namespace tev { 27 | 28 | class TiffImageLoader : public ImageLoader { 29 | public: 30 | Task> 31 | load(std::istream& iStream, const fs::path& path, std::string_view channelSelector, int priority, bool applyGainmaps) const override; 32 | 33 | std::string name() const override { return "TIFF"; } 34 | }; 35 | 36 | } // namespace tev 37 | -------------------------------------------------------------------------------- /include/tev/imageio/WebpImageLoader.h: -------------------------------------------------------------------------------- 1 | /* 2 | * tev -- the EDR viewer 3 | * 4 | * Copyright (C) 2025 Thomas Müller 5 | * 6 | * This program is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | #pragma once 20 | 21 | #include 22 | #include 23 | 24 | #include 25 | 26 | namespace tev { 27 | 28 | class WebpImageLoader : public ImageLoader { 29 | public: 30 | Task> 31 | load(std::istream& iStream, const fs::path& path, std::string_view channelSelector, int priority, bool applyGainmaps) const override; 32 | 33 | std::string name() const override { return "WEBP"; } 34 | }; 35 | 36 | } // namespace tev 37 | -------------------------------------------------------------------------------- /include/tev/imageio/EmptyImageLoader.h: -------------------------------------------------------------------------------- 1 | /* 2 | * tev -- the EDR viewer 3 | * 4 | * Copyright (C) 2025 Thomas Müller 5 | * 6 | * This program is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | #pragma once 20 | 21 | #include 22 | #include 23 | 24 | #include 25 | 26 | namespace tev { 27 | 28 | class EmptyImageLoader : public ImageLoader { 29 | public: 30 | Task> 31 | load(std::istream& iStream, const fs::path& path, std::string_view channelSelector, int priority, bool applyGainmaps) const override; 32 | 33 | std::string name() const override { return "IPC"; } 34 | }; 35 | 36 | } // namespace tev 37 | -------------------------------------------------------------------------------- /include/tev/imageio/ExrImageLoader.h: -------------------------------------------------------------------------------- 1 | /* 2 | * tev -- the EDR viewer 3 | * 4 | * Copyright (C) 2025 Thomas Müller 5 | * 6 | * This program is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | #pragma once 20 | 21 | #include 22 | #include 23 | 24 | #include 25 | 26 | namespace tev { 27 | 28 | class ExrImageLoader : public ImageLoader { 29 | public: 30 | Task> 31 | load(std::istream& iStream, const fs::path& path, std::string_view channelSelector, int priority, bool applyGainmaps) const override; 32 | 33 | std::string name() const override { return "OpenEXR"; } 34 | }; 35 | 36 | } // namespace tev 37 | -------------------------------------------------------------------------------- /include/tev/imageio/JxlImageLoader.h: -------------------------------------------------------------------------------- 1 | /* 2 | * tev -- the EDR viewer 3 | * 4 | * Copyright (C) 2025 Thomas Müller 5 | * 6 | * This program is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | #pragma once 20 | 21 | #include 22 | #include 23 | 24 | #include 25 | 26 | namespace tev { 27 | 28 | class JxlImageLoader : public ImageLoader { 29 | public: 30 | Task> 31 | load(std::istream& iStream, const fs::path& path, std::string_view channelSelector, int priority, bool applyGainmaps) const override; 32 | 33 | std::string name() const override { return "JPEG XL"; } 34 | }; 35 | 36 | } // namespace tev 37 | -------------------------------------------------------------------------------- /include/tev/imageio/ClipboardImageLoader.h: -------------------------------------------------------------------------------- 1 | /* 2 | * tev -- the EDR viewer 3 | * 4 | * Copyright (C) 2025 Thomas Müller 5 | * 6 | * This program is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | #pragma once 20 | 21 | #include 22 | #include 23 | 24 | #include 25 | 26 | namespace tev { 27 | 28 | class ClipboardImageLoader : public ImageLoader { 29 | public: 30 | Task> 31 | load(std::istream& iStream, const fs::path& path, std::string_view channelSelector, int priority, bool applyGainmaps) const override; 32 | 33 | std::string name() const override { return "clipboard"; } 34 | }; 35 | 36 | } // namespace tev 37 | -------------------------------------------------------------------------------- /include/tev/imageio/UltraHdrImageLoader.h: -------------------------------------------------------------------------------- 1 | /* 2 | * tev -- the EDR viewer 3 | * 4 | * Copyright (C) 2025 Thomas Müller 5 | * 6 | * This program is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | #pragma once 20 | 21 | #include 22 | #include 23 | 24 | #include 25 | 26 | namespace tev { 27 | 28 | class UltraHdrImageLoader : public ImageLoader { 29 | public: 30 | Task> 31 | load(std::istream& iStream, const fs::path& path, std::string_view channelSelector, int priority, bool applyGainmaps) const override; 32 | 33 | std::string name() const override { return "Ultra HDR"; } 34 | }; 35 | 36 | } // namespace tev 37 | -------------------------------------------------------------------------------- /include/tev/imageio/JpegTurboImageLoader.h: -------------------------------------------------------------------------------- 1 | /* 2 | * tev -- the EDR viewer 3 | * 4 | * Copyright (C) 2025 Thomas Müller 5 | * 6 | * This program is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | #pragma once 20 | 21 | #include 22 | #include 23 | 24 | #include 25 | 26 | namespace tev { 27 | 28 | class JpegTurboImageLoader : public ImageLoader { 29 | public: 30 | Task> 31 | load(std::istream& iStream, const fs::path& path, std::string_view channelSelector, int priority, bool applyGainmaps) const override; 32 | 33 | std::string name() const override { return "JPEG Turbo"; } 34 | }; 35 | 36 | } // namespace tev 37 | -------------------------------------------------------------------------------- /include/tev/imageio/ExrImageSaver.h: -------------------------------------------------------------------------------- 1 | /* 2 | * tev -- the EDR viewer 3 | * 4 | * Copyright (C) 2025 Thomas Müller 5 | * 6 | * This program is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | #pragma once 20 | 21 | #include 22 | 23 | #include 24 | 25 | namespace tev { 26 | 27 | class ExrImageSaver : public TypedImageSaver { 28 | public: 29 | void save( 30 | std::ostream& oStream, const fs::path& path, std::span data, const nanogui::Vector2i& imageSize, int nChannels 31 | ) const override; 32 | 33 | bool hasPremultipliedAlpha() const override { return true; } 34 | 35 | virtual bool canSaveFile(std::string_view extension) const override { return toLower(extension) == ".exr"; } 36 | }; 37 | 38 | } // namespace tev 39 | -------------------------------------------------------------------------------- /include/tev/imageio/JxlImageSaver.h: -------------------------------------------------------------------------------- 1 | /* 2 | * tev -- the EDR viewer 3 | * 4 | * Copyright (C) 2025 Thomas Müller 5 | * 6 | * This program is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | #pragma once 20 | 21 | #include 22 | 23 | #include 24 | 25 | namespace tev { 26 | 27 | class JxlImageSaver : public TypedImageSaver { 28 | public: 29 | void save( 30 | std::ostream& oStream, const fs::path& path, std::span data, const nanogui::Vector2i& imageSize, int nChannels 31 | ) const override; 32 | 33 | bool hasPremultipliedAlpha() const override { return true; } 34 | 35 | virtual bool canSaveFile(std::string_view extension) const override { return toLower(extension) == ".jxl"; } 36 | }; 37 | 38 | } // namespace tev 39 | -------------------------------------------------------------------------------- /include/tev/imageio/QoiImageSaver.h: -------------------------------------------------------------------------------- 1 | /* 2 | * tev -- the EDR viewer 3 | * 4 | * Copyright (C) 2025 Thomas Müller 5 | * 6 | * This program is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | #pragma once 20 | 21 | #include 22 | 23 | #include 24 | 25 | namespace tev { 26 | 27 | class QoiImageSaver : public TypedImageSaver { 28 | public: 29 | void save( 30 | std::ostream& oStream, const fs::path& path, std::span data, const nanogui::Vector2i& imageSize, int nChannels 31 | ) const override; 32 | 33 | bool hasPremultipliedAlpha() const override { return false; } 34 | 35 | virtual bool canSaveFile(std::string_view extension) const override { return toLower(extension) == ".qoi"; } 36 | }; 37 | 38 | } // namespace tev 39 | -------------------------------------------------------------------------------- /include/tev/imageio/StbiHdrImageSaver.h: -------------------------------------------------------------------------------- 1 | /* 2 | * tev -- the EDR viewer 3 | * 4 | * Copyright (C) 2025 Thomas Müller 5 | * 6 | * This program is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | #pragma once 20 | 21 | #include 22 | 23 | #include 24 | 25 | namespace tev { 26 | 27 | class StbiHdrImageSaver : public TypedImageSaver { 28 | public: 29 | void save( 30 | std::ostream& oStream, const fs::path& path, std::span data, const nanogui::Vector2i& imageSize, int nChannels 31 | ) const override; 32 | 33 | bool hasPremultipliedAlpha() const override { return false; } 34 | 35 | virtual bool canSaveFile(std::string_view extension) const override { return toLower(extension) == ".hdr"; } 36 | }; 37 | 38 | } // namespace tev 39 | -------------------------------------------------------------------------------- /scripts/create-appimage.cmake: -------------------------------------------------------------------------------- 1 | include(CMakePrintHelpers) 2 | cmake_print_variables(CPACK_TEMPORARY_DIRECTORY) 3 | cmake_print_variables(CPACK_TOPLEVEL_DIRECTORY) 4 | cmake_print_variables(CPACK_PACKAGE_DIRECTORY) 5 | cmake_print_variables(CPACK_PACKAGE_FILE_NAME) 6 | cmake_print_variables(CMAKE_SYSTEM_PROCESSOR) 7 | cmake_print_variables(PROJECT_BINARY_DIR) 8 | 9 | find_program(LINUXDEPLOY_EXECUTABLE 10 | NAMES linuxdeploy linuxdeploy-${CMAKE_SYSTEM_PROCESSOR}.AppImage 11 | PATHS ${CPACK_PACKAGE_DIRECTORY}/dependencies/) 12 | 13 | if (NOT LINUXDEPLOY_EXECUTABLE) 14 | message(Warning "Couldn't build linuxdeploy. Downloading pre-build binary instead.") 15 | set(LINUXDEPLOY_EXECUTABLE ${CPACK_PACKAGE_DIRECTORY}/dependencies/linuxdeploy-${CMAKE_SYSTEM_PROCESSOR}.AppImage) 16 | file(DOWNLOAD 17 | https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-${CMAKE_SYSTEM_PROCESSOR}.AppImage 18 | ${LINUXDEPLOY_EXECUTABLE} 19 | INACTIVITY_TIMEOUT 10 20 | LOG ${CPACK_PACKAGE_DIRECTORY}/linuxdeploy/download.log 21 | STATUS LINUXDEPLOY_DOWNLOAD) 22 | execute_process(COMMAND chmod +x ${LINUXDEPLOY_EXECUTABLE} COMMAND_ECHO STDOUT) 23 | endif() 24 | 25 | execute_process( 26 | COMMAND 27 | ${CMAKE_COMMAND} -E env 28 | OUTPUT=${CPACK_PACKAGE_FILE_NAME}.appimage 29 | VERSION=${CPACK_PACKAGE_VERSION} 30 | ${LINUXDEPLOY_EXECUTABLE} 31 | --appdir=${CPACK_TEMPORARY_DIRECTORY} 32 | --output=appimage 33 | # --verbosity=2 34 | ) -------------------------------------------------------------------------------- /src/imageio/StbiHdrImageSaver.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * tev -- the EDR viewer 3 | * 4 | * Copyright (C) 2025 Thomas Müller 5 | * 6 | * This program is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | #include 20 | 21 | #include 22 | 23 | #include 24 | #include 25 | 26 | using namespace nanogui; 27 | using namespace std; 28 | 29 | namespace tev { 30 | 31 | void StbiHdrImageSaver::save(ostream& oStream, const fs::path&, span data, const Vector2i& imageSize, int nChannels) const { 32 | static const auto stbiOStreamWrite = [](void* context, void* stbidata, int size) { 33 | reinterpret_cast(context)->write(reinterpret_cast(stbidata), size); 34 | }; 35 | 36 | stbi_write_hdr_to_func(stbiOStreamWrite, &oStream, imageSize.x(), imageSize.y(), nChannels, data.data()); 37 | } 38 | 39 | } // namespace tev 40 | -------------------------------------------------------------------------------- /include/tev/HelpWindow.h: -------------------------------------------------------------------------------- 1 | /* 2 | * tev -- the EDR viewer 3 | * 4 | * Copyright (C) 2025 Thomas Müller 5 | * 6 | * This program is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | #pragma once 20 | 21 | #include 22 | #include 23 | 24 | #include 25 | #include 26 | #include 27 | 28 | #include 29 | #include 30 | 31 | namespace tev { 32 | 33 | class HelpWindow : public nanogui::Window { 34 | public: 35 | HelpWindow(nanogui::Widget* parent, std::weak_ptr ipc, std::function closeCallback); 36 | 37 | bool keyboard_event(int key, int scancode, int action, int modifiers) override; 38 | 39 | static std::string COMMAND; 40 | static std::string ALT; 41 | 42 | private: 43 | std::function mCloseCallback; 44 | nanogui::TabWidget* mTabWidget = nullptr; 45 | nanogui::VScrollPanel* mScrollPanel = nullptr; 46 | }; 47 | 48 | } // namespace tev 49 | -------------------------------------------------------------------------------- /include/tev/imageio/StbiLdrImageSaver.h: -------------------------------------------------------------------------------- 1 | /* 2 | * tev -- the EDR viewer 3 | * 4 | * Copyright (C) 2025 Thomas Müller 5 | * 6 | * This program is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | #pragma once 20 | 21 | #include 22 | 23 | #include 24 | 25 | namespace tev { 26 | 27 | class StbiLdrImageSaver : public TypedImageSaver { 28 | public: 29 | void save( 30 | std::ostream& oStream, const fs::path& path, std::span data, const nanogui::Vector2i& imageSize, int nChannels 31 | ) const override; 32 | 33 | bool hasPremultipliedAlpha() const override { return false; } 34 | 35 | virtual bool canSaveFile(std::string_view extension) const override { 36 | std::string lowerExtension = toLower(extension); 37 | return lowerExtension == ".jpg" || lowerExtension == ".jpeg" || lowerExtension == ".png" || lowerExtension == ".bmp" || 38 | lowerExtension == ".tga"; 39 | } 40 | }; 41 | 42 | } // namespace tev 43 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "flake-utils": { 4 | "inputs": { 5 | "systems": "systems" 6 | }, 7 | "locked": { 8 | "lastModified": 1731533236, 9 | "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", 10 | "owner": "numtide", 11 | "repo": "flake-utils", 12 | "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", 13 | "type": "github" 14 | }, 15 | "original": { 16 | "owner": "numtide", 17 | "repo": "flake-utils", 18 | "type": "github" 19 | } 20 | }, 21 | "nixpkgs": { 22 | "locked": { 23 | "lastModified": 1747179050, 24 | "narHash": "sha256-qhFMmDkeJX9KJwr5H32f1r7Prs7XbQWtO0h3V0a0rFY=", 25 | "owner": "NixOS", 26 | "repo": "nixpkgs", 27 | "rev": "adaa24fbf46737f3f1b5497bf64bae750f82942e", 28 | "type": "github" 29 | }, 30 | "original": { 31 | "owner": "NixOS", 32 | "ref": "nixos-unstable", 33 | "repo": "nixpkgs", 34 | "type": "github" 35 | } 36 | }, 37 | "root": { 38 | "inputs": { 39 | "flake-utils": "flake-utils", 40 | "nixpkgs": "nixpkgs" 41 | } 42 | }, 43 | "systems": { 44 | "locked": { 45 | "lastModified": 1681028828, 46 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 47 | "owner": "nix-systems", 48 | "repo": "default", 49 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 50 | "type": "github" 51 | }, 52 | "original": { 53 | "owner": "nix-systems", 54 | "repo": "default", 55 | "type": "github" 56 | } 57 | } 58 | }, 59 | "root": "root", 60 | "version": 7 61 | } 62 | -------------------------------------------------------------------------------- /include/tev/imageio/ImageSaver.h: -------------------------------------------------------------------------------- 1 | /* 2 | * tev -- the EDR viewer 3 | * 4 | * Copyright (C) 2025 Thomas Müller 5 | * 6 | * This program is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | #pragma once 20 | 21 | #include 22 | 23 | #include 24 | 25 | #include 26 | #include 27 | #include 28 | 29 | namespace tev { 30 | 31 | template class TypedImageSaver; 32 | 33 | class ImageSaver { 34 | public: 35 | virtual ~ImageSaver() {} 36 | 37 | virtual bool hasPremultipliedAlpha() const = 0; 38 | 39 | virtual bool canSaveFile(std::string_view extension) const = 0; 40 | bool canSaveFile(const fs::path& path) const { return canSaveFile(std::string_view{toLower(toString(path.extension()))}); } 41 | 42 | static const std::vector>& getSavers(); 43 | }; 44 | 45 | template class TypedImageSaver : public ImageSaver { 46 | public: 47 | virtual void save( 48 | std::ostream& oStream, const fs::path& path, std::span data, const nanogui::Vector2i& imageSize, int nChannels 49 | ) const = 0; 50 | }; 51 | 52 | } // namespace tev 53 | -------------------------------------------------------------------------------- /include/tev/imageio/Exif.h: -------------------------------------------------------------------------------- 1 | /* 2 | * tev -- the EDR viewer 3 | * 4 | * Copyright (C) 2025 Thomas Müller 5 | * 6 | * This program is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | #pragma once 20 | 21 | #include 22 | #include 23 | #include 24 | 25 | #include 26 | #include 27 | #include 28 | 29 | struct _ExifData; 30 | struct _ExifLog; 31 | 32 | namespace tev { 33 | 34 | class Exif { 35 | public: 36 | static constexpr std::array FOURCC = { 37 | 'E', 38 | 'x', 39 | 'i', 40 | 'f', 41 | '\0', 42 | '\0', 43 | }; 44 | 45 | Exif(); 46 | Exif(std::span exifData, bool autoPrependFourcc = true); 47 | ~Exif(); 48 | 49 | void reset(); 50 | 51 | AppleMakerNote tryGetAppleMakerNote() const; 52 | 53 | EOrientation getOrientation() const; 54 | 55 | AttributeNode toAttributes() const; 56 | 57 | private: 58 | bool mReverseEndianess = false; 59 | 60 | _ExifData* mExif = nullptr; 61 | _ExifLog* mExifLog = nullptr; 62 | std::unique_ptr mExifLogError = nullptr; 63 | }; 64 | 65 | } // namespace tev 66 | -------------------------------------------------------------------------------- /resources/macos/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en-US 7 | CFBundleDocumentTypes 8 | 9 | 10 | CFBundleTypeName 11 | Image file 12 | CFBundleTypeRole 13 | Viewer 14 | LSHandlerRank 15 | Default 16 | LSItemContentTypes 17 | 18 | public.image 19 | 20 | 21 | 22 | CFBundleExecutable 23 | tev 24 | CFBundleIconFile 25 | icon.icns 26 | CFBundleIdentifier 27 | org.tom94.tev 28 | CFBundleInfoDictionaryVersion 29 | 6.0 30 | CFBundleName 31 | tev 32 | CFBundlePackageType 33 | APPL 34 | CFBundleSignature 35 | ???? 36 | CFBundleVersion 37 | @MACOSX_BUNDLE_BUNDLE_VERSION@ 38 | CFBundleShortVersionString 39 | @MACOSX_BUNDLE_SHORT_VERSION_STRING@ 40 | CSResourcesFileMapped 41 | 42 | NSHumanReadableCopyright 43 | © 2017-2025 Thomas Müller 44 | NSPrincipalClass 45 | NSApplication 46 | NSHighResolutionCapable 47 | True 48 | @MACOSX_BUNDLE_MIN_OS_VERSION@ 49 | 50 | 51 | -------------------------------------------------------------------------------- /src/WaylandClipboard.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * tev -- the EDR viewer 3 | * 4 | * Copyright (C) 2025 Thomas Müller 5 | * 6 | * This program is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | #include 20 | #include 21 | 22 | #if !defined(__APPLE__) && !defined(_WIN32) 23 | # define GLFW_EXPOSE_NATIVE_WAYLAND 24 | #endif 25 | 26 | #include 27 | #include 28 | 29 | namespace tev { 30 | 31 | void waylandSetClipboardPngImage(const char* data, size_t size) { 32 | if (glfwGetPlatform() != GLFW_PLATFORM_WAYLAND) { 33 | throw std::runtime_error("Wayland clipboard operations are only supported on Wayland."); 34 | } 35 | 36 | #if !defined(__APPLE__) && !defined(_WIN32) 37 | glfwSetWaylandClipboardData(data, "image/png", size); 38 | #endif 39 | } 40 | 41 | const char* waylandGetClipboardPngImage(size_t* size) { 42 | if (glfwGetPlatform() != GLFW_PLATFORM_WAYLAND) { 43 | throw std::runtime_error("Wayland clipboard operations are only supported on Wayland."); 44 | } 45 | 46 | #if !defined(__APPLE__) && !defined(_WIN32) 47 | return glfwGetWaylandClipboardData("image/png", size); 48 | #else 49 | *size = 0; 50 | return nullptr; 51 | #endif 52 | } 53 | 54 | } // namespace tev 55 | -------------------------------------------------------------------------------- /src/imageio/ImageSaver.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * tev -- the EDR viewer 3 | * 4 | * Copyright (C) 2025 Thomas Müller 5 | * 6 | * This program is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | #include 20 | 21 | #include 22 | #include 23 | #include 24 | #include 25 | 26 | #ifdef TEV_SUPPORT_JXL 27 | # include 28 | #endif 29 | 30 | #include 31 | 32 | using namespace std; 33 | 34 | namespace tev { 35 | 36 | const vector>& ImageSaver::getSavers() { 37 | auto makeSavers = [] { 38 | vector> imageSavers; 39 | imageSavers.emplace_back(make_unique()); 40 | imageSavers.emplace_back(make_unique()); 41 | #ifdef TEV_SUPPORT_JXL 42 | imageSavers.emplace_back(make_unique()); 43 | #endif 44 | imageSavers.emplace_back(make_unique()); 45 | imageSavers.emplace_back(make_unique()); 46 | return imageSavers; 47 | }; 48 | 49 | static const vector imageSavers = makeSavers(); 50 | return imageSavers; 51 | } 52 | 53 | } // namespace tev 54 | -------------------------------------------------------------------------------- /src/Task.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * tev -- the EDR viewer 3 | * 4 | * Copyright (C) 2025 Thomas Müller 5 | * 6 | * This program is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | #include 20 | 21 | namespace tev { 22 | 23 | void waitAll(std::span> futures) { 24 | std::exception_ptr eptr = {}; 25 | 26 | for (auto&& f : futures) { 27 | try { 28 | f.get(); 29 | } catch (const std::exception& e) { 30 | if (eptr) { 31 | tlog::error() << "Multiple exceptions in waitAll(). Rethrowing first and logging others: " << e.what(); 32 | } else { 33 | eptr = std::current_exception(); 34 | } 35 | } 36 | } 37 | 38 | if (eptr) { 39 | std::rethrow_exception(eptr); 40 | } 41 | } 42 | 43 | Task awaitAll(std::span> futures) { 44 | std::exception_ptr eptr = {}; 45 | 46 | for (auto&& f : futures) { 47 | try { 48 | co_await f; 49 | } catch (const std::exception& e) { 50 | if (eptr) { 51 | tlog::error() << "Multiple exceptions in awaitAll(). Rethrowing first and logging others: " << e.what(); 52 | } else { 53 | eptr = std::current_exception(); 54 | } 55 | } 56 | } 57 | 58 | if (eptr) { 59 | std::rethrow_exception(eptr); 60 | } 61 | } 62 | 63 | } // namespace tev 64 | -------------------------------------------------------------------------------- /src/imageio/QoiImageSaver.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * tev -- the EDR viewer 3 | * 4 | * Copyright (C) 2025 Thomas Müller 5 | * 6 | * This program is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | #include 20 | 21 | #define QOI_NO_STDIO 22 | #include 23 | 24 | #include 25 | #include 26 | 27 | using namespace nanogui; 28 | using namespace std; 29 | 30 | namespace tev { 31 | 32 | void QoiImageSaver::save(ostream& oStream, const fs::path&, span data, const Vector2i& imageSize, int nChannels) const { 33 | // The QOI image format expects nChannels to be either 3 for RGB data or 4 for RGBA. 34 | if (nChannels != 4 && nChannels != 3) { 35 | throw ImageSaveError{fmt::format("Invalid number of channels {}.", nChannels)}; 36 | } 37 | 38 | const qoi_desc desc{ 39 | .width = static_cast(imageSize.x()), 40 | .height = static_cast(imageSize.y()), 41 | .channels = static_cast(nChannels), 42 | .colorspace = QOI_SRGB, 43 | }; 44 | int sizeInBytes = 0; 45 | void* encodedData = qoi_encode(data.data(), &desc, &sizeInBytes); 46 | 47 | ScopeGuard encodedDataGuard{[encodedData] { free(encodedData); }}; 48 | 49 | if (!encodedData) { 50 | throw ImageSaveError{"Failed to encode data into the QOI format."}; 51 | } 52 | 53 | oStream.write(reinterpret_cast(encodedData), sizeInBytes); 54 | } 55 | 56 | } // namespace tev 57 | -------------------------------------------------------------------------------- /src/imageio/StbiLdrImageSaver.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * tev -- the EDR viewer 3 | * 4 | * Copyright (C) 2025 Thomas Müller 5 | * 6 | * This program is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | #include 20 | 21 | #define STB_IMAGE_WRITE_IMPLEMENTATION 22 | #include 23 | 24 | #include 25 | #include 26 | 27 | using namespace nanogui; 28 | using namespace std; 29 | 30 | namespace tev { 31 | 32 | void StbiLdrImageSaver::save(ostream& oStream, const fs::path& path, span data, const Vector2i& imageSize, int nChannels) const { 33 | static const auto stbiOStreamWrite = [](void* context, void* stbidata, int size) { 34 | reinterpret_cast(context)->write(reinterpret_cast(stbidata), size); 35 | }; 36 | 37 | auto extension = toLower(toString(path.extension())); 38 | 39 | if (extension == ".jpg" || extension == ".jpeg") { 40 | stbi_write_jpg_to_func(stbiOStreamWrite, &oStream, imageSize.x(), imageSize.y(), nChannels, data.data(), 100); 41 | } else if (extension == ".png") { 42 | stbi_write_png_to_func(stbiOStreamWrite, &oStream, imageSize.x(), imageSize.y(), nChannels, data.data(), 0); 43 | } else if (extension == ".bmp") { 44 | stbi_write_bmp_to_func(stbiOStreamWrite, &oStream, imageSize.x(), imageSize.y(), nChannels, data.data()); 45 | } else if (extension == ".tga") { 46 | stbi_write_tga_to_func(stbiOStreamWrite, &oStream, imageSize.x(), imageSize.y(), nChannels, data.data()); 47 | } else { 48 | throw ImageSaveError{fmt::format("Image {} has unknown format.", path)}; 49 | } 50 | } 51 | 52 | } // namespace tev 53 | -------------------------------------------------------------------------------- /resources/linux/tev.metainfo.xml.in: -------------------------------------------------------------------------------- 1 | 2 | 3 | @TEV_LINUX_APP_ID@ 4 | tev 5 | High dynamic range image viewer 6 | CC0-1.0 7 | GPL-3.0-or-later 8 | 9 | Thomas Müller 10 | 11 | 12 | 13 |

High dynamic range (HDR) image viewer for people who care about colors. It is:

14 |
    15 |
  • Lightning fast: starts up instantly, loads hundreds of images in seconds.
  • 16 |
  • Accurate: understands HDR and color profiles (ICC, CICP, etc.). Displays HDR on all operating systems.
  • 17 |
  • Versatile: supports many file formats, histograms, pixel-peeping, tonemaps, error metrics, etc.
  • 18 |
19 |
20 | 21 | keyboard 22 | pointing 23 | 24 | 25 | 26 | 768 27 | 28 | 29 | 30 | @TEV_METAINFO_RELEASES@ 31 | 32 | 33 | 34 | 35 | 36 | https://raw.githubusercontent.com/Tom94/tev/refs/heads/master/resources/linux/screenshot.png 37 | Two images compared in false color. 38 | 39 | 40 | 41 | https://github.com/Tom94/tev 42 | https://github.com/Tom94/tev/issues 43 | https://github.com/Tom94/tev 44 | 45 | @TEV_LINUX_APP_ID@.desktop 46 | 47 | tev 48 | 49 | 50 | 51 | #e89df4 52 | #854091 53 | 54 |
55 | -------------------------------------------------------------------------------- /include/tev/SharedQueue.h: -------------------------------------------------------------------------------- 1 | /* 2 | * tev -- the EDR viewer 3 | * 4 | * Copyright (C) 2025 Thomas Müller 5 | * 6 | * This program is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | #pragma once 20 | 21 | #include 22 | 23 | #include 24 | #include 25 | #include 26 | #include 27 | 28 | namespace tev { 29 | 30 | template class SharedQueue { 31 | public: 32 | bool empty() const { 33 | std::lock_guard lock{mMutex}; 34 | return mRawQueue.empty(); 35 | } 36 | 37 | size_t size() const { 38 | std::lock_guard lock{mMutex}; 39 | return mRawQueue.size(); 40 | } 41 | 42 | void push(T newElem) { 43 | std::lock_guard lock{mMutex}; 44 | mRawQueue.push_back(newElem); 45 | mDataCondition.notify_one(); 46 | } 47 | 48 | T waitAndPop() { 49 | std::unique_lock lock{mMutex}; 50 | 51 | while (mRawQueue.empty()) { 52 | mDataCondition.wait(lock); 53 | } 54 | 55 | T result = std::move(mRawQueue.front()); 56 | mRawQueue.pop_front(); 57 | 58 | return result; 59 | } 60 | 61 | std::optional tryPop() { 62 | std::unique_lock lock{mMutex}; 63 | 64 | if (mRawQueue.empty()) { 65 | return {}; 66 | } 67 | 68 | T result = std::move(mRawQueue.front()); 69 | mRawQueue.pop_front(); 70 | 71 | return result; 72 | } 73 | 74 | // Only call while holding the mutex! 75 | const T& front() const { 76 | std::unique_lock lock{mMutex}; 77 | return mRawQueue.front(); 78 | } 79 | 80 | private: 81 | std::deque mRawQueue; 82 | mutable std::mutex mMutex; 83 | std::condition_variable mDataCondition; 84 | }; 85 | 86 | } // namespace tev 87 | -------------------------------------------------------------------------------- /src/imageio/EmptyImageLoader.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * tev -- the EDR viewer 3 | * 4 | * Copyright (C) 2025 Thomas Müller 5 | * 6 | * This program is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | #include 20 | 21 | #include 22 | 23 | using namespace nanogui; 24 | using namespace std; 25 | 26 | namespace tev { 27 | 28 | Task> EmptyImageLoader::load(istream& iStream, const fs::path&, string_view, int, bool) const { 29 | char magic[6]; 30 | iStream.read(magic, 6); 31 | string magicString(magic, 6); 32 | 33 | if (!iStream || magicString != "empty ") { 34 | throw FormatNotSupported{fmt::format("Invalid magic empty string {}.", magic)}; 35 | } 36 | 37 | Vector2i size; 38 | int nChannels; 39 | iStream >> size.x() >> size.y() >> nChannels; 40 | 41 | auto numPixels = (size_t)size.x() * size.y(); 42 | if (numPixels == 0) { 43 | throw ImageLoadError{"Image has zero pixels."}; 44 | } 45 | 46 | vector result(1); 47 | ImageData& data = result.front(); 48 | 49 | for (int i = 0; i < nChannels; ++i) { 50 | // The following lines decode strings by prefix length. The reason for using sthis encoding is to allow arbitrary characters, 51 | // including whitespaces, in the channel names. 52 | std::vector channelNameData; 53 | int length; 54 | iStream >> length; 55 | channelNameData.resize(length + 1, 0); 56 | iStream.read(channelNameData.data(), length); 57 | 58 | string channelName = channelNameData.data(); 59 | 60 | data.channels.emplace_back(Channel{channelName, size, EPixelFormat::F32, EPixelFormat::F32}).setZero(); 61 | } 62 | 63 | data.hasPremultipliedAlpha = true; 64 | 65 | co_return result; 66 | } 67 | 68 | } // namespace tev 69 | -------------------------------------------------------------------------------- /include/tev/ImageInfoWindow.h: -------------------------------------------------------------------------------- 1 | /* 2 | * tev -- the EDR viewer 3 | * 4 | * Copyright (C) 2025 Thomas Müller 5 | * 6 | * This program is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | #pragma once 20 | 21 | #include 22 | 23 | #include 24 | #include 25 | #include 26 | 27 | #include 28 | #include 29 | 30 | #include 31 | 32 | namespace tev { 33 | 34 | class ImageInfoWindow : public nanogui::Window { 35 | public: 36 | ImageInfoWindow(nanogui::Widget* parent, const std::shared_ptr& image, std::function closeCallback); 37 | 38 | bool keyboard_event(int key, int scancode, int action, int modifiers) override; 39 | 40 | static std::string COMMAND; 41 | static std::string ALT; 42 | 43 | std::string_view currentTabName() const { 44 | return mTabWidget && mTabWidget->tab_count() > 0 ? mTabWidget->tab_caption(mTabWidget->selected_id()) : std::string_view{}; 45 | } 46 | 47 | bool selectTabWithName(const std::string_view name) { 48 | if (mTabWidget) { 49 | for (int i = 0; i < mTabWidget->tab_count(); ++i) { 50 | if (mTabWidget->tab_caption(mTabWidget->tab_id(i)) == name) { 51 | mTabWidget->set_selected_index(i); 52 | return true; 53 | } 54 | } 55 | } 56 | 57 | return false; 58 | } 59 | 60 | float currentScroll() const { return mScrollPanel ? mScrollPanel->scroll() : 0.0f; } 61 | void setScroll(const float scroll) { 62 | if (mScrollPanel) { 63 | mScrollPanel->set_scroll(scroll); 64 | } 65 | } 66 | 67 | private: 68 | std::function mCloseCallback; 69 | nanogui::TabWidget* mTabWidget = nullptr; 70 | nanogui::VScrollPanel* mScrollPanel = nullptr; 71 | }; 72 | 73 | } // namespace tev 74 | -------------------------------------------------------------------------------- /.clang-format: -------------------------------------------------------------------------------- 1 | AccessModifierOffset: -4 2 | AlignAfterOpenBracket: BlockIndent 3 | AlignArrayOfStructures: Left 4 | AlignConsecutiveBitFields: Consecutive 5 | AlignConsecutiveShortCaseStatements: 6 | Enabled: false 7 | AlignEscapedNewlines: Left 8 | AlignOperands: false 9 | AlignTrailingComments: 10 | Kind: Leave 11 | AllowAllArgumentsOnNextLine: true 12 | AllowAllParametersOfDeclarationOnNextLine: true 13 | AllowShortBlocksOnASingleLine: Always 14 | AllowShortCaseExpressionOnASingleLine: true 15 | AllowShortCaseLabelsOnASingleLine: true 16 | AllowShortCompoundRequirementOnASingleLine: true 17 | AllowShortEnumsOnASingleLine: true 18 | AllowShortFunctionsOnASingleLine: true 19 | AllowShortLambdasOnASingleLine: true 20 | AllowShortIfStatementsOnASingleLine: Never 21 | AllowShortLoopsOnASingleLine: false 22 | AlwaysBreakBeforeMultilineStrings: true 23 | BinPackArguments: false 24 | BinPackParameters: false 25 | BreakAfterAttributes: Leave 26 | BreakAfterReturnType: Automatic 27 | BreakBeforeBraces: Attach 28 | BreakBeforeConceptDeclarations: Allowed 29 | # BreakBeforeTemplateCloser: true 30 | BreakBeforeTernaryOperators: false 31 | BreakConstructorInitializers: AfterColon 32 | BreakInheritanceList: AfterColon 33 | BreakStringLiterals: false 34 | BreakTemplateDeclarations: MultiLine 35 | ColumnLimit: 140 36 | CompactNamespaces: true 37 | ConstructorInitializerAllOnOneLineOrOnePerLine: true 38 | ConstructorInitializerIndentWidth: 4 39 | ContinuationIndentWidth: 4 40 | Cpp11BracedListStyle: true 41 | EmptyLineAfterAccessModifier: Never 42 | EmptyLineBeforeAccessModifier: Always 43 | IncludeBlocks: Preserve 44 | IndentCaseLabels: true 45 | IndentPPDirectives: AfterHash 46 | IndentWidth: 4 47 | IndentWrappedFunctionNames: true 48 | InsertBraces: true 49 | InsertNewlineAtEOF: true 50 | InsertTrailingCommas: Wrapped 51 | MaxEmptyLinesToKeep: 1 52 | PackConstructorInitializers: NextLine 53 | PenaltyBreakAssignment: 65 54 | PenaltyBreakBeforeFirstCallParameter: 16 55 | PenaltyBreakComment: 320 56 | PenaltyBreakFirstLessLess: 50 57 | PenaltyBreakString: 0 58 | PenaltyExcessCharacter: 10 59 | PenaltyReturnTypeOnItsOwnLine: 100 60 | PointerAlignment: Left 61 | QualifierAlignment: Left 62 | ReferenceAlignment: Left 63 | SortIncludes: CaseSensitive 64 | SortUsingDeclarations: true 65 | SpaceAfterCStyleCast: false 66 | SpaceAfterLogicalNot: false 67 | SpaceAfterTemplateKeyword: true 68 | SpaceAroundPointerQualifiers: Both 69 | SpaceBeforeParens: ControlStatementsExceptForEachMacros 70 | SpacesInAngles: Never 71 | TabWidth: 4 72 | UseCRLF: false 73 | UseTab: Never 74 | -------------------------------------------------------------------------------- /include/tev/UberShader.h: -------------------------------------------------------------------------------- 1 | /* 2 | * tev -- the EDR viewer 3 | * 4 | * Copyright (C) 2025 Thomas Müller 5 | * 6 | * This program is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | #pragma once 20 | 21 | #include 22 | #include 23 | 24 | #include 25 | #include 26 | #include 27 | 28 | #include 29 | #include 30 | 31 | namespace tev { 32 | 33 | class UberShader { 34 | public: 35 | UberShader(nanogui::RenderPass* renderPass, float ditherScale); 36 | virtual ~UberShader(); 37 | 38 | void draw( 39 | const nanogui::Vector2f& pixelSize, 40 | const nanogui::Vector2f& checkerSize, 41 | Image* textureImage, 42 | const nanogui::Matrix3f& transformImage, 43 | Image* textureReference, 44 | const nanogui::Matrix3f& transformReference, 45 | std::string_view requestedChannelGroup, 46 | EInterpolationMode minFilter, 47 | EInterpolationMode magFilter, 48 | float exposure, 49 | float offset, 50 | float gamma, 51 | float colorMultiplier, 52 | bool clipToLdr, 53 | const nanogui::Color& backgroundColor, 54 | ETonemap tonemap, 55 | EMetric metric, 56 | const std::optional& crop 57 | ); 58 | 59 | private: 60 | void bindCheckerboardData(const nanogui::Vector2f& pixelSize, const nanogui::Vector2f& checkerSize, const nanogui::Color& backgroundColor); 61 | 62 | void bindImageData( 63 | nanogui::Texture* textureImage, const nanogui::Matrix3f& transformImage, float exposure, float offset, float gamma, ETonemap tonemap 64 | ); 65 | 66 | void bindReferenceData(nanogui::Texture* textureReference, const nanogui::Matrix3f& transformReference, EMetric metric); 67 | 68 | nanogui::ref mShader; 69 | nanogui::ref mColorMap; 70 | nanogui::ref mDitherMatrix; 71 | }; 72 | 73 | } // namespace tev 74 | -------------------------------------------------------------------------------- /package.nix: -------------------------------------------------------------------------------- 1 | { lib 2 | , stdenv 3 | , cmake 4 | , dbus 5 | , fetchFromGitHub 6 | , lcms2 7 | , libGL 8 | , libffi 9 | , libxkbcommon 10 | , nasm 11 | , ninja 12 | , perl 13 | , pkg-config 14 | , wayland 15 | , wayland-protocols 16 | , wayland-scanner 17 | , xorg 18 | , 19 | }: 20 | 21 | stdenv.mkDerivation rec { 22 | pname = "tev"; 23 | 24 | # Extract version from CMakeLists.txt 25 | version = with builtins; 26 | let 27 | cmakeContents = readFile ./CMakeLists.txt; 28 | versionMatch = match ".*VERSION[[:space:]]+([0-9]+\\.[0-9]+\\.[0-9]+).*" cmakeContents; 29 | in 30 | if versionMatch == null then throw "Could not find version in CMakeLists.txt" 31 | else head versionMatch; 32 | 33 | src = ./.; 34 | 35 | postPatch = lib.optionalString stdenv.hostPlatform.isLinux ( 36 | let 37 | waylandLibPath = "${lib.getLib wayland}/lib"; 38 | in 39 | '' 40 | substituteInPlace ./dependencies/nanogui/ext/glfw/src/wl_init.c \ 41 | --replace-fail "libwayland-client.so" "${waylandLibPath}/libwayland-client.so" \ 42 | --replace-fail "libwayland-cursor.so" "${waylandLibPath}/libwayland-cursor.so" \ 43 | --replace-fail "libwayland-egl.so" "${waylandLibPath}/libwayland-egl.so" \ 44 | --replace-fail "libxkbcommon.so" "${lib.getLib libxkbcommon}/lib/libxkbcommon.so" 45 | '' 46 | ); 47 | 48 | nativeBuildInputs = [ 49 | cmake 50 | nasm 51 | ninja 52 | perl 53 | pkg-config 54 | ]; 55 | 56 | buildInputs = [ 57 | lcms2 58 | ] ++ lib.optionals stdenv.hostPlatform.isLinux ( 59 | [ 60 | dbus 61 | libffi 62 | libGL 63 | libxkbcommon 64 | wayland 65 | wayland-protocols 66 | wayland-scanner 67 | ] ++ (with xorg; [ 68 | libX11 69 | libXcursor 70 | libXi 71 | libXinerama 72 | libXrandr 73 | ]) 74 | ); 75 | 76 | cmakeFlags = [ 77 | "-DTEV_DEPLOY=1" 78 | ]; 79 | 80 | meta = { 81 | description = "High dynamic range (HDR) image viewer for people who care about colors"; 82 | mainProgram = "tev"; 83 | longDescription = '' 84 | High dynamic range (HDR) image viewer for people who care about colors. It is 85 | - Lightning fast: starts up instantly, loads hundreds of images in seconds. 86 | - Accurate: understands color profiles and displays HDR. 87 | - Versatile: supports many formats, histograms, pixel peeping, tonemaps, etc. 88 | ''; 89 | changelog = "https://github.com/Tom94/tev/releases/tag/v${version}"; 90 | homepage = "https://github.com/Tom94/tev"; 91 | license = lib.licenses.gpl3; 92 | maintainers = with lib.maintainers; [ tom94 ]; 93 | platforms = lib.platforms.unix; 94 | }; 95 | } 96 | -------------------------------------------------------------------------------- /src/imageio/QoiImageLoader.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * tev -- the EDR viewer 3 | * 4 | * Copyright (C) 2025 Thomas Müller 5 | * 6 | * This program is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | #include 20 | #include 21 | 22 | #define QOI_NO_STDIO 23 | #define QOI_IMPLEMENTATION 24 | #include 25 | 26 | using namespace nanogui; 27 | using namespace std; 28 | 29 | namespace tev { 30 | 31 | Task> QoiImageLoader::load(istream& iStream, const fs::path&, string_view, int priority, bool) const { 32 | char magic[4]; 33 | iStream.read(magic, 4); 34 | string magicString(magic, 4); 35 | 36 | if (magicString != "qoif") { 37 | throw FormatNotSupported{fmt::format("Invalid magic QOI string {}.", magicString)}; 38 | } 39 | 40 | iStream.clear(); 41 | iStream.seekg(0, iStream.end); 42 | size_t dataSize = iStream.tellg(); 43 | iStream.seekg(0, iStream.beg); 44 | vector data(dataSize); 45 | iStream.read(data.data(), dataSize); 46 | 47 | qoi_desc desc; 48 | void* decodedData = qoi_decode(data.data(), static_cast(dataSize), &desc, 0); 49 | 50 | ScopeGuard decodedDataGuard{[decodedData] { free(decodedData); }}; 51 | 52 | if (!decodedData) { 53 | throw ImageLoadError{"Failed to decode data from the QOI format."}; 54 | } 55 | 56 | Vector2i size{static_cast(desc.width), static_cast(desc.height)}; 57 | auto numPixels = (size_t)size.x() * size.y(); 58 | if (numPixels == 0) { 59 | throw ImageLoadError{"Image has zero pixels."}; 60 | } 61 | 62 | int numChannels = static_cast(desc.channels); 63 | if (numChannels != 4 && numChannels != 3) { 64 | throw ImageLoadError{fmt::format("Invalid number of channels {}.", numChannels)}; 65 | } 66 | 67 | vector result(1); 68 | ImageData& resultData = result.front(); 69 | 70 | // QOI images are 8 bit per pixel which easily fits into F16. 71 | resultData.channels = makeRgbaInterleavedChannels(numChannels, numChannels == 4, size, EPixelFormat::F32, EPixelFormat::F16); 72 | resultData.hasPremultipliedAlpha = false; 73 | 74 | if (desc.colorspace == QOI_LINEAR) { 75 | co_await toFloat32( 76 | (uint8_t*)decodedData, numChannels, resultData.channels.front().floatData(), 4, size, numChannels == 4, priority 77 | ); 78 | } else { 79 | co_await toFloat32( 80 | (uint8_t*)decodedData, numChannels, resultData.channels.front().floatData(), 4, size, numChannels == 4, priority 81 | ); 82 | } 83 | 84 | co_return result; 85 | } 86 | 87 | } // namespace tev 88 | -------------------------------------------------------------------------------- /include/tev/MultiGraph.h: -------------------------------------------------------------------------------- 1 | /* 2 | * tev -- the EDR viewer 3 | * 4 | * Copyright (C) 2025 Thomas Müller 5 | * 6 | * This program is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | // This file was adapted from the nanogui::Graph class, which was developed 20 | // by Wenzel Jakob and based on the NanoVG demo application 21 | // by Mikko Mononen. Modifications were developed by Thomas Müller . 22 | 23 | #pragma once 24 | 25 | #include 26 | 27 | #include 28 | 29 | #include 30 | 31 | namespace tev { 32 | 33 | class MultiGraph : public nanogui::Widget { 34 | public: 35 | MultiGraph(nanogui::Widget* parent, std::string_view caption = "Untitled"); 36 | 37 | std::string_view caption() const { return mCaption; } 38 | void setCaption(std::string_view caption) { mCaption = caption; } 39 | 40 | std::string_view header() const { return mHeader; } 41 | void setHeader(std::string_view header) { mHeader = header; } 42 | 43 | std::string_view footer() const { return mFooter; } 44 | void setFooter(std::string_view footer) { mFooter = footer; } 45 | 46 | const nanogui::Color& backgroundColor() const { return mBackgroundColor; } 47 | void setBackgroundColor(const nanogui::Color& backgroundColor) { mBackgroundColor = backgroundColor; } 48 | 49 | const nanogui::Color& foregroundColor() const { return mForegroundColor; } 50 | void setForegroundColor(const nanogui::Color& foregroundColor) { mForegroundColor = foregroundColor; } 51 | 52 | const nanogui::Color& textColor() const { return mTextColor; } 53 | void setTextColor(const nanogui::Color& textColor) { mTextColor = textColor; } 54 | 55 | std::span values() const { return mValues; } 56 | void setValues(std::span values) { mValues = {values.begin(), values.end()}; } 57 | 58 | std::span colors() { return mColors; } 59 | void setColors(std::span colors) { mColors = {colors.begin(), colors.end()}; } 60 | 61 | void setNChannels(int nChannels) { mNChannels = nChannels; } 62 | 63 | virtual nanogui::Vector2i preferred_size_impl(NVGcontext* ctx) const override; 64 | virtual void draw(NVGcontext* ctx) override; 65 | 66 | void setMinimum(float minimum) { mMinimum = minimum; } 67 | 68 | void setMean(float mean) { mMean = mean; } 69 | 70 | void setMaximum(float maximum) { mMaximum = maximum; } 71 | 72 | void setZero(int zeroBin) { mZeroBin = zeroBin; } 73 | 74 | protected: 75 | std::string mCaption, mHeader, mFooter; 76 | nanogui::Color mBackgroundColor, mForegroundColor, mTextColor; 77 | std::vector mValues; 78 | std::vector mColors; 79 | int mNChannels = 1; 80 | float mMinimum = 0, mMean = 0, mMaximum = 0; 81 | int mZeroBin = 0; 82 | }; 83 | 84 | } // namespace tev 85 | -------------------------------------------------------------------------------- /src/imageio/ExrImageSaver.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * tev -- the EDR viewer 3 | * 4 | * Copyright (C) 2025 Thomas Müller 5 | * 6 | * This program is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | #include 20 | 21 | #include 22 | #include 23 | #include 24 | #include 25 | #include 26 | #include 27 | #include 28 | 29 | #include 30 | #include 31 | #include 32 | #include 33 | 34 | using namespace nanogui; 35 | using namespace std; 36 | 37 | namespace tev { 38 | 39 | class StdOStream : public Imf::OStream { 40 | public: 41 | StdOStream(ostream& stream, const char fileName[]) : Imf::OStream{fileName}, mStream{stream} {} 42 | 43 | void write(const char c[/*n*/], int n) { 44 | clearError(); 45 | mStream.write(c, n); 46 | checkError(mStream); 47 | } 48 | 49 | uint64_t tellp() { return std::streamoff(mStream.tellp()); } 50 | 51 | void seekp(uint64_t pos) { 52 | mStream.seekp(pos); 53 | checkError(mStream); 54 | } 55 | 56 | private: 57 | // The following error-checking functions were copy&pasted from the OpenEXR source code 58 | static void clearError() { errno = 0; } 59 | 60 | static void checkError(ostream& os) { 61 | if (!os) { 62 | if (errno) { 63 | IEX_NAMESPACE::throwErrnoExc(); 64 | } 65 | 66 | throw IEX_NAMESPACE::ErrnoExc("File output failed."); 67 | } 68 | } 69 | 70 | ostream& mStream; 71 | }; 72 | 73 | void ExrImageSaver::save(ostream& oStream, const fs::path& path, span data, const Vector2i& imageSize, int nChannels) const { 74 | vector channelNames = { 75 | "R", 76 | "G", 77 | "B", 78 | "A", 79 | }; 80 | 81 | if (nChannels <= 0 || nChannels > 4) { 82 | throw ImageSaveError{fmt::format("Invalid number of channels {}.", nChannels)}; 83 | } 84 | 85 | Imf::Header header{imageSize.x(), imageSize.y()}; 86 | Imf::FrameBuffer frameBuffer; 87 | 88 | for (int i = 0; i < nChannels; ++i) { 89 | header.channels().insert(channelNames[i], Imf::Channel(Imf::FLOAT)); 90 | frameBuffer.insert( 91 | channelNames[i], 92 | Imf::Slice( 93 | Imf::FLOAT, // Type 94 | (char*)(data.data() + i), // Base pointer 95 | sizeof(float) * nChannels, // x-stride in bytes 96 | sizeof(float) * imageSize.x() * nChannels // y-stride in bytes 97 | ) 98 | ); 99 | } 100 | 101 | StdOStream imfOStream{oStream, toString(path).c_str()}; 102 | Imf::OutputFile file{imfOStream, header}; 103 | file.setFrameBuffer(frameBuffer); 104 | file.writePixels(imageSize.y()); 105 | } 106 | 107 | } // namespace tev 108 | -------------------------------------------------------------------------------- /src/imageio/GainMap.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * tev -- the EDR viewer 3 | * 4 | * Copyright (C) 2025 Thomas Müller 5 | * 6 | * This program is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | #include 20 | #include 21 | #include 22 | 23 | using namespace nanogui; 24 | using namespace std; 25 | 26 | namespace tev { 27 | 28 | Task applyAppleGainMap(ImageData& image, const ImageData& gainMap, int priority, const AppleMakerNote* amn) { 29 | auto size = image.channels[0].size(); 30 | TEV_ASSERT(size == gainMap.channels[0].size(), "Image and gain map must have the same size."); 31 | 32 | // Apply gain map per https://developer.apple.com/documentation/appkit/applying-apple-hdr-effect-to-your-photos 33 | 34 | // 0.0 and 8.0 result in the weakest effect. They are a sane default; see https://developer.apple.com/forums/thread/709331 35 | float maker33 = 0.0f; 36 | float maker48 = 8.0f; 37 | 38 | if (amn) { 39 | maker33 = amn->tryGetFloat(33, maker33); 40 | maker48 = amn->tryGetFloat(48, maker48); 41 | } 42 | 43 | float stops; 44 | if (maker33 < 1.0f) { 45 | if (maker48 <= 0.01f) { 46 | stops = -20.0f * maker48 + 1.8f; 47 | } else { 48 | stops = -0.101f * maker48 + 1.601f; 49 | } 50 | } else { 51 | if (maker48 <= 0.01f) { 52 | stops = -70.0f * maker48 + 3.0f; 53 | } else { 54 | stops = -0.303f * maker48 + 2.303f; 55 | } 56 | } 57 | 58 | float headroom = pow(2.0f, std::max(stops, 0.0f)); 59 | tlog::debug() << fmt::format("Derived gain map headroom {} from maker note entries #33={} and #48={}.", headroom, maker33, maker48); 60 | 61 | const int numImageChannels = (int)image.channels.size(); 62 | const int numGainMapChannels = (int)gainMap.channels.size(); 63 | 64 | int alphaChannelIndex = -1; 65 | for (int c = 0; c < numImageChannels; ++c) { 66 | bool isAlpha = Channel::isAlpha(image.channels[c].name()); 67 | if (isAlpha) { 68 | if (alphaChannelIndex != -1) { 69 | tlog::warning() 70 | << fmt::format("Image has multiple alpha channels, using the first one: {}", image.channels[alphaChannelIndex].name()); 71 | continue; 72 | } 73 | 74 | alphaChannelIndex = c; 75 | } 76 | } 77 | 78 | const size_t numPixels = (size_t)size.x() * size.y(); 79 | co_await ThreadPool::global().parallelForAsync( 80 | 0, 81 | numPixels, 82 | [&](size_t i) { 83 | for (int c = 0; c < numImageChannels; ++c) { 84 | if (c == alphaChannelIndex) { 85 | continue; 86 | } 87 | 88 | const int gainmapChannel = std::min(c, numGainMapChannels - 1); 89 | image.channels[c].setAt(i, image.channels[c].at(i) * (1.0f + (headroom - 1.0f) * gainMap.channels[gainmapChannel].at(i))); 90 | } 91 | }, 92 | priority 93 | ); 94 | 95 | co_return; 96 | } 97 | 98 | } // namespace tev 99 | -------------------------------------------------------------------------------- /include/tev/Lazy.h: -------------------------------------------------------------------------------- 1 | /* 2 | * tev -- the EDR viewer 3 | * 4 | * Copyright (C) 2025 Thomas Müller 5 | * 6 | * This program is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | #pragma once 20 | 21 | #include 22 | #include 23 | 24 | #include 25 | #include 26 | #include 27 | 28 | namespace tev { 29 | 30 | // Encapsulates a lazy, potentially asynchronous computation of some value. The public interface of this object is not thread-safe, i.e. it 31 | // is expected to never be used from multiple threads at once. 32 | template class Lazy { 33 | public: 34 | Lazy(std::function compute) : Lazy{compute, nullptr} {} 35 | 36 | Lazy(std::function compute, ThreadPool* threadPool) : mThreadPool{threadPool}, mCompute{compute} {} 37 | 38 | Lazy(std::future&& future) : mAsyncValue{std::move(future)} {} 39 | 40 | T get() { 41 | if (mIsComputed) { 42 | return mValue; 43 | } 44 | 45 | if (mAsyncValue.valid()) { 46 | mValue = mAsyncValue.get(); 47 | } else { 48 | mValue = compute(); 49 | } 50 | 51 | mIsComputed = true; 52 | mBecameReadyAt = std::chrono::steady_clock::now(); 53 | return mValue; 54 | } 55 | 56 | bool isReady() const { 57 | if (mIsComputed) { 58 | TEV_ASSERT(!mAsyncValue.valid(), "There should never be a background computation while the result is already available."); 59 | 60 | return true; 61 | } 62 | 63 | if (!mAsyncValue.valid()) { 64 | return false; 65 | } 66 | 67 | return mAsyncValue.wait_for(std::chrono::seconds{0}) == std::future_status::ready; 68 | } 69 | 70 | std::chrono::steady_clock::time_point becameReadyAt() const { 71 | if (!isReady()) { 72 | return std::chrono::steady_clock::now(); 73 | } else { 74 | return mBecameReadyAt; 75 | } 76 | } 77 | 78 | void computeAsync(int priority) { 79 | // No need to perform an async computation if we already computed the value before or if one is already running. 80 | if (mAsyncValue.valid() || mIsComputed) { 81 | return; 82 | } 83 | 84 | if (mThreadPool) { 85 | mAsyncValue = mThreadPool->enqueueTask([this]() { return compute(); }, priority); 86 | } else { 87 | mAsyncValue = std::async(std::launch::async, [this]() { return compute(); }); 88 | } 89 | } 90 | 91 | private: 92 | T compute() { 93 | T result = mCompute(); 94 | mCompute = std::function{}; 95 | return result; 96 | } 97 | 98 | // If this thread pool is present, use it to run tasks instead of std::async. 99 | ThreadPool* mThreadPool = nullptr; 100 | 101 | std::function mCompute; 102 | std::future mAsyncValue; 103 | T mValue; 104 | bool mIsComputed = false; 105 | std::chrono::steady_clock::time_point mBecameReadyAt; 106 | }; 107 | 108 | } // namespace tev 109 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "dependencies/args"] 2 | path = dependencies/args 3 | url = https://github.com/Taywee/args 4 | shallow = true 5 | [submodule "dependencies/Imath"] 6 | path = dependencies/Imath 7 | url = https://github.com/AcademySoftwareFoundation/Imath 8 | shallow = true 9 | [submodule "dependencies/openexr"] 10 | path = dependencies/openexr 11 | url = https://github.com/AcademySoftwareFoundation/openexr 12 | shallow = true 13 | [submodule "dependencies/zlib"] 14 | path = dependencies/zlib 15 | url = https://github.com/madler/zlib 16 | shallow = true 17 | [submodule "dependencies/utfcpp"] 18 | path = dependencies/utfcpp 19 | url = https://github.com/Tom94/utfcpp 20 | shallow = true 21 | [submodule "dependencies/tinylogger"] 22 | path = dependencies/tinylogger 23 | url = https://github.com/Tom94/tinylogger 24 | shallow = true 25 | [submodule "dependencies/clip"] 26 | path = dependencies/clip 27 | url = https://github.com/Tom94/clip 28 | shallow = true 29 | [submodule "dependencies/DirectXTex"] 30 | path = dependencies/DirectXTex 31 | url = https://github.com/Tom94/DirectXTex 32 | shallow = true 33 | [submodule "dependencies/nanogui"] 34 | path = dependencies/nanogui 35 | url = https://github.com/Tom94/nanogui-1 36 | shallow = true 37 | [submodule "dependencies/qoi"] 38 | path = dependencies/qoi 39 | url = https://github.com/phoboslab/qoi.git 40 | shallow = true 41 | [submodule "dependencies/fmt"] 42 | path = dependencies/fmt 43 | url = https://github.com/fmtlib/fmt 44 | shallow = true 45 | [submodule "dependencies/libdeflate"] 46 | path = dependencies/libdeflate 47 | url = https://github.com/ebiggers/libdeflate 48 | shallow = true 49 | [submodule "dependencies/libheif"] 50 | path = dependencies/libheif 51 | url = https://github.com/Tom94/libheif 52 | shallow = true 53 | [submodule "dependencies/libde265"] 54 | path = dependencies/libde265 55 | url = https://github.com/strukturag/libde265 56 | shallow = true 57 | [submodule "dependencies/aom"] 58 | path = dependencies/aom 59 | url = https://github.com/Tom94/aom 60 | shallow = true 61 | [submodule "dependencies/Little-CMS"] 62 | path = dependencies/Little-CMS 63 | url = https://github.com/mm2/Little-CMS 64 | shallow = true 65 | [submodule "dependencies/libexif"] 66 | path = dependencies/libexif 67 | url = https://github.com/Tom94/libexif 68 | shallow = true 69 | [submodule "dependencies/libultrahdr"] 70 | path = dependencies/libultrahdr 71 | url = https://github.com/tom94/libultrahdr 72 | shallow = true 73 | [submodule "dependencies/libjpeg-turbo"] 74 | path = dependencies/libjpeg-turbo 75 | url = https://github.com/libjpeg-turbo/libjpeg-turbo 76 | shallow = true 77 | [submodule "dependencies/libjxl"] 78 | path = dependencies/libjxl 79 | url = https://github.com/libjxl/libjxl 80 | shallow = true 81 | [submodule "dependencies/libpng"] 82 | path = dependencies/libpng 83 | url = https://github.com/pnggroup/libpng 84 | shallow = true 85 | [submodule "dependencies/libtiff"] 86 | path = dependencies/libtiff 87 | url = https://github.com/tom94/libtiff 88 | shallow = true 89 | [submodule "dependencies/libwebp"] 90 | path = dependencies/libwebp 91 | url = https://chromium.googlesource.com/webm/libwebp 92 | shallow = true 93 | [submodule "dependencies/LibRaw"] 94 | path = dependencies/LibRaw 95 | url = https://github.com/LibRaw/LibRaw 96 | shallow = true 97 | [submodule "dependencies/LibRaw-cmake"] 98 | path = dependencies/LibRaw-cmake 99 | url = https://github.com/LibRaw/LibRaw-cmake 100 | shallow = true 101 | [submodule "dependencies/xmp"] 102 | path = dependencies/xmp 103 | url = https://github.com/tom94/xmp 104 | [submodule "dependencies/libexpat"] 105 | path = dependencies/libexpat 106 | url = https://github.com/libexpat/libexpat 107 | -------------------------------------------------------------------------------- /src/imageio/AppleMakerNote.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * tev -- the EDR viewer 3 | * 4 | * Copyright (C) 2025 Thomas Müller 5 | * 6 | * This program is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | #include 20 | #include 21 | 22 | using namespace std; 23 | 24 | namespace tev { 25 | 26 | static const uint8_t APPLE_SIGNATURE[] = {0x41, 0x70, 0x70, 0x6C, 0x65, 0x20, 0x69, 0x4F, 0x53, 0x00}; // "Apple iOS\0" 27 | static const size_t SIG_LENGTH = sizeof(APPLE_SIGNATURE); 28 | 29 | bool isAppleMakernote(const uint8_t* data, size_t length) { 30 | if (length < SIG_LENGTH) { 31 | return false; 32 | } 33 | 34 | return memcmp(data, APPLE_SIGNATURE, SIG_LENGTH) == 0; 35 | } 36 | 37 | // This whole function is one huge hack. It was pieced together by referencing the EXIF spec as well as the (non-functional) implementation 38 | // over at libexif. https://github.com/libexif/libexif/blob/master/libexif/apple/exif-mnote-data-apple.c That, plus quite a bit of trial and 39 | // error, finally got this to work. Who knows when Apple will break it. :) 40 | AppleMakerNote::AppleMakerNote(const uint8_t* data, size_t length) { 41 | if (!isAppleMakernote(data, length)) { 42 | throw invalid_argument{"AppleMakerNote: invalid header."}; 43 | } 44 | 45 | mReverseEndianess = false; 46 | 47 | size_t ofs = 0; 48 | if ((data[ofs + 12] == 'M') && (data[ofs + 13] == 'M')) { 49 | mReverseEndianess = std::endian::little == std::endian::native; 50 | } else if ((data[ofs + 12] == 'I') && (data[ofs + 13] == 'I')) { 51 | mReverseEndianess = std::endian::big == std::endian::native; 52 | } else { 53 | throw invalid_argument{"AppleMakerNote: failed to determine byte order."}; 54 | } 55 | 56 | uint32_t tcount = read(data + ofs + 14); 57 | 58 | if (length < ofs + 16 + tcount * 12 + 4) { 59 | throw invalid_argument{"AppleMakerNote: too short"}; 60 | } 61 | 62 | ofs += 16; 63 | 64 | tlog::debug() << "Decoding Apple maker note:"; 65 | for (uint32_t i = 0; i < tcount; i++) { 66 | if (ofs + 12 > length) { 67 | throw invalid_argument{"AppleMakerNote: overflow"}; 68 | } 69 | 70 | AppleMakerNoteEntry entry; 71 | entry.tag = read(data + ofs); 72 | entry.format = read(data + ofs + 2); 73 | entry.nComponents = read(data + ofs + 4); 74 | 75 | if (ofs + 4 + entry.size() > length) { 76 | throw invalid_argument{"AppleMakerNote: elem overflow"}; 77 | } 78 | 79 | size_t entryOffset; 80 | if (entry.size() > 4) { 81 | // Entry is stored somewhere else, pointed to by the following 82 | entryOffset = read(data + ofs + 8); // -6? 83 | } else { 84 | entryOffset = ofs + 8; 85 | } 86 | 87 | entry.data = vector(data + entryOffset, data + entryOffset + entry.size()); 88 | 89 | if (entryOffset + entry.size() > length) { 90 | throw invalid_argument{"AppleMakerNote: offset overflow"}; 91 | } 92 | 93 | ofs += 12; 94 | mTags[entry.tag] = entry; 95 | 96 | tlog::debug() << fmt::format(" tag={} format={} components={}", entry.tag, (int)entry.format, entry.nComponents); 97 | } 98 | } 99 | 100 | } // namespace tev 101 | -------------------------------------------------------------------------------- /include/tev/ImageButton.h: -------------------------------------------------------------------------------- 1 | /* 2 | * tev -- the EDR viewer 3 | * 4 | * Copyright (C) 2025 Thomas Müller 5 | * 6 | * This program is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | #pragma once 20 | 21 | #include 22 | 23 | #include 24 | #include 25 | 26 | #include 27 | 28 | namespace tev { 29 | 30 | class ImageButton : public nanogui::Widget { 31 | public: 32 | ImageButton(nanogui::Widget* parent, std::string_view caption, bool canBeReference); 33 | 34 | nanogui::Vector2i preferred_size_impl(NVGcontext* ctx) const override; 35 | 36 | bool mouse_button_event(const nanogui::Vector2i& p, int button, bool down, int modifiers) override; 37 | 38 | void draw(NVGcontext* ctx) override; 39 | 40 | void set_theme(nanogui::Theme* theme) override { 41 | if (theme != m_theme.get()) { 42 | preferred_size_changed(); 43 | nanogui::Widget::set_theme(theme); 44 | nanogui::Theme* captionTextBoxTheme = new nanogui::Theme(*theme); 45 | captionTextBoxTheme->m_text_box_font_size = m_font_size; 46 | captionTextBoxTheme->m_text_color = nanogui::Color(255, 255); 47 | mCaptionTextBox->set_theme(captionTextBoxTheme); 48 | } 49 | } 50 | 51 | std::string_view caption() const { return mCaption; } 52 | 53 | void setCaption(std::string_view caption) { 54 | if (caption != mCaption) { 55 | preferred_size_changed(); 56 | mCaption = caption; 57 | 58 | // Reset drawing state 59 | mSizeForWhichCutoffWasComputed = {0}; 60 | mHighlightBegin = 0; 61 | mHighlightEnd = 0; 62 | 63 | if (mCaptionChangeCallback) { 64 | mCaptionChangeCallback(); 65 | } 66 | } 67 | } 68 | 69 | void setReferenceCallback(const std::function& callback) { mReferenceCallback = callback; } 70 | 71 | void setIsReference(bool isReference) { mIsReference = isReference; } 72 | 73 | bool isReference() const { return mIsReference; } 74 | 75 | void setSelectedCallback(const std::function& callback) { mSelectedCallback = callback; } 76 | 77 | void setCaptionChangeCallback(const std::function& callback) { mCaptionChangeCallback = callback; } 78 | 79 | void setIsSelected(bool isSelected) { mIsSelected = isSelected; } 80 | 81 | bool isSelected() const { return mIsSelected; } 82 | 83 | void setId(size_t id) { 84 | if (id != mId) { 85 | preferred_size_changed(); 86 | mId = id; 87 | } 88 | } 89 | 90 | size_t id() const { return mId; } 91 | 92 | void setHighlightRange(size_t begin, size_t end); 93 | 94 | void showTextBox(); 95 | void hideTextBox(); 96 | 97 | bool textBoxVisible() const { return mCaptionTextBox->visible(); } 98 | 99 | private: 100 | std::string mCaption; 101 | nanogui::TextBox* mCaptionTextBox; 102 | 103 | bool mCanBeReference; 104 | 105 | bool mIsReference = false; 106 | std::function mReferenceCallback; 107 | 108 | bool mIsSelected = false; 109 | std::function mSelectedCallback; 110 | 111 | std::function mCaptionChangeCallback; 112 | 113 | size_t mId = 0; 114 | size_t mCutoff = 0; 115 | nanogui::Vector2i mSizeForWhichCutoffWasComputed = {0}; 116 | 117 | size_t mHighlightBegin = 0; 118 | size_t mHighlightEnd = 0; 119 | }; 120 | 121 | } // namespace tev 122 | -------------------------------------------------------------------------------- /src/imageio/Xmp.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * tev -- the EDR viewer 3 | * 4 | * Copyright (C) 2025 Thomas Müller 5 | * 6 | * This program is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | #include 20 | #include 21 | 22 | #define TXMP_STRING_TYPE std::string 23 | #include 24 | #include 25 | 26 | #include 27 | #include 28 | #include 29 | 30 | using namespace std; 31 | 32 | namespace tev { 33 | 34 | class XMPContext { 35 | public: 36 | static bool init() { 37 | std::call_once(initFlag, []() { 38 | initialized = SXMPMeta::Initialize(); 39 | if (initialized) { 40 | std::atexit(shutdown); 41 | } 42 | }); 43 | 44 | return initialized; 45 | } 46 | 47 | private: 48 | static void shutdown() { 49 | if (initialized) { 50 | SXMPMeta::Terminate(); 51 | } 52 | } 53 | 54 | static std::once_flag initFlag; 55 | static bool initialized; 56 | }; 57 | 58 | std::once_flag XMPContext::initFlag; 59 | bool XMPContext::initialized = false; 60 | 61 | Xmp::Xmp(string_view xmpData) { 62 | if (!XMPContext::init()) { 63 | throw invalid_argument{"Failed to initialize XMP toolkit."}; 64 | } 65 | 66 | mAttributes.name = "XMP"; 67 | 68 | try { 69 | SXMPMeta meta; 70 | meta.ParseFromBuffer(xmpData.data(), xmpData.size()); 71 | 72 | // tlog::debug() << xmpData; 73 | 74 | SXMPIterator iter{meta}; 75 | string schema, path, value; 76 | 77 | while (iter.Next(&schema, &path, &value)) { 78 | // tlog::debug() << fmt::format("{} | {} | {}", schema, path, value); 79 | 80 | if (value.empty()) { 81 | continue; 82 | } 83 | 84 | AttributeNode* node = &mAttributes; 85 | const auto parts = split(path, ":/", true); 86 | 87 | for (const auto& part : parts) { 88 | // Search from the back because XMP properties are often nested in order. 89 | const auto it = std::find_if(node->children.rbegin(), node->children.rend(), [&](const auto& child) { 90 | return child.name == part; 91 | }); 92 | 93 | if (it == node->children.rend()) { 94 | node->children.emplace_back(AttributeNode{.name = string{part}, .value = "", .type = "", .children = {}}); 95 | node = &node->children.back(); 96 | continue; 97 | } 98 | 99 | node = &(*it); 100 | } 101 | 102 | if (!node->value.empty()) { 103 | tlog::warning() 104 | << fmt::format("XMP property '{}' already has a value '{}', overwriting with new value '{}'.", path, node->value, value); 105 | } 106 | 107 | node->value = value; 108 | node->type = "string"; 109 | } 110 | 111 | if (string orientationStr; meta.GetProperty(kXMP_NS_TIFF, "Orientation", &orientationStr, nullptr)) { 112 | try { 113 | const int orientationInt = stoi(orientationStr); 114 | mOrientation = static_cast(orientationInt); 115 | tlog::debug() << fmt::format("Found XMP orientation: {}", orientationInt); 116 | } catch (const invalid_argument&) { 117 | tlog::warning() << fmt::format("Failed to parse XMP orientation value: '{}'.", orientationStr); 118 | } 119 | } 120 | } catch (XMP_Error& e) { throw invalid_argument{e.GetErrMsg()}; } 121 | } 122 | 123 | } // namespace tev 124 | -------------------------------------------------------------------------------- /src/imageio/PfmImageLoader.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * tev -- the EDR viewer 3 | * 4 | * Copyright (C) 2025 Thomas Müller 5 | * 6 | * This program is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | #include 20 | #include 21 | 22 | #include 23 | 24 | using namespace nanogui; 25 | using namespace std; 26 | 27 | namespace tev { 28 | 29 | Task> PfmImageLoader::load(istream& iStream, const fs::path&, string_view, int priority, bool) const { 30 | char pf[2]; 31 | iStream.read(pf, 2); 32 | if (!iStream || pf[0] != 'P' || (pf[1] != 'F' && pf[1] != 'f')) { 33 | throw FormatNotSupported{"Invalid PFM magic string."}; 34 | } 35 | 36 | iStream.clear(); 37 | iStream.seekg(0); 38 | 39 | string magic; 40 | Vector2i size; 41 | float scale; 42 | 43 | iStream >> magic >> size.x() >> size.y() >> scale; 44 | 45 | int numChannels; 46 | if (magic == "Pf") { 47 | numChannels = 1; 48 | } else if (magic == "PF") { 49 | numChannels = 3; 50 | } else if (magic == "PF4") { 51 | numChannels = 4; 52 | } else { 53 | throw FormatNotSupported{fmt::format("Invalid PFM magic string {}", magic)}; 54 | } 55 | 56 | if (!isfinite(scale) || scale == 0) { 57 | throw ImageLoadError{fmt::format("Invalid PFM scale {}", scale)}; 58 | } 59 | 60 | bool isPfmLittleEndian = scale < 0; 61 | scale = abs(scale); 62 | 63 | vector result(1); 64 | ImageData& resultData = result.front(); 65 | 66 | resultData.channels = makeRgbaInterleavedChannels(numChannels, numChannels == 4, size, EPixelFormat::F32, EPixelFormat::F32); 67 | 68 | auto numPixels = (size_t)size.x() * size.y(); 69 | if (numPixels == 0) { 70 | throw ImageLoadError{"Image has zero pixels."}; 71 | } 72 | 73 | auto numFloats = numPixels * numChannels; 74 | auto numBytes = numFloats * sizeof(float); 75 | 76 | // Skip last newline at the end of the header. 77 | { 78 | char c; 79 | while (iStream.get(c) && c != '\r' && c != '\n') 80 | ; 81 | } 82 | 83 | // Read entire file in binary mode. 84 | vector data(numFloats); 85 | iStream.read(reinterpret_cast(data.data()), numBytes); 86 | if (iStream.gcount() < (streamsize)numBytes) { 87 | throw ImageLoadError{fmt::format("Not sufficient bytes to read ({} vs {})", iStream.gcount(), numBytes)}; 88 | } 89 | 90 | // Reverse bytes of every float if endianness does not match up with system 91 | const bool shallSwapBytes = (std::endian::native == std::endian::little) != isPfmLittleEndian; 92 | 93 | co_await ThreadPool::global().parallelForAsync( 94 | 0, 95 | size.y(), 96 | [&](int y) { 97 | for (int x = 0; x < size.x(); ++x) { 98 | int baseIdx = (y * size.x() + x) * numChannels; 99 | for (int c = 0; c < numChannels; ++c) { 100 | float val = data[baseIdx + c]; 101 | 102 | // Thankfully, due to branch prediction, the "if" in the inner loop is no significant overhead. 103 | if (shallSwapBytes) { 104 | val = swapBytes(val); 105 | } 106 | 107 | // Flip image vertically due to PFM format 108 | resultData.channels[c].setAt({x, size.y() - (int)y - 1}, scale * val); 109 | } 110 | } 111 | }, 112 | priority 113 | ); 114 | 115 | // Treated like EXR: scene-referred by nature. Usually corresponds to linear light, so should not get its white point adjusted. 116 | resultData.renderingIntent = ERenderingIntent::AbsoluteColorimetric; 117 | resultData.hasPremultipliedAlpha = false; 118 | 119 | co_return result; 120 | } 121 | 122 | } // namespace tev 123 | -------------------------------------------------------------------------------- /src/Channel.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * tev -- the EDR viewer 3 | * 4 | * Copyright (C) 2025 Thomas Müller 5 | * 6 | * This program is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | #include 20 | #include 21 | 22 | #include 23 | 24 | using namespace nanogui; 25 | using namespace std; 26 | 27 | namespace tev { 28 | 29 | pair Channel::split(string_view channel) { 30 | size_t dotPosition = channel.rfind("."); 31 | if (dotPosition != string::npos) { 32 | return {channel.substr(0, dotPosition + 1), channel.substr(dotPosition + 1)}; 33 | } 34 | 35 | return {"", channel}; 36 | } 37 | 38 | string_view Channel::tail(string_view channel) { return split(channel).second; } 39 | 40 | string_view Channel::head(string_view channel) { return split(channel).first; } 41 | 42 | bool Channel::isTopmost(string_view channel) { return tail(channel) == channel; } 43 | 44 | bool Channel::isAlpha(string_view channel) { return toLower(tail(channel)) == "a"; } 45 | 46 | Color Channel::color(string_view channel, bool pastel) { 47 | auto lowerChannel = toLower(tail(channel)); 48 | 49 | if (pastel) { 50 | if (lowerChannel == "r") { 51 | return Color(0.8f, 0.2f, 0.2f, 1.0f); 52 | } else if (lowerChannel == "g") { 53 | return Color(0.2f, 0.8f, 0.2f, 1.0f); 54 | } else if (lowerChannel == "b") { 55 | return Color(0.2f, 0.3f, 1.0f, 1.0f); 56 | } 57 | } else { 58 | if (lowerChannel == "r") { 59 | return Color(1.0f, 0.0f, 0.0f, 1.0f); 60 | } else if (lowerChannel == "g") { 61 | return Color(0.0f, 1.0f, 0.0f, 1.0f); 62 | } else if (lowerChannel == "b") { 63 | return Color(0.0f, 0.0f, 1.0f, 1.0f); 64 | } 65 | } 66 | 67 | return Color(1.0f, 1.0f); 68 | } 69 | 70 | Channel::Channel( 71 | string_view name, 72 | const nanogui::Vector2i& size, 73 | EPixelFormat format, 74 | EPixelFormat desiredFormat, 75 | shared_ptr> data, 76 | size_t dataOffset, 77 | size_t dataStride 78 | ) : 79 | mName{name}, mSize{size}, mPixelFormat{format}, mDesiredPixelFormat{desiredFormat} { 80 | if (data) { 81 | mData = data; 82 | mDataOffset = dataOffset; 83 | mDataStride = dataStride; 84 | } else { 85 | mData = make_shared>(nBytes(format) * (size_t)size.x() * size.y()); 86 | mDataOffset = 0; 87 | mDataStride = nBytes(format); 88 | } 89 | } 90 | 91 | Task Channel::divideByAsync(const Channel& other, int priority) { 92 | co_await ThreadPool::global().parallelForAsync( 93 | 0, 94 | other.numPixels(), 95 | [&](size_t i) { 96 | if (other.at(i) != 0) { 97 | setAt(i, at(i) / other.at(i)); 98 | } else { 99 | setAt(i, 0); 100 | } 101 | }, 102 | priority 103 | ); 104 | } 105 | 106 | Task Channel::multiplyWithAsync(const Channel& other, int priority) { 107 | co_await ThreadPool::global().parallelForAsync(0, other.numPixels(), [&](size_t i) { setAt(i, at(i) * other.at(i)); }, priority); 108 | } 109 | 110 | void Channel::updateTile(int x, int y, int width, int height, span newData) { 111 | if (x < 0 || y < 0 || x + width > size().x() || y + height > size().y()) { 112 | tlog::warning() << "Tile [" << x << "," << y << "," << width << "," << height 113 | << "] could not be updated because it does not fit into the channel's size " << size(); 114 | return; 115 | } 116 | 117 | for (int posY = 0; posY < height; ++posY) { 118 | for (int posX = 0; posX < width; ++posX) { 119 | setAt({x + posX, y + posY}, newData[posX + posY * (size_t)width]); 120 | } 121 | } 122 | } 123 | 124 | } // namespace tev 125 | -------------------------------------------------------------------------------- /src/imageio/ClipboardImageLoader.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * tev -- the EDR viewer 3 | * 4 | * Copyright (C) 2025 Thomas Müller 5 | * 6 | * This program is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | #include 20 | #include 21 | 22 | #include 23 | 24 | using namespace nanogui; 25 | using namespace std; 26 | 27 | namespace tev { 28 | 29 | Task> ClipboardImageLoader::load(istream& iStream, const fs::path&, string_view, int priority, bool) const { 30 | char magic[4]; 31 | clip::image_spec spec; 32 | 33 | iStream.read(magic, 4); 34 | string magicString(magic, 4); 35 | 36 | if (!iStream || magicString != "clip") { 37 | throw FormatNotSupported{fmt::format("Invalid magic clipboard string {}.", magicString)}; 38 | } 39 | 40 | iStream.read(reinterpret_cast(&spec), sizeof(clip::image_spec)); 41 | if (iStream.gcount() < (streamsize)sizeof(clip::image_spec)) { 42 | throw ImageLoadError{fmt::format("Insufficient bytes to read image spec ({} vs {}).", iStream.gcount(), sizeof(clip::image_spec))}; 43 | } 44 | 45 | Vector2i size{(int)spec.width, (int)spec.height}; 46 | 47 | auto numPixels = (size_t)size.x() * size.y(); 48 | if (numPixels == 0) { 49 | throw ImageLoadError{"Image has zero pixels."}; 50 | } 51 | 52 | auto numChannels = (int)(spec.bits_per_pixel / 8); 53 | if (numChannels > 4) { 54 | throw ImageLoadError{"Image has too many channels."}; 55 | } 56 | 57 | const size_t numBytesPerRow = spec.bytes_per_row; 58 | const size_t numBytes = numBytesPerRow * size.y(); 59 | const int alphaChannelIndex = 3; 60 | 61 | vector result(1); 62 | ImageData& resultData = result.front(); 63 | 64 | // Clipboard images are always 32 bit RGBA. Can be comfortably represented as F16. 65 | resultData.channels = makeRgbaInterleavedChannels(numChannels, numChannels == 4, size, EPixelFormat::F32, EPixelFormat::F16); 66 | 67 | vector data(numBytes); 68 | iStream.read(reinterpret_cast(data.data()), numBytes); 69 | if (iStream.gcount() < (streamsize)numBytes) { 70 | throw ImageLoadError{fmt::format("Insufficient bytes to read image data ({} vs {}).", iStream.gcount(), numBytes)}; 71 | } 72 | 73 | const size_t shifts[4] = { 74 | (size_t)(spec.red_shift / 8), 75 | (size_t)(spec.green_shift / 8), 76 | (size_t)(spec.blue_shift / 8), 77 | (size_t)(spec.alpha_shift / 8), 78 | }; 79 | 80 | for (int c = 0; c < numChannels; ++c) { 81 | if (shifts[c] >= (size_t)numChannels) { 82 | throw ImageLoadError{"Invalid shift encountered in clipboard image."}; 83 | } 84 | } 85 | 86 | float* floatData = resultData.channels.front().floatData(); 87 | co_await ThreadPool::global().parallelForAsync( 88 | 0, 89 | size.y(), 90 | [&](int y) { 91 | size_t rowIdxIn = y * numBytesPerRow; 92 | size_t rowIdxOut = y * size.x() * numChannels; 93 | 94 | for (int x = 0; x < size.x(); ++x) { 95 | float alpha = 1.0f; 96 | 97 | const size_t baseIdxIn = rowIdxIn + x * numChannels; 98 | const size_t baseIdxOut = rowIdxOut + x * numChannels; 99 | 100 | for (int c = numChannels - 1; c >= 0; --c) { 101 | const unsigned char val = data[baseIdxIn + shifts[c]]; 102 | if (c == alphaChannelIndex) { 103 | alpha = val / 255.0f; 104 | floatData[baseIdxOut + c] = alpha; 105 | } else { 106 | floatData[baseIdxOut + c] = toLinear(val / 255.0f) * alpha; 107 | } 108 | } 109 | } 110 | }, 111 | priority 112 | ); 113 | 114 | resultData.hasPremultipliedAlpha = true; 115 | 116 | co_return result; 117 | } 118 | 119 | } // namespace tev 120 | -------------------------------------------------------------------------------- /include/tev/Box.h: -------------------------------------------------------------------------------- 1 | /* 2 | * tev -- the EDR viewer 3 | * 4 | * Copyright (C) 2025 Thomas Müller 5 | * 6 | * This program is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | #pragma once 20 | 21 | #include 22 | 23 | #include 24 | 25 | namespace tev { 26 | 27 | template struct Box { 28 | using Vector = nanogui::Array; 29 | 30 | Box(const Vector& _min, const Vector& _max) : min{_min}, max{_max} {} 31 | Box(const Vector& _max) : Box{Vector{(T)0}, _max} {} 32 | Box() : Box{Vector{std::numeric_limits::max()}, Vector{std::numeric_limits::min()}} {} 33 | 34 | // Casting boxes of other types to this one 35 | template Box(const Box& other) : min{other.min}, max{other.max} {} 36 | 37 | Box(std::span points) : Box() { 38 | for (const auto& point : points) { 39 | min = nanogui::min(min, point); 40 | max = nanogui::max(max, point); 41 | } 42 | } 43 | 44 | Vector size() const { return nanogui::max(max - min, Vector{(T)0}); } 45 | 46 | using area_t = std::conditional_t, size_t, T>; 47 | area_t area() const { 48 | auto size = this->size(); 49 | area_t result = (T)1; 50 | for (uint32_t i = 0; i < N_DIMS; ++i) { 51 | result *= (area_t)size[i]; 52 | } 53 | 54 | return result; 55 | } 56 | 57 | Vector middle() const { return (min + max) / (T)2; } 58 | 59 | bool isValid() const { 60 | bool result = true; 61 | for (uint32_t i = 0; i < N_DIMS; ++i) { 62 | result &= max[i] >= min[i]; 63 | } 64 | 65 | return result; 66 | } 67 | 68 | bool contains(const Vector& pos) const { 69 | bool result = true; 70 | for (uint32_t i = 0; i < N_DIMS; ++i) { 71 | result &= pos[i] >= min[i] && pos[i] < max[i]; 72 | } 73 | 74 | return result; 75 | } 76 | 77 | bool contains_inclusive(const Vector& pos) const { 78 | bool result = true; 79 | for (uint32_t i = 0; i < N_DIMS; ++i) { 80 | result &= pos[i] >= min[i] && pos[i] <= max[i]; 81 | } 82 | 83 | return result; 84 | } 85 | 86 | bool contains(const Box& other) const { return contains_inclusive(other.min) && contains_inclusive(other.max); } 87 | 88 | Box intersect(const Box& other) const { return {nanogui::max(min, other.min), nanogui::min(max, other.max)}; } 89 | 90 | Box translate(const Vector& offset) const { return {min + offset, max + offset}; } 91 | 92 | bool operator==(const Box& other) const { return min == other.min && max == other.max; } 93 | 94 | Box inflate(T amount) const { return {min - Vector{amount}, max + Vector{amount}}; } 95 | 96 | Vector min, max; 97 | }; 98 | 99 | using Box2f = Box; 100 | using Box3f = Box; 101 | using Box4f = Box; 102 | using Box2i = Box; 103 | using Box3i = Box; 104 | using Box4i = Box; 105 | 106 | inline Box2i applyOrientation(EOrientation orientation, const Box2i& box) { 107 | Box2i result = {{{ 108 | // Passing {1, 1} as size has the effect of simply flipping the sign of the axes getting flipped. 109 | applyOrientation(orientation, box.min, {1, 1}), 110 | applyOrientation(orientation, box.max - nanogui::Vector2i{1}, {1, 1}), 111 | }}}; 112 | result.max += nanogui::Vector2i{1}; 113 | 114 | return result; 115 | }; 116 | 117 | } // namespace tev 118 | 119 | template struct fmt::formatter> { 120 | template constexpr ParseContext::iterator parse(ParseContext& ctx) { return ctx.begin(); } 121 | template FmtContext::iterator format(const tev::Box& box, FmtContext& ctx) const { 122 | return fmt::format_to(ctx.out(), "[{}, {}]", box.min, box.max); 123 | } 124 | }; 125 | 126 | template , int> = 0> 127 | Stream& operator<<(Stream& os, const tev::Box& v) { 128 | os << '[' << v.min << ", " << v.max << ']'; 129 | return os; 130 | } 131 | -------------------------------------------------------------------------------- /src/imageio/StbiImageLoader.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * tev -- the EDR viewer 3 | * 4 | * Copyright (C) 2025 Thomas Müller 5 | * 6 | * This program is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | #include 20 | #include 21 | 22 | #define STB_IMAGE_IMPLEMENTATION 23 | #include 24 | 25 | using namespace nanogui; 26 | using namespace std; 27 | 28 | namespace tev { 29 | 30 | Task> StbiImageLoader::load(istream& iStream, const fs::path&, string_view, int priority, bool) const { 31 | static const stbi_io_callbacks callbacks = { 32 | // Read 33 | [](void* context, char* data, int size) { 34 | auto stream = reinterpret_cast(context); 35 | stream->read(data, size); 36 | return (int)stream->gcount(); 37 | }, 38 | // Seek 39 | [](void* context, int size) { reinterpret_cast(context)->seekg(size, ios_base::cur); }, 40 | // EOF 41 | [](void* context) { return (int)!!(*reinterpret_cast(context)); }, 42 | }; 43 | 44 | void* data; 45 | int numChannels; 46 | int numFrames = 1; 47 | Vector2i size; 48 | bool isHdr = stbi_is_hdr_from_callbacks(&callbacks, &iStream) != 0; 49 | iStream.clear(); 50 | iStream.seekg(0); 51 | 52 | if (isHdr) { 53 | data = stbi_loadf_from_callbacks(&callbacks, &iStream, &size.x(), &size.y(), &numChannels, 0); 54 | } else { 55 | stbi__context s; 56 | stbi__start_callbacks(&s, (stbi_io_callbacks*)&callbacks, &iStream); 57 | bool isGif = stbi__gif_test(&s); 58 | iStream.clear(); 59 | iStream.seekg(0); 60 | 61 | if (isGif) { 62 | stbi__start_callbacks(&s, (stbi_io_callbacks*)&callbacks, &iStream); 63 | int* delays; // We don't care about gif frame delays. Read and discard. 64 | data = stbi__load_gif_main(&s, &delays, &size.x(), &size.y(), &numFrames, &numChannels, 0); 65 | } else { 66 | data = stbi_load_from_callbacks(&callbacks, &iStream, &size.x(), &size.y(), &numChannels, 0); 67 | } 68 | } 69 | 70 | if (!data) { 71 | throw ImageLoadError{std::string{stbi_failure_reason()}}; 72 | } 73 | 74 | if (numFrames == 0) { 75 | throw ImageLoadError{"Image has zero frames."}; 76 | } 77 | 78 | if (size.x() == 0 || size.y() == 0) { 79 | throw ImageLoadError{"Image has zero pixels."}; 80 | } 81 | 82 | ScopeGuard dataGuard{[data] { stbi_image_free(data); }}; 83 | 84 | vector result(numFrames); 85 | for (int frameIdx = 0; frameIdx < numFrames; ++frameIdx) { 86 | ImageData& resultData = result[frameIdx]; 87 | 88 | // Unless the image is a .hdr file, it's 8 bits per channel, so we can comfortably fit it into F16. 89 | resultData.channels = makeRgbaInterleavedChannels( 90 | numChannels, numChannels == 4, size, EPixelFormat::F32, isHdr ? EPixelFormat::F32 : EPixelFormat::F16 91 | ); 92 | resultData.hasPremultipliedAlpha = false; 93 | if (numFrames > 1) { 94 | resultData.partName = fmt::format("frames.{}", frameIdx); 95 | } 96 | 97 | auto numPixels = (size_t)size.x() * size.y(); 98 | if (isHdr) { 99 | // Treated like EXR: scene-referred by nature. Usually corresponds to linear light, so should not get its white point adjusted. 100 | resultData.renderingIntent = ERenderingIntent::AbsoluteColorimetric; 101 | 102 | co_await toFloat32((float*)data, numChannels, resultData.channels.front().floatData(), 4, size, numChannels == 4, priority); 103 | data = (float*)data + numPixels * numChannels; 104 | } else { 105 | // Assume sRGB-encoded LDR images are display-referred. 106 | resultData.renderingIntent = ERenderingIntent::RelativeColorimetric; 107 | 108 | co_await toFloat32( 109 | (uint8_t*)data, numChannels, resultData.channels.front().floatData(), 4, size, numChannels == 4, priority 110 | ); 111 | data = (uint8_t*)data + numPixels * numChannels; 112 | } 113 | } 114 | 115 | co_return result; 116 | } 117 | 118 | } // namespace tev 119 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | 11 | env: 12 | SUPPORT_HEIC: ${{ github.event_name == 'pull_request' }} 13 | 14 | jobs: 15 | build_linux: 16 | name: Build on linux systems 17 | runs-on: ${{ matrix.os }} 18 | strategy: 19 | matrix: 20 | os: [ubuntu-22.04, ubuntu-22.04-arm] 21 | env: 22 | build_dir: "build" 23 | config: "Release" 24 | CC: gcc 25 | CXX: g++ 26 | APPIMAGE_EXTRACT_AND_RUN: 1# https://github.com/AppImage/AppImageKit/wiki/FUSE#docker 27 | steps: 28 | - name: Install dependencies 29 | run: sudo apt-get update && sudo apt-get install -y cmake gcc-10 g++-10 libglu1-mesa-dev xorg-dev libdbus-1-dev libwayland-dev wayland-protocols libxkbcommon-dev libffi-dev nasm ninja-build 30 | - uses: actions/checkout@v1 31 | with: 32 | submodules: recursive 33 | - name: CMake 34 | run: cmake . -B ${{ env.build_dir }} -GNinja -DCMAKE_BUILD_TYPE=${{ env.config }} -DTEV_DEPLOY=1 -DTEV_SUPPORT_HEIC=${{ env.SUPPORT_HEIC }} 35 | - name: Build 36 | working-directory: ${{ env.build_dir }} 37 | run: cmake --build . --target all --verbose -j 38 | - name: Package 39 | working-directory: ${{ env.build_dir }} 40 | run: cmake --build . --config ${{ env.config }} --target package --verbose 41 | - name: Upload executable 42 | if: github.event_name != 'pull_request' 43 | uses: actions/upload-artifact@v4 44 | with: 45 | name: Linux executable (${{ matrix.os }}) 46 | path: ${{ env.build_dir }}/tev.appimage 47 | 48 | build_macos: 49 | name: Build on macOS 50 | runs-on: ${{ matrix.os }} 51 | strategy: 52 | matrix: 53 | include: 54 | - arch: x86_64 55 | os: macos-15-intel 56 | - arch: arm64 57 | os: macos-15 58 | env: 59 | build_dir: "build" 60 | config: "Release" 61 | steps: 62 | - uses: actions/checkout@v1 63 | with: 64 | submodules: recursive 65 | - name: Install dependencies 66 | run: brew install nasm 67 | - name: Remove pre-installed dependencies that conlict with our build 68 | run: brew uninstall --ignore-dependencies jpeg-xl || echo "jpeg-xl not installed, continuing" 69 | - name: CMake 70 | run: cmake . -B ${{ env.build_dir }} -GNinja -DCMAKE_BUILD_TYPE=${{ env.config }} -DCMAKE_OSX_DEPLOYMENT_TARGET=10.15 -DTEV_DEPLOY=1 -DTEV_SUPPORT_HEIC=${{ env.SUPPORT_HEIC }} 71 | - name: Build 72 | working-directory: ${{ env.build_dir }} 73 | run: cmake --build . --config ${{ env.config }} --target all --verbose -j 74 | - name: Zip app 75 | working-directory: ${{ env.build_dir }} 76 | run: zip -r tev.app.zip tev.app 77 | - name: Upload executable 78 | if: github.event_name != 'pull_request' 79 | uses: actions/upload-artifact@v4 80 | with: 81 | name: macOS executable (${{ matrix.arch }}) 82 | path: ${{ env.build_dir }}/tev.app.zip 83 | 84 | build_windows: 85 | name: Build on Windows 86 | runs-on: ${{ matrix.os }} 87 | strategy: 88 | matrix: 89 | os: 90 | - windows-2022 91 | # CMake is currently borked on the ARM runner, detecting AMD64. Disable for now. 92 | # https://github.com/orgs/community/discussions/155713#discussioncomment-12991583 93 | # - windows-11-arm 94 | env: 95 | build_dir: "build" 96 | config: "Release" 97 | steps: 98 | - uses: actions/checkout@v1 99 | with: 100 | submodules: recursive 101 | - name: Install dependencies 102 | run: | 103 | choco install nasm 104 | echo "C:\Program Files\NASM" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append 105 | - name: CMake 106 | run: cmake . -B ${{ env.build_dir }} -DCMAKE_BUILD_TYPE=${{ env.config }} -DTEV_DEPLOY=1 -DTEV_SUPPORT_HEIC=${{ env.SUPPORT_HEIC }} 107 | - name: Build 108 | working-directory: ${{ env.build_dir }} 109 | run: cmake --build . --config ${{ env.config }} --target ALL_BUILD --verbose 110 | - name: Create installer 111 | working-directory: ${{ env.build_dir }} 112 | run: cpack 113 | - name: Upload executable 114 | if: github.event_name != 'pull_request' 115 | uses: actions/upload-artifact@v4 116 | with: 117 | name: Windows executable (${{ matrix.os }}) 118 | path: ${{ env.build_dir }}/${{ env.config }}/tev.exe 119 | - name: Upload installer 120 | if: github.event_name != 'pull_request' 121 | uses: actions/upload-artifact@v4 122 | with: 123 | name: Windows installer (${{ matrix.os }}) 124 | path: ${{ env.build_dir }}/tev-installer.msi 125 | -------------------------------------------------------------------------------- /src/ThreadPool.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * tev -- the EDR viewer 3 | * 4 | * Copyright (C) 2025 Thomas Müller 5 | * 6 | * This program is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | #include 20 | 21 | #include 22 | 23 | using namespace std; 24 | 25 | namespace tev { 26 | 27 | ThreadPool::ThreadPool() : ThreadPool{thread::hardware_concurrency()} {} 28 | 29 | ThreadPool::ThreadPool(size_t maxNumThreads, bool force) { 30 | if (!force) { 31 | maxNumThreads = min((size_t)thread::hardware_concurrency(), maxNumThreads); 32 | } 33 | 34 | startThreads(maxNumThreads); 35 | mNumTasksInSystem.store(0); 36 | } 37 | 38 | ThreadPool::~ThreadPool() { 39 | waitUntilFinished(); 40 | shutdown(); 41 | 42 | while (!mThreads.empty()) { 43 | this_thread::sleep_for(1ms); 44 | } 45 | } 46 | 47 | void ThreadPool::startThreads(size_t num) { 48 | const lock_guard lock{mTaskQueueMutex}; 49 | if (mShuttingDown) { 50 | return; 51 | } 52 | 53 | mNumThreads += num; 54 | for (size_t i = mThreads.size(); i < mNumThreads; ++i) { 55 | mThreads.emplace_back([this] { 56 | const auto id = this_thread::get_id(); 57 | // tlog::debug() << "Spawning thread pool thread " << id; 58 | 59 | unique_lock lock{mTaskQueueMutex}; 60 | while (true) { 61 | if (!lock) { 62 | lock.lock(); 63 | } 64 | 65 | // look for a work item 66 | while (mThreads.size() <= mNumThreads && mTaskQueue.empty()) { 67 | // if there are none wait for notification 68 | mWorkerCondition.wait(lock); 69 | } 70 | 71 | if (mThreads.size() > mNumThreads) { 72 | break; 73 | } 74 | 75 | const function task{std::move(mTaskQueue.top().fun)}; 76 | mTaskQueue.pop(); 77 | 78 | // Unlock the lock, so we can process the task without blocking other threads 79 | lock.unlock(); 80 | 81 | task(); 82 | 83 | { 84 | const unique_lock localLock{mTaskQueueMutex}; 85 | 86 | mNumTasksInSystem--; 87 | 88 | if (mNumTasksInSystem == 0) { 89 | mSystemBusyCondition.notify_all(); 90 | } 91 | } 92 | } 93 | 94 | // Remove oneself from the thread pool. NOTE: at this point, the lock is still held, so modifying mThreads is safe. 95 | // tlog::debug() << "Shutting down thread pool thread " << id; 96 | 97 | const auto it = find_if(mThreads.begin(), mThreads.end(), [&id](const std::thread& t) { return t.get_id() == id; }); 98 | TEV_ASSERT(it != mThreads.end(), "Thread not found in thread pool."); 99 | 100 | // Thread must be detached, otherwise running our own constructor while still running would result in errors. 101 | thread self = std::move(*it); 102 | mThreads.erase(it); 103 | 104 | self.detach(); 105 | }); 106 | } 107 | } 108 | 109 | void ThreadPool::shutdownThreads(size_t num) { 110 | { 111 | const lock_guard lock{mTaskQueueMutex}; 112 | 113 | const auto numToClose = min(num, mNumThreads); 114 | mNumThreads -= numToClose; 115 | } 116 | 117 | // Wake up all the threads to have them quit 118 | mWorkerCondition.notify_all(); 119 | } 120 | 121 | void ThreadPool::shutdown() { 122 | { 123 | const lock_guard lock{mTaskQueueMutex}; 124 | 125 | mNumThreads = 0; 126 | mShuttingDown = true; 127 | } 128 | 129 | // Wake up all the threads to have them quit 130 | mWorkerCondition.notify_all(); 131 | } 132 | 133 | void ThreadPool::waitUntilFinished() { 134 | unique_lock lock{mTaskQueueMutex}; 135 | 136 | if (mNumTasksInSystem == 0) { 137 | return; 138 | } 139 | 140 | mSystemBusyCondition.wait(lock); 141 | } 142 | 143 | void ThreadPool::waitUntilFinishedFor(const chrono::microseconds Duration) { 144 | unique_lock lock{mTaskQueueMutex}; 145 | 146 | if (mNumTasksInSystem == 0) { 147 | return; 148 | } 149 | 150 | mSystemBusyCondition.wait_for(lock, Duration); 151 | } 152 | 153 | void ThreadPool::flushQueue() { 154 | const lock_guard lock{mTaskQueueMutex}; 155 | 156 | mNumTasksInSystem -= mTaskQueue.size(); 157 | while (!mTaskQueue.empty()) { 158 | mTaskQueue.pop(); 159 | } 160 | 161 | if (mNumTasksInSystem == 0) { 162 | mSystemBusyCondition.notify_all(); 163 | } 164 | } 165 | 166 | } // namespace tev 167 | -------------------------------------------------------------------------------- /include/tev/imageio/ImageLoader.h: -------------------------------------------------------------------------------- 1 | /* 2 | * tev -- the EDR viewer 3 | * 4 | * Copyright (C) 2025 Thomas Müller 5 | * 6 | * This program is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | #pragma once 20 | 21 | #include 22 | #include 23 | #include 24 | #include 25 | 26 | #include 27 | 28 | #include 29 | #include 30 | #include 31 | #include 32 | #include 33 | 34 | namespace tev { 35 | 36 | template 37 | Task toFloat32( 38 | const T* __restrict imageData, 39 | size_t numSamplesPerPixelIn, 40 | float* __restrict floatData, 41 | size_t numSamplesPerPixelOut, 42 | const nanogui::Vector2i& size, 43 | bool hasAlpha, 44 | int priority, 45 | // 0 defaults to 1/(2**bitsPerSample-1) 46 | float scale = 0.0f, 47 | // 0 defaults to numSamplesPerPixelIn * size.x() 48 | size_t numSamplesPerRowIn = 0, 49 | size_t numSamplesPerRowOut = 0 50 | ) { 51 | if constexpr (std::is_integral_v) { 52 | if (scale == 0.0f) { 53 | scale = 1.0f / (((size_t)1 << (sizeof(T) * 8)) - 1); 54 | } 55 | } else { 56 | if (scale == 0.0f) { 57 | scale = 1.0f; 58 | } 59 | } 60 | 61 | if (numSamplesPerRowIn == 0) { 62 | numSamplesPerRowIn = numSamplesPerPixelIn * size.x(); 63 | } 64 | 65 | if (numSamplesPerRowOut == 0) { 66 | numSamplesPerRowOut = numSamplesPerPixelOut * size.x(); 67 | } 68 | 69 | size_t numSamplesPerPixel = std::min(numSamplesPerPixelIn, numSamplesPerPixelOut); 70 | co_await ThreadPool::global().parallelForAsync( 71 | 0, 72 | size.y(), 73 | [&](int y) { 74 | size_t rowIdxIn = y * numSamplesPerRowIn; 75 | size_t rowIdxOut = y * numSamplesPerRowOut; 76 | 77 | for (int x = 0; x < size.x(); ++x) { 78 | size_t baseIdxIn = rowIdxIn + x * numSamplesPerPixelIn; 79 | size_t baseIdxOut = rowIdxOut + x * numSamplesPerPixelOut; 80 | 81 | for (size_t c = 0; c < numSamplesPerPixel; ++c) { 82 | if (hasAlpha && c == numSamplesPerPixelIn - 1) { 83 | // Copy alpha channel to the last output channel without conversion 84 | floatData[baseIdxOut + numSamplesPerPixelOut - 1] = (float)imageData[baseIdxIn + c] * scale; 85 | } else { 86 | float result; 87 | if constexpr (SRGB_TO_LINEAR) { 88 | result = toLinear((float)imageData[baseIdxIn + c] * scale); 89 | } else { 90 | result = (float)imageData[baseIdxIn + c] * scale; 91 | } 92 | 93 | if constexpr (MULTIPLY_ALPHA) { 94 | if (hasAlpha) { 95 | result *= (float)imageData[baseIdxIn + numSamplesPerPixelIn - 1] * scale; 96 | } 97 | } 98 | 99 | floatData[baseIdxOut + c] = result; 100 | } 101 | } 102 | } 103 | }, 104 | priority 105 | ); 106 | } 107 | 108 | class ImageLoader { 109 | public: 110 | class FormatNotSupported : public std::runtime_error { 111 | public: 112 | FormatNotSupported(const std::string& message) : std::runtime_error{message} {} 113 | }; 114 | 115 | virtual ~ImageLoader() {} 116 | 117 | virtual Task> 118 | load(std::istream& iStream, const fs::path& path, std::string_view channelSelector, int priority, bool applyGainmaps) const = 0; 119 | 120 | virtual std::string name() const = 0; 121 | 122 | static const std::vector>& getLoaders(); 123 | 124 | // Returns a list of all supported mime types, sorted by decoding preference. 125 | static const std::vector& supportedMimeTypes(); 126 | 127 | static std::vector makeRgbaInterleavedChannels( 128 | int numChannels, 129 | bool hasAlpha, 130 | const nanogui::Vector2i& size, 131 | EPixelFormat format, 132 | EPixelFormat desiredFormat, 133 | std::string_view namePrefix = "" 134 | ); 135 | 136 | static std::vector makeNChannels( 137 | int numChannels, const nanogui::Vector2i& size, EPixelFormat format, EPixelFormat desiredFormat, std::string_view namePrefix = "" 138 | ); 139 | 140 | static Task resizeChannelsAsync(const std::vector& srcChannels, std::vector& dstChannels, int priority); 141 | }; 142 | 143 | } // namespace tev 144 | -------------------------------------------------------------------------------- /src/ImageInfoWindow.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * tev -- the EDR viewer 3 | * 4 | * Copyright (C) 2025 Thomas Müller 5 | * 6 | * This program is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | #include 20 | 21 | #include 22 | #include 23 | #include 24 | #include 25 | #include 26 | #include 27 | #include 28 | #include 29 | #include 30 | 31 | using namespace nanogui; 32 | using namespace std; 33 | 34 | namespace tev { 35 | 36 | void addRows(Widget* current, const AttributeNode& node, int indentation) { 37 | auto row = new Widget{current}; 38 | row->set_layout(new BoxLayout{Orientation::Horizontal, Alignment::Maximum, 0, 0}); 39 | 40 | auto spacer = new Widget{row}; 41 | spacer->set_fixed_width(indentation * 8); 42 | 43 | auto nameWidget = new Label{row, node.name, "sans-bold"}; 44 | nameWidget->set_fixed_width(180 - indentation * 8); 45 | 46 | auto valueWidget = new Label{row, node.value, "sans"}; 47 | valueWidget->set_fixed_width(320); 48 | 49 | auto typeWidget = new Label{row, node.type, "sans"}; 50 | typeWidget->set_fixed_width(140); 51 | 52 | for (const auto& it : node.children) { 53 | addRows(current, it, indentation + 1); 54 | } 55 | }; 56 | 57 | ImageInfoWindow::ImageInfoWindow(Widget* parent, const std::shared_ptr& image, function closeCallback) : 58 | Window{parent, "Info"}, mCloseCallback{closeCallback} { 59 | 60 | auto closeButton = new Button{button_panel(), "", FA_TIMES}; 61 | closeButton->set_callback(mCloseCallback); 62 | 63 | static const int WINDOW_WIDTH = 700; 64 | static const int WINDOW_HEIGHT = 680; 65 | 66 | set_layout(new GroupLayout{}); 67 | set_fixed_width(WINDOW_WIDTH); 68 | 69 | mTabWidget = new TabWidget{this}; 70 | mTabWidget->set_fixed_height(WINDOW_HEIGHT - 12); 71 | 72 | // Each attributes entry is a tab 73 | for (const auto& tab : image->attributes()) { 74 | Widget* tmp = new Widget(mTabWidget); 75 | 76 | VScrollPanel* scrollPanel = new VScrollPanel{tmp}; 77 | scrollPanel->set_fixed_height(WINDOW_HEIGHT - 40); 78 | scrollPanel->set_fixed_width(WINDOW_WIDTH - 40); 79 | 80 | mTabWidget->append_tab(tab.name, tmp); 81 | 82 | Widget* container = new Widget(scrollPanel); 83 | container->set_layout(new GroupLayout{}); 84 | 85 | // Each top-level child of the attribute is a section 86 | for (const auto& section : tab.children) { 87 | new Label{container, section.name, "sans-bold", 18}; 88 | auto attributes = new Widget{container}; 89 | attributes->set_layout(new BoxLayout{Orientation::Vertical, Alignment::Fill, 0, 0}); 90 | 91 | for (const auto& c : section.children) { 92 | addRows(attributes, c, 0); 93 | } 94 | } 95 | } 96 | 97 | perform_layout(screen()->nvg_context()); 98 | 99 | mTabWidget->set_callback([this](int id) mutable { 100 | mTabWidget->set_selected_id(id); 101 | mScrollPanel = dynamic_cast(mTabWidget->child_at(0)->child_at(0)); 102 | }); 103 | 104 | if (mTabWidget->tab_count() > 0) { 105 | mTabWidget->set_selected_id(0); 106 | mScrollPanel = dynamic_cast(mTabWidget->child_at(0)->child_at(0)); 107 | } 108 | } 109 | 110 | bool ImageInfoWindow::keyboard_event(int key, int scancode, int action, int modifiers) { 111 | if (Window::keyboard_event(key, scancode, action, modifiers)) { 112 | return true; 113 | } 114 | 115 | // TODO: unify this implementation with the help window 116 | if (action == GLFW_PRESS || action == GLFW_REPEAT) { 117 | if (key == GLFW_KEY_ESCAPE || key == GLFW_KEY_Q) { 118 | mCloseCallback(); 119 | return true; 120 | } else if (key == GLFW_KEY_TAB && (modifiers & GLFW_MOD_CONTROL)) { 121 | if (modifiers & GLFW_MOD_SHIFT) { 122 | mTabWidget->set_selected_id((mTabWidget->selected_id() - 1 + mTabWidget->tab_count()) % mTabWidget->tab_count()); 123 | } else { 124 | mTabWidget->set_selected_id((mTabWidget->selected_id() + 1) % mTabWidget->tab_count()); 125 | } 126 | 127 | return true; 128 | } else if (key == GLFW_KEY_J) { 129 | if (mScrollPanel) { 130 | mScrollPanel->scroll_absolute(48.0f); 131 | } 132 | 133 | return true; 134 | } else if (key == GLFW_KEY_K) { 135 | if (mScrollPanel) { 136 | mScrollPanel->scroll_absolute(-48.0f); 137 | } 138 | 139 | return true; 140 | } 141 | } 142 | 143 | return false; 144 | } 145 | 146 | } // namespace tev 147 | -------------------------------------------------------------------------------- /include/tev/ThreadPool.h: -------------------------------------------------------------------------------- 1 | /* 2 | * tev -- the EDR viewer 3 | * 4 | * Copyright (C) 2025 Thomas Müller 5 | * 6 | * This program is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | #pragma once 20 | 21 | #include 22 | #include 23 | 24 | #include 25 | #include 26 | #include 27 | #include 28 | #include 29 | #include 30 | 31 | namespace tev { 32 | 33 | class ThreadPool { 34 | public: 35 | ThreadPool(); 36 | ThreadPool(size_t maxNumThreads, bool force = false); 37 | virtual ~ThreadPool(); 38 | 39 | static ThreadPool& global() { 40 | static ThreadPool pool; 41 | return pool; 42 | } 43 | 44 | template auto enqueueTask(F&& f, int priority) { 45 | using return_type = std::invoke_result_t; 46 | 47 | const auto task = std::make_shared>(std::forward(f)); 48 | auto res = task->get_future(); 49 | 50 | { 51 | const std::lock_guard lock{mTaskQueueMutex}; 52 | 53 | if (!mShuttingDown) { 54 | mTaskQueue.push({priority, [task]() { (*task)(); }}); 55 | ++mNumTasksInSystem; 56 | } 57 | } 58 | 59 | mWorkerCondition.notify_one(); 60 | return res; 61 | } 62 | 63 | inline auto enqueueCoroutine(int priority) noexcept { 64 | class Awaiter { 65 | public: 66 | Awaiter(ThreadPool* pool, int priority) : mPool{pool}, mPriority{priority} {} 67 | 68 | bool await_ready() const noexcept { return false; } 69 | 70 | // Suspend and enqueue coroutine continuation onto the threadpool 71 | void await_suspend(std::coroutine_handle<> coroutine) noexcept { mPool->enqueueTask(coroutine, mPriority); } 72 | 73 | void await_resume() const noexcept {} 74 | 75 | private: 76 | ThreadPool* mPool; 77 | int mPriority; 78 | }; 79 | 80 | return Awaiter{this, priority}; 81 | } 82 | 83 | template auto enqueueCoroutine(F&& fun, int priority) -> Task { 84 | return [](F&& fun, ThreadPool* pool, int tPriority) -> Task { 85 | // Makes sure the function's captures have same lifetime as coroutine 86 | auto exec = std::move(fun); 87 | co_await pool->enqueueCoroutine(tPriority); 88 | co_await exec(); 89 | }(std::forward(fun), this, priority); 90 | } 91 | 92 | void startThreads(size_t num); 93 | void shutdownThreads(size_t num); 94 | void shutdown(); 95 | 96 | size_t numTasksInSystem() const { return mNumTasksInSystem; } 97 | 98 | void waitUntilFinished(); 99 | void waitUntilFinishedFor(const std::chrono::microseconds Duration); 100 | void flushQueue(); 101 | 102 | template Task parallelForAsync(Int start, Int end, F body, int priority) { 103 | Int range = end - start; 104 | Int nTasks = std::min({(Int)mNumThreads, (Int)mHardwareConcurrency, range}); 105 | 106 | std::vector> tasks; 107 | for (Int i = 0; i < nTasks; ++i) { 108 | Int taskStart = start + (range * i / nTasks); 109 | Int taskEnd = start + (range * (i + 1) / nTasks); 110 | TEV_ASSERT(taskStart != taskEnd, "Should not produce tasks with empty range."); 111 | 112 | tasks.emplace_back([](Int tStart, Int tEnd, F tBody, int tPriority, ThreadPool* pool) -> Task { 113 | co_await pool->enqueueCoroutine(tPriority); 114 | for (Int j = tStart; j < tEnd; ++j) { 115 | tBody(j); 116 | } 117 | }(taskStart, taskEnd, body, priority, this)); 118 | } 119 | 120 | co_await awaitAll(tasks); 121 | } 122 | 123 | template void parallelFor(Int start, Int end, F body, int priority) { 124 | parallelForAsync(start, end, body, priority).get(); 125 | } 126 | 127 | size_t numThreads() const { return mNumThreads; } 128 | 129 | private: 130 | size_t mNumThreads = 0; 131 | bool mShuttingDown = false; 132 | 133 | const size_t mHardwareConcurrency = std::thread::hardware_concurrency(); 134 | std::vector mThreads; 135 | 136 | struct QueuedTask { 137 | int priority; 138 | std::function fun; 139 | 140 | struct Comparator { 141 | bool operator()(const QueuedTask& a, const QueuedTask& b) { return a.priority < b.priority; } 142 | }; 143 | }; 144 | 145 | std::priority_queue, QueuedTask::Comparator> mTaskQueue; 146 | std::mutex mTaskQueueMutex; 147 | std::condition_variable mWorkerCondition; 148 | std::condition_variable mSystemBusyCondition; 149 | 150 | std::atomic mNumTasksInSystem; 151 | }; 152 | 153 | } // namespace tev 154 | -------------------------------------------------------------------------------- /include/tev/imageio/AppleMakerNote.h: -------------------------------------------------------------------------------- 1 | /* 2 | * tev -- the EDR viewer 3 | * 4 | * Copyright (C) 2025 Thomas Müller 5 | * 6 | * This program is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | #pragma once 20 | 21 | #include 22 | 23 | #include 24 | #include 25 | #include 26 | #include 27 | 28 | namespace tev { 29 | 30 | bool isAppleMakernote(const uint8_t* data, size_t length); 31 | 32 | struct AppleMakerNoteEntry { 33 | enum class EFormat : uint16_t { 34 | Byte = 1, 35 | Ascii = 2, 36 | Short = 3, 37 | Long = 4, 38 | Rational = 5, 39 | Sbyte = 6, 40 | Undefined = 7, 41 | Sshort = 8, 42 | Slong = 9, 43 | Srational = 10, 44 | Float = 11, 45 | Double = 12, 46 | }; 47 | 48 | uint16_t tag; 49 | EFormat format; 50 | uint32_t nComponents; 51 | std::vector data; 52 | 53 | static size_t formatSize(EFormat format) { 54 | switch (format) { 55 | case EFormat::Byte: 56 | case EFormat::Ascii: 57 | case EFormat::Sbyte: 58 | case EFormat::Undefined: return 1; 59 | case EFormat::Short: 60 | case EFormat::Sshort: return 2; 61 | case EFormat::Long: 62 | case EFormat::Slong: 63 | case EFormat::Float: return 4; 64 | case EFormat::Rational: 65 | case EFormat::Srational: 66 | case EFormat::Double: return 8; 67 | default: 68 | // The default size of 4 for unknown types is chosen to make parsing easier. Larger types would be stored at a remote 69 | // location with the 4 bytes interpreted as an offset, which may be invalid depending on the indended behavior of the 70 | // unknown type. Better play it safe and just read 4 bytes, leaving it to the user to know whether they represent an offset 71 | // or a meaningful value by themselves. 72 | return 4; 73 | } 74 | 75 | throw std::invalid_argument{fmt::format("AppleMakerNoteEntry: unknown format: {}", (uint32_t)format)}; 76 | } 77 | 78 | size_t size() const { return nComponents * formatSize(format); } 79 | }; 80 | 81 | class AppleMakerNote { 82 | public: 83 | AppleMakerNote(const uint8_t* data, size_t length); 84 | 85 | template T read(const uint8_t* data) const { 86 | T result = *reinterpret_cast(data); 87 | if (mReverseEndianess) { 88 | result = swapBytes(result); 89 | } 90 | 91 | return result; 92 | } 93 | 94 | template T tryGetFloat(uint16_t tag, T defaultValue) const { 95 | if (mTags.count(tag) == 0) { 96 | return defaultValue; 97 | } 98 | 99 | return getFloat(tag); 100 | } 101 | 102 | template T getFloat(uint16_t tag) const { 103 | if (mTags.count(tag) == 0) { 104 | throw std::invalid_argument{"AppleMakerNote: requested tag does not exist."}; 105 | } 106 | 107 | const auto& entry = mTags.at(tag); 108 | const uint8_t* data = entry.data.data(); 109 | 110 | switch (entry.format) { 111 | case AppleMakerNoteEntry::EFormat::Byte: return static_cast(*data); 112 | case AppleMakerNoteEntry::EFormat::Short: return static_cast(read(data)); 113 | case AppleMakerNoteEntry::EFormat::Long: return static_cast(read(data)); 114 | case AppleMakerNoteEntry::EFormat::Rational: { 115 | uint32_t numerator = read(data); 116 | uint32_t denominator = read(data + sizeof(uint32_t)); 117 | return static_cast(numerator) / static_cast(denominator); 118 | } 119 | case AppleMakerNoteEntry::EFormat::Sbyte: return static_cast(*reinterpret_cast(data)); 120 | case AppleMakerNoteEntry::EFormat::Sshort: return static_cast(read(data)); 121 | case AppleMakerNoteEntry::EFormat::Slong: return static_cast(read(data)); 122 | case AppleMakerNoteEntry::EFormat::Srational: { 123 | int32_t numerator = read(data); 124 | int32_t denominator = read(data + sizeof(int32_t)); 125 | return static_cast(numerator) / static_cast(denominator); 126 | } 127 | case AppleMakerNoteEntry::EFormat::Float: return static_cast(*reinterpret_cast(data)); 128 | case AppleMakerNoteEntry::EFormat::Double: return static_cast(*reinterpret_cast(data)); 129 | case AppleMakerNoteEntry::EFormat::Ascii: 130 | case AppleMakerNoteEntry::EFormat::Undefined: throw std::invalid_argument{"Cannot convert this format to float."}; 131 | } 132 | 133 | throw std::invalid_argument{"Unknown format."}; 134 | } 135 | 136 | private: 137 | std::map mTags; 138 | bool mReverseEndianess = false; 139 | }; 140 | 141 | } // namespace tev 142 | -------------------------------------------------------------------------------- /src/imageio/JxlImageSaver.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * tev -- the EDR viewer 3 | * 4 | * Copyright (C) 2025 Thomas Müller 5 | * 6 | * This program is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | #include 20 | 21 | #include 22 | #include 23 | #include 24 | #include 25 | 26 | #include 27 | #include 28 | #include 29 | #include 30 | 31 | using namespace nanogui; 32 | using namespace std; 33 | 34 | namespace tev { 35 | 36 | void JxlImageSaver::save(ostream& oStream, const fs::path& path, span data, const Vector2i& imageSize, int nChannels) const { 37 | if (nChannels <= 0 || nChannels > 4098) { 38 | throw invalid_argument{fmt::format("Invalid number of channels {}.", nChannels)}; 39 | } 40 | 41 | auto encoder = JxlEncoderMake(nullptr); 42 | if (!encoder) { 43 | throw ImageSaveError{"Failed to create encoder."}; 44 | } 45 | 46 | auto runner = JxlThreadParallelRunnerMake(nullptr, thread::hardware_concurrency()); 47 | if (JXL_ENC_SUCCESS != JxlEncoderSetParallelRunner(encoder.get(), JxlThreadParallelRunner, runner.get())) { 48 | throw ImageSaveError{"Failed to set parallel runner."}; 49 | } 50 | 51 | // Configure encoder options for lossless HDR output 52 | JxlEncoderFrameSettings* options = JxlEncoderFrameSettingsCreate(encoder.get(), nullptr); 53 | if (!options) { 54 | throw ImageSaveError{"Failed to create encoder options."}; 55 | } 56 | 57 | // Set the effort level (0-9, 9 is the highest quality) 58 | if (JXL_ENC_SUCCESS != JxlEncoderFrameSettingsSetOption(options, JXL_ENC_FRAME_SETTING_EFFORT, 7)) { 59 | throw ImageSaveError{"Failed to set effort level."}; 60 | } 61 | 62 | if (JXL_ENC_SUCCESS != JxlEncoderSetFrameLossless(options, 1)) { 63 | throw ImageSaveError{"Failed to set lossless mode."}; 64 | } 65 | 66 | JxlBasicInfo basicInfo; 67 | JxlEncoderInitBasicInfo(&basicInfo); 68 | basicInfo.xsize = imageSize.x(); 69 | basicInfo.ysize = imageSize.y(); 70 | basicInfo.bits_per_sample = 32; 71 | basicInfo.exponent_bits_per_sample = 8; // ieee single precision floating point 72 | basicInfo.uses_original_profile = JXL_TRUE; 73 | 74 | bool hasAlpha = nChannels == 2 || nChannels == 4; 75 | if (hasAlpha) { 76 | basicInfo.alpha_bits = 32; 77 | basicInfo.alpha_exponent_bits = 8; 78 | basicInfo.alpha_premultiplied = JXL_TRUE; 79 | } 80 | 81 | basicInfo.num_color_channels = nChannels - (hasAlpha ? 1 : 0); 82 | basicInfo.num_extra_channels = hasAlpha ? 1 : 0; 83 | 84 | if (JXL_ENC_SUCCESS != JxlEncoderSetBasicInfo(encoder.get(), &basicInfo)) { 85 | throw ImageSaveError{"Failed to set basic info."}; 86 | } 87 | 88 | // Since JXL treats alpha channels as extra channels, a bit of redundant information needs to be attached to it here. 89 | if (hasAlpha) { 90 | JxlExtraChannelInfo alphaChannelInfo; 91 | JxlEncoderInitExtraChannelInfo(JXL_CHANNEL_ALPHA, &alphaChannelInfo); 92 | alphaChannelInfo.bits_per_sample = basicInfo.alpha_bits; 93 | alphaChannelInfo.exponent_bits_per_sample = basicInfo.alpha_exponent_bits; 94 | alphaChannelInfo.alpha_premultiplied = basicInfo.alpha_premultiplied; 95 | 96 | if (JXL_ENC_SUCCESS != JxlEncoderSetExtraChannelInfo(encoder.get(), 0, &alphaChannelInfo)) { 97 | throw ImageSaveError{ 98 | fmt::format("Failed to set extra channel info for the alpha channel: {}.", (size_t)JxlEncoderGetError(encoder.get())) 99 | }; 100 | } 101 | } 102 | 103 | JxlColorEncoding colorEncoding; 104 | JxlColorEncodingSetToLinearSRGB(&colorEncoding, nChannels == 1 ? JXL_TRUE : JXL_FALSE); // Assume 1 channel is grayscale 105 | 106 | if (JXL_ENC_SUCCESS != JxlEncoderSetColorEncoding(encoder.get(), &colorEncoding)) { 107 | throw ImageSaveError{"Failed to set color encoding."}; 108 | } 109 | 110 | JxlPixelFormat pixelFormat = { 111 | static_cast(nChannels), // num_channels 112 | JXL_TYPE_FLOAT, // data_type 113 | JXL_LITTLE_ENDIAN, // endianness 114 | 0 // align 115 | }; 116 | 117 | // Add the frame to be encoded 118 | if (JXL_ENC_SUCCESS != JxlEncoderAddImageFrame(options, &pixelFormat, data.data(), data.size() * sizeof(float))) { 119 | throw ImageSaveError{"Failed to add image frame to encoder."}; 120 | } 121 | 122 | JxlEncoderCloseInput(encoder.get()); 123 | 124 | // Encode the image and write it to file in 1mb chunks 125 | vector compressed(1024 * 1024); 126 | while (true) { 127 | uint8_t* nextOut = compressed.data(); 128 | size_t availableOut = compressed.size(); 129 | JxlEncoderStatus processResult = JxlEncoderProcessOutput(encoder.get(), &nextOut, &availableOut); 130 | if (processResult == JXL_ENC_ERROR) { 131 | throw ImageSaveError{fmt::format("Failed to process output: {}.", (size_t)JxlEncoderGetError(encoder.get()))}; 132 | } 133 | 134 | oStream.write(reinterpret_cast(compressed.data()), compressed.size() - availableOut); 135 | if (!oStream) { 136 | throw ImageSaveError{fmt::format("Failed to write data to {}.", toString(path))}; 137 | } 138 | 139 | if (processResult == JXL_ENC_SUCCESS) { 140 | break; // Encoding is done 141 | } 142 | } 143 | } 144 | 145 | } // namespace tev 146 | -------------------------------------------------------------------------------- /src/MultiGraph.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * tev -- the EDR viewer 3 | * 4 | * Copyright (C) 2025 Thomas Müller 5 | * 6 | * This program is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | // This file was adapted from the nanogui::Graph class, which was developed 20 | // by Wenzel Jakob and based on the NanoVG demo application 21 | // by Mikko Mononen. Modifications were developed by Thomas Müller . 22 | 23 | #include 24 | 25 | #include 26 | #include 27 | 28 | #include 29 | 30 | using namespace nanogui; 31 | using namespace std; 32 | 33 | namespace tev { 34 | 35 | static string formatNumber(float v) { 36 | bool needsScientificNotation = v != 0 && (abs(v) < 0.01f || abs(v) >= 1000); 37 | return needsScientificNotation ? fmt::format("{:.2e}", v) : fmt::format("{:.3f}", v); 38 | } 39 | 40 | MultiGraph::MultiGraph(Widget* parent, std::string_view caption) : Widget{parent}, mCaption{caption} { 41 | mBackgroundColor = Color(20, 128); 42 | mForegroundColor = Color(255, 192, 0, 128); 43 | mTextColor = Color(240, 192); 44 | } 45 | 46 | Vector2i MultiGraph::preferred_size_impl(NVGcontext*) const { return Vector2i(180, 80); } 47 | 48 | void MultiGraph::draw(NVGcontext* ctx) { 49 | Widget::draw(ctx); 50 | 51 | NVGpaint bg = nvgBoxGradient(ctx, m_pos.x() + 1, m_pos.y() + 1 + 1.0f, m_size.x() - 2, m_size.y() - 2, 3, 4, Color(120, 32), Color(32, 32)); 52 | 53 | nvgBeginPath(ctx); 54 | nvgRoundedRect(ctx, m_pos.x() + 1, m_pos.y() + 1 + 1.0f, m_size.x() - 2, m_size.y() - 2, 3); 55 | 56 | nvgFillPaint(ctx, bg); 57 | 58 | nvgFill(ctx); 59 | 60 | if (mValues.size() >= 2) { 61 | nvgSave(ctx); 62 | 63 | // Additive blending 64 | nvgGlobalCompositeBlendFunc(ctx, NVGblendFactor::NVG_SRC_ALPHA, NVGblendFactor::NVG_ONE); 65 | 66 | size_t nBins = mValues.size() / mNChannels; 67 | 68 | for (size_t i = 0; i < (size_t)mNChannels; i++) { 69 | nvgBeginPath(ctx); 70 | nvgMoveTo(ctx, m_pos.x(), m_pos.y() + m_size.y()); 71 | 72 | for (size_t j = 0; j < (size_t)nBins; j++) { 73 | float value = mValues[j + i * nBins]; 74 | float vx = m_pos.x() + 2 + j * (m_size.x() - 4) / (float)(nBins - 1); 75 | float vy = m_pos.y() + (1 - value) * m_size.y(); 76 | nvgLineTo(ctx, vx, vy); 77 | } 78 | 79 | auto color = i < mColors.size() ? mColors[i] : mForegroundColor; 80 | nvgLineTo(ctx, m_pos.x() + m_size.x(), m_pos.y() + m_size.y()); 81 | nvgFillColor(ctx, color); 82 | nvgFill(ctx); 83 | } 84 | 85 | nvgRestore(ctx); 86 | 87 | if (mZeroBin > 0) { 88 | nvgBeginPath(ctx); 89 | nvgRect(ctx, m_pos.x() + 1 + mZeroBin * (m_size.x() - 4) / (float)(nBins - 1), m_pos.y() + 15, 4, m_size.y() - 15); 90 | nvgFillColor(ctx, Color(0, 128)); 91 | nvgFill(ctx); 92 | nvgBeginPath(ctx); 93 | nvgRect(ctx, m_pos.x() + 2 + mZeroBin * (m_size.x() - 4) / (float)(nBins - 1), m_pos.y() + 15, 2, m_size.y() - 15); 94 | nvgFillColor(ctx, Color(200, 255)); 95 | nvgFill(ctx); 96 | } 97 | 98 | nvgFontFace(ctx, "sans"); 99 | 100 | nvgFontSize(ctx, 15.0f); 101 | nvgTextAlign(ctx, NVG_ALIGN_LEFT | NVG_ALIGN_TOP); 102 | nvgFillColor(ctx, mTextColor); 103 | drawTextWithShadow(ctx, m_pos.x() + 3, m_pos.y() + 1, formatNumber(mMinimum)); 104 | 105 | nvgTextAlign(ctx, NVG_ALIGN_MIDDLE | NVG_ALIGN_TOP); 106 | nvgFillColor(ctx, mTextColor); 107 | string meanString = formatNumber(mMean); 108 | float textWidth = nvgTextBounds(ctx, 0, 0, meanString.c_str(), nullptr, nullptr); 109 | drawTextWithShadow(ctx, m_pos.x() + (float)m_size.x() / 2 - textWidth / 2, m_pos.y() + 1, meanString); 110 | 111 | nvgTextAlign(ctx, NVG_ALIGN_RIGHT | NVG_ALIGN_TOP); 112 | nvgFillColor(ctx, mTextColor); 113 | drawTextWithShadow(ctx, m_pos.x() + m_size.x() - 3, m_pos.y() + 1, formatNumber(mMaximum)); 114 | 115 | if (!mCaption.empty()) { 116 | nvgFontSize(ctx, 14.0f); 117 | nvgTextAlign(ctx, NVG_ALIGN_LEFT | NVG_ALIGN_TOP); 118 | nvgFillColor(ctx, mTextColor); 119 | nvgText(ctx, m_pos.x() + 3, m_pos.y() + 1, mCaption.data(), mCaption.data() + mCaption.size()); 120 | } 121 | 122 | if (!mHeader.empty()) { 123 | nvgFontSize(ctx, 18.0f); 124 | nvgTextAlign(ctx, NVG_ALIGN_RIGHT | NVG_ALIGN_TOP); 125 | nvgFillColor(ctx, mTextColor); 126 | nvgText(ctx, m_pos.x() + m_size.x() - 3, m_pos.y() + 1, mHeader.data(), mHeader.data() + mHeader.size()); 127 | } 128 | 129 | if (!mFooter.empty()) { 130 | nvgFontSize(ctx, 15.0f); 131 | nvgTextAlign(ctx, NVG_ALIGN_RIGHT | NVG_ALIGN_BOTTOM); 132 | nvgFillColor(ctx, mTextColor); 133 | nvgText(ctx, m_pos.x() + m_size.x() - 3, m_pos.y() + m_size.y() - 1, mFooter.data(), mFooter.data() + mFooter.size()); 134 | } 135 | } 136 | 137 | nvgBeginPath(ctx); 138 | nvgRect(ctx, m_pos.x(), m_pos.y(), m_size.x(), m_size.y()); 139 | nvgRoundedRect(ctx, m_pos.x(), m_pos.y(), m_size.x(), m_size.y(), 2.5f); 140 | nvgPathWinding(ctx, NVG_HOLE); 141 | nvgFillColor(ctx, Color(0.23f, 1.0f)); 142 | nvgFill(ctx); 143 | 144 | nvgBeginPath(ctx); 145 | nvgRoundedRect(ctx, m_pos.x() + 0.5f, m_pos.y() + 0.5f, m_size.x() - 1, m_size.y() - 1, 2.5f); 146 | nvgStrokeColor(ctx, Color(0, 48)); 147 | nvgStroke(ctx); 148 | } 149 | 150 | } // namespace tev 151 | -------------------------------------------------------------------------------- /src/imageio/Exif.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * tev -- the EDR viewer 3 | * 4 | * Copyright (C) 2025 Thomas Müller 5 | * 6 | * This program is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | #include 20 | #include 21 | #include 22 | 23 | #include 24 | 25 | #include 26 | #include 27 | 28 | using namespace std; 29 | 30 | namespace tev { 31 | 32 | namespace { 33 | ExifByteOrder byteOrder(bool reverseEndianness) { return reverseEndianness ? EXIF_BYTE_ORDER_MOTOROLA : EXIF_BYTE_ORDER_INTEL; } 34 | } // namespace 35 | 36 | Exif::Exif() { 37 | ScopeGuard guard{[this]() { reset(); }}; 38 | 39 | mExif = exif_data_new(); 40 | if (!mExif) { 41 | throw invalid_argument{"Failed to init EXIF decoder."}; 42 | } 43 | 44 | mExifLog = exif_log_new(); 45 | if (!mExifLog) { 46 | throw invalid_argument{"Failed to init EXIF log."}; 47 | } 48 | 49 | mExifLogError = make_unique(false); 50 | 51 | exif_log_set_func( 52 | mExifLog, 53 | [](ExifLog*, ExifLogCode kind, const char* domain, const char* format, va_list args, void* userData) { 54 | bool* error = static_cast(userData); 55 | 56 | char buf[1024]; 57 | vsnprintf(buf, sizeof(buf), format, args); 58 | string msg = fmt::format("{}: {}", domain, buf); 59 | switch (kind) { 60 | case EXIF_LOG_CODE_NONE: tlog::info() << msg; break; 61 | case EXIF_LOG_CODE_DEBUG: tlog::debug() << msg; break; 62 | case EXIF_LOG_CODE_NO_MEMORY: 63 | *error = true; 64 | tlog::error() << msg; 65 | break; 66 | case EXIF_LOG_CODE_CORRUPT_DATA: 67 | *error = true; 68 | tlog::error() << msg; 69 | break; 70 | } 71 | }, 72 | mExifLogError.get() 73 | ); 74 | 75 | exif_data_log(mExif, mExifLog); 76 | 77 | guard.disarm(); 78 | } 79 | 80 | Exif::Exif(span exifData, bool autoPrependFourcc) : Exif() { 81 | ScopeGuard guard{[this]() { reset(); }}; 82 | 83 | // If data doesn't already start with fourcc, prepend 84 | vector newExifData; 85 | if (autoPrependFourcc && (exifData.size() < 6 || memcmp(exifData.data(), Exif::FOURCC.data(), 6) != 0)) { 86 | newExifData.reserve(exifData.size() + FOURCC.size()); 87 | newExifData.insert(newExifData.end(), FOURCC.begin(), FOURCC.end()); 88 | newExifData.insert(newExifData.end(), exifData.begin(), exifData.end()); 89 | exifData = newExifData; 90 | } 91 | 92 | if (exifData.size() > numeric_limits::max()) { 93 | throw invalid_argument{"EXIF data size exceeds maximum supported size."}; 94 | } 95 | 96 | exif_data_load_data(mExif, exifData.data(), (unsigned int)exifData.size()); 97 | 98 | if (*mExifLogError) { 99 | throw invalid_argument{"Failed to decode EXIF data."}; 100 | } 101 | 102 | // Uncomment to dump complete EXIF contents 103 | // tlog::debug() << "Loaded EXIF data. Entries:"; 104 | // if (tlog::Logger::global()->hiddenSeverities().count(tlog::ESeverity::Debug) == 0) { 105 | // exif_data_dump(mExif); 106 | // } 107 | 108 | auto exifByteOrder = exif_data_get_byte_order(mExif); 109 | auto systemByteOrder = endian::native == std::endian::little ? EXIF_BYTE_ORDER_INTEL : EXIF_BYTE_ORDER_MOTOROLA; 110 | mReverseEndianess = exifByteOrder != systemByteOrder; 111 | 112 | guard.disarm(); 113 | } 114 | 115 | Exif::~Exif() { reset(); } 116 | 117 | void Exif::reset() { 118 | if (mExifLog) { 119 | exif_log_unref(mExifLog); 120 | mExifLog = nullptr; 121 | } 122 | 123 | if (mExif) { 124 | exif_data_unref(mExif); 125 | mExif = nullptr; 126 | } 127 | } 128 | 129 | AppleMakerNote Exif::tryGetAppleMakerNote() const { 130 | const ExifEntry* makerNote = exif_data_get_entry(mExif, EXIF_TAG_MAKER_NOTE); 131 | return AppleMakerNote{makerNote->data, makerNote->size}; 132 | } 133 | 134 | EOrientation Exif::getOrientation() const { 135 | const ExifEntry* orientationEntry = exif_content_get_entry(mExif->ifd[EXIF_IFD_0], EXIF_TAG_ORIENTATION); 136 | if (!orientationEntry) { 137 | return EOrientation::None; 138 | } 139 | 140 | return (EOrientation)exif_get_short(orientationEntry->data, byteOrder(mReverseEndianess)); 141 | } 142 | 143 | AttributeNode Exif::toAttributes() const { 144 | AttributeNode result; 145 | result.name = "EXIF"; 146 | 147 | for (int ifd = EXIF_IFD_0; ifd < EXIF_IFD_COUNT; ++ifd) { 148 | const ExifContent* content = mExif->ifd[ifd]; 149 | if (content->count == 0) { 150 | continue; 151 | } 152 | 153 | AttributeNode& ifdNode = result.children.emplace_back(); 154 | 155 | ifdNode.name = exif_ifd_get_name((ExifIfd)ifd); 156 | ifdNode.type = "IFD"; 157 | 158 | for (size_t i = 0; i < content->count; ++i) { 159 | ExifEntry* entry = content->entries[i]; 160 | const char* name = exif_tag_get_name_in_ifd(entry->tag, (ExifIfd)ifd); 161 | if (!name) { 162 | continue; 163 | } 164 | 165 | const char* type = exif_format_get_name(entry->format); 166 | if (!type) { 167 | type = "unknown"; 168 | } 169 | 170 | char buf[256] = {0}; 171 | string value = exif_entry_get_value(entry, buf, sizeof(buf)); 172 | if (value.empty()) { 173 | value = "n/a"; 174 | } else if (value.length() >= 255) { 175 | value += "…"s; 176 | } 177 | 178 | ifdNode.children.push_back({name, value, type, {}}); 179 | } 180 | } 181 | 182 | return result; 183 | } 184 | 185 | } // namespace tev 186 | -------------------------------------------------------------------------------- /include/tev/VectorGraphics.h: -------------------------------------------------------------------------------- 1 | /* 2 | * tev -- the EDR viewer 3 | * 4 | * Copyright (C) 2025 Thomas Müller 5 | * 6 | * This program is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | #pragma once 20 | 21 | #include 22 | 23 | #include 24 | 25 | namespace tev { 26 | 27 | struct VgCommand { 28 | enum class EType : int8_t { 29 | Invalid = 127, 30 | Save = 0, 31 | Restore = 1, 32 | FillColor = 2, 33 | Fill = 3, 34 | StrokeColor = 4, 35 | Stroke = 5, 36 | BeginPath = 6, 37 | ClosePath = 7, 38 | PathWinding = 8, 39 | DebugDumpPathCache = 9, 40 | MoveTo = 10, 41 | LineTo = 11, 42 | ArcTo = 12, 43 | Arc = 13, 44 | BezierTo = 14, 45 | Circle = 15, 46 | Ellipse = 16, 47 | QuadTo = 17, 48 | Rect = 18, 49 | RoundedRect = 19, 50 | RoundedRectVarying = 20, 51 | }; 52 | 53 | VgCommand() : type{EType::Invalid} {} 54 | VgCommand(EType _type, std::span _data) : type{_type}, data{_data.begin(), _data.end()} { 55 | if (size() != data.size()) { 56 | throw std::runtime_error{"VgCommand constructed with invalid amount of data"}; 57 | } 58 | } 59 | 60 | enum EWinding : int { 61 | CounterClockwise = 1, 62 | Clockwise = 2, 63 | }; 64 | 65 | struct Pos { 66 | float x, y; 67 | }; 68 | 69 | struct Size { 70 | float width, height; 71 | }; 72 | 73 | struct Color { 74 | float r, g, b, a; 75 | }; 76 | 77 | // Returns the expected (not actual) size of `data` in number of bytes, depending on the type of the command. 78 | size_t bytes() const { 79 | switch (type) { 80 | case EType::Save: return 0; 81 | case EType::Restore: return 0; 82 | case EType::FillColor: return sizeof(Color); 83 | case EType::Fill: return 0; 84 | case EType::StrokeColor: return sizeof(Color); 85 | case EType::Stroke: return 0; 86 | case EType::BeginPath: return 0; 87 | case EType::ClosePath: return 0; 88 | case EType::PathWinding: return sizeof(float); 89 | case EType::DebugDumpPathCache: return 0; 90 | case EType::MoveTo: return sizeof(Pos); 91 | case EType::LineTo: return sizeof(Pos); 92 | case EType::ArcTo: return sizeof(Pos) * 2 + sizeof(float) /* radius */; 93 | case EType::Arc: return sizeof(Pos) + sizeof(float) * 4 /* radius, 2 angles, winding */; 94 | case EType::BezierTo: return sizeof(Pos) * 3 /* 2 control points, end point */; 95 | case EType::Circle: return sizeof(Pos) + sizeof(float) /* radius */; 96 | case EType::Ellipse: return sizeof(Pos) + sizeof(Size); 97 | case EType::QuadTo: return sizeof(Pos) * 2 /* control point, end point */; 98 | case EType::Rect: return sizeof(Pos) + sizeof(Size); 99 | case EType::RoundedRect: return sizeof(Pos) + sizeof(Size) + sizeof(float) /* radius */; 100 | case EType::RoundedRectVarying: return sizeof(Pos) + sizeof(Size) + sizeof(float) * 4 /* radius per corner */; 101 | default: throw std::runtime_error{"Invalid VgCommand type."}; 102 | } 103 | } 104 | 105 | // Returns the expected size of `data` in number of floats, depending on the type of the command. 106 | size_t size() const { return bytes() / sizeof(float); } 107 | 108 | static VgCommand save() { return {EType::Save, {}}; } 109 | static VgCommand restore() { return {EType::Restore, {}}; } 110 | 111 | static VgCommand fillColor(const Color& c) { return {EType::FillColor, {{c.r, c.g, c.b, c.a}}}; } 112 | static VgCommand fill() { return {EType::Fill, {}}; } 113 | 114 | static VgCommand strokeColor(const Color& c) { return {EType::StrokeColor, {{c.r, c.g, c.b, c.a}}}; } 115 | static VgCommand stroke() { return {EType::Stroke, {}}; } 116 | 117 | static VgCommand beginPath() { return {EType::BeginPath, {}}; } 118 | static VgCommand closePath() { return {EType::ClosePath, {}}; } 119 | static VgCommand pathWinding(EWinding winding) { return {EType::PathWinding, {{(float)(int)winding}}}; } 120 | 121 | static VgCommand moveTo(const Pos& p) { return {EType::MoveTo, {{p.x, p.y}}}; } 122 | 123 | static VgCommand lineTo(const Pos& p) { return {EType::LineTo, {{p.x, p.y}}}; } 124 | 125 | static VgCommand arcTo(const Pos& p1, const Pos& p2, float radius) { return {EType::ArcTo, {{p1.x, p1.y, p2.x, p2.y, radius}}}; } 126 | 127 | static VgCommand arc(const Pos& center, float radius, float angle_begin, float angle_end, EWinding winding) { 128 | return {EType::Arc, {{center.x, center.y, radius, angle_begin, angle_end, (float)(int)winding}}}; 129 | } 130 | 131 | static VgCommand bezierTo(const Pos& c1, const Pos& c2, const Pos& p) { 132 | return {EType::BezierTo, {{c1.x, c1.y, c2.x, c2.y, p.x, p.y}}}; 133 | } 134 | 135 | static VgCommand circle(const Pos& center, float radius) { return {EType::Circle, {{center.x, center.y, radius}}}; } 136 | 137 | static VgCommand ellipse(const Pos& center, const Size& radius) { 138 | return {EType::Ellipse, {{center.x, center.y, radius.width, radius.height}}}; 139 | } 140 | 141 | static VgCommand quadTo(const Pos& c, const Pos& p) { return {EType::QuadTo, {{c.x, c.y, p.x, p.y}}}; } 142 | 143 | static VgCommand rect(const Pos& p, const Size& size) { return {EType::Rect, {{p.x, p.y, size.width, size.height}}}; } 144 | 145 | static VgCommand roundedRect(const Pos& p, const Size& size, float radius) { 146 | return {EType::RoundedRect, {{p.x, p.y, size.width, size.height, radius}}}; 147 | } 148 | 149 | static VgCommand roundedRectVarying( 150 | const Pos& p, const Size& size, float radiusTopLeft, float radiusTopRight, float radiusBottomRight, float radiusBottomLeft 151 | ) { 152 | return { 153 | EType::RoundedRectVarying, 154 | {{p.x, p.y, size.width, size.height, radiusTopLeft, radiusTopRight, radiusBottomRight, radiusBottomLeft}} 155 | }; 156 | } 157 | 158 | EType type; 159 | std::vector data; 160 | }; 161 | 162 | } // namespace tev 163 | -------------------------------------------------------------------------------- /include/tev/Channel.h: -------------------------------------------------------------------------------- 1 | /* 2 | * tev -- the EDR viewer 3 | * 4 | * Copyright (C) 2025 Thomas Müller 5 | * 6 | * This program is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | #pragma once 20 | 21 | #include 22 | #include 23 | 24 | #include 25 | 26 | #include 27 | 28 | #include 29 | #include 30 | #include 31 | 32 | namespace tev { 33 | 34 | class Channel { 35 | public: 36 | static std::pair split(std::string_view fullChannel); 37 | 38 | static std::string_view tail(std::string_view fullChannel); 39 | static std::string_view head(std::string_view fullChannel); 40 | 41 | static bool isTopmost(std::string_view fullChannel); 42 | static bool isAlpha(std::string_view fullChannel); 43 | 44 | static nanogui::Color color(std::string_view fullChannel, bool pastel); 45 | 46 | Channel( 47 | std::string_view name, 48 | const nanogui::Vector2i& size, 49 | EPixelFormat format, 50 | EPixelFormat desiredFormat, 51 | std::shared_ptr> data = nullptr, 52 | size_t dataOffset = 0, 53 | size_t dataStride = 1 54 | ); 55 | 56 | std::string_view name() const { return mName; } 57 | void setName(std::string_view name) { mName = name; } 58 | 59 | size_t numPixels() const { return (size_t)mSize.x() * mSize.y(); } 60 | 61 | const nanogui::Vector2i& size() const { return mSize; } 62 | void setSize(const nanogui::Vector2i& size) { mSize = size; } 63 | 64 | std::tuple minMaxMean() const { 65 | float min = std::numeric_limits::infinity(); 66 | float max = -std::numeric_limits::infinity(); 67 | float mean = 0; 68 | 69 | const size_t nPixels = numPixels(); 70 | for (size_t i = 0; i < nPixels; ++i) { 71 | const float f = at(i); 72 | 73 | mean += f; 74 | if (f < min) { 75 | min = f; 76 | } 77 | 78 | if (f > max) { 79 | max = f; 80 | } 81 | } 82 | 83 | return {min, max, mean / nPixels}; 84 | } 85 | 86 | Task divideByAsync(const Channel& other, int priority); 87 | Task multiplyWithAsync(const Channel& other, int priority); 88 | 89 | void setZero() { 90 | const size_t nBytesPerPixel = nBytes(mPixelFormat); 91 | if (mDataStride == 1) { 92 | std::memset(data(), 0, numPixels() * nBytesPerPixel); 93 | } else { 94 | const size_t nPixels = numPixels(); 95 | for (size_t i = 0; i < nPixels; ++i) { 96 | std::memset(data() + i * mDataStride, 0, nBytesPerPixel); 97 | } 98 | } 99 | } 100 | 101 | void updateTile(int x, int y, int width, int height, std::span newData); 102 | 103 | float at(nanogui::Vector2i index) const { return at(index.x() + index.y() * (size_t)mSize.x()); } 104 | float at(size_t index) const { 105 | switch (mPixelFormat) { 106 | case EPixelFormat::U8: return *dataAt(index); 107 | case EPixelFormat::U16: return *(const uint16_t*)dataAt(index); 108 | case EPixelFormat::F16: return *(const half*)dataAt(index); 109 | case EPixelFormat::F32: return *(const float*)dataAt(index); 110 | } 111 | 112 | return 0; 113 | } 114 | 115 | void setAt(nanogui::Vector2i index, float value) { setAt(index.x() + index.y() * (size_t)mSize.x(), value); } 116 | void setAt(size_t index, float value) { 117 | switch (mPixelFormat) { 118 | case EPixelFormat::U8: *dataAt(index) = (uint8_t)value; break; 119 | case EPixelFormat::U16: *(uint16_t*)dataAt(index) = (uint16_t)value; break; 120 | case EPixelFormat::F16: *(half*)dataAt(index) = (half)value; break; 121 | case EPixelFormat::F32: *(float*)dataAt(index) = value; break; 122 | } 123 | } 124 | 125 | float eval(nanogui::Vector2i index) const { 126 | if (index.x() < 0 || index.x() >= mSize.x() || index.y() < 0 || index.y() >= mSize.y()) { 127 | return 0; 128 | } 129 | 130 | return at(index.x() + (size_t)index.y() * (size_t)mSize.x()); 131 | } 132 | 133 | uint8_t* data() const { return mData->data() + mDataOffset; } 134 | 135 | uint8_t* dataAt(nanogui::Vector2i index) const { return dataAt(index.x() + index.y() * (size_t)mSize.x()); } 136 | uint8_t* dataAt(size_t index) const { return data() + index * mDataStride; } 137 | 138 | template T typedDataAt(T* src, nanogui::Vector2i index) const { 139 | return typedDataAt(src, index.x() + index.y() * (size_t)mSize.x()); 140 | } 141 | 142 | template T typedDataAt(T* src, size_t index) const { return src[index * mDataStride / sizeof(T)]; } 143 | 144 | float* floatData() const { 145 | if (mPixelFormat != EPixelFormat::F32) { 146 | throw std::runtime_error{"Channel is not in F32 format."}; 147 | } 148 | 149 | return (float*)data(); 150 | } 151 | 152 | half* halfData() const { 153 | if (mPixelFormat != EPixelFormat::F16) { 154 | throw std::runtime_error{"Channel is not in F16 format."}; 155 | } 156 | 157 | return (half*)data(); 158 | } 159 | 160 | void setOffset(size_t offset) { mDataOffset = offset; } 161 | size_t offset() const { return mDataOffset; } 162 | 163 | void setStride(size_t stride) { mDataStride = stride; } 164 | size_t stride() const { return mDataStride; } 165 | 166 | std::shared_ptr>& dataBuf() { return mData; } 167 | const std::shared_ptr>& dataBuf() const { return mData; } 168 | 169 | EPixelFormat desiredPixelFormat() const { return mDesiredPixelFormat; } 170 | 171 | void setPixelFormat(EPixelFormat format) { mPixelFormat = format; } 172 | EPixelFormat pixelFormat() const { return mPixelFormat; } 173 | 174 | private: 175 | std::string mName; 176 | nanogui::Vector2i mSize; 177 | 178 | EPixelFormat mPixelFormat = EPixelFormat::F32; 179 | 180 | // tev defaults to storing images in fp32 for maximum precision. However, many images only require fp16 to be displayed as good as 181 | // losslessly. For such images, loaders can set this to F16 to save memory. 182 | EPixelFormat mDesiredPixelFormat = EPixelFormat::F32; 183 | 184 | std::shared_ptr> mData; 185 | size_t mDataOffset; 186 | size_t mDataStride; 187 | }; 188 | 189 | } // namespace tev 190 | -------------------------------------------------------------------------------- /include/tev/ImageCanvas.h: -------------------------------------------------------------------------------- 1 | /* 2 | * tev -- the EDR viewer 3 | * 4 | * Copyright (C) 2025 Thomas Müller 5 | * 6 | * This program is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | #pragma once 20 | 21 | #include 22 | #include 23 | #include 24 | #include 25 | #include 26 | 27 | #include 28 | 29 | #include 30 | #include 31 | 32 | namespace tev { 33 | 34 | struct CanvasStatistics { 35 | float mean; 36 | float maximum; 37 | float minimum; 38 | std::vector histogram; 39 | std::vector histogramColors; 40 | int nChannels; 41 | int histogramZero; 42 | }; 43 | 44 | class ImageCanvas : public nanogui::Canvas { 45 | public: 46 | ImageCanvas(nanogui::Widget* parent); 47 | 48 | bool scroll_event(const nanogui::Vector2i& p, const nanogui::Vector2f& rel) override; 49 | 50 | void draw_contents() override; 51 | 52 | void draw(NVGcontext* ctx) override; 53 | 54 | void translate(const nanogui::Vector2f& amount); 55 | void scale(float amount, const nanogui::Vector2f& origin); 56 | float scale() const { return extractScale(mTransform); } 57 | 58 | void setExposure(float exposure) { mExposure = exposure; } 59 | void setOffset(float offset) { mOffset = offset; } 60 | void setGamma(float gamma) { mGamma = gamma; } 61 | 62 | float applyExposureAndOffset(float value) const; 63 | 64 | void setImage(std::shared_ptr image) { mImage = image; } 65 | void setReference(std::shared_ptr reference) { mReference = reference; } 66 | void setRequestedChannelGroup(std::string_view groupName) { mRequestedChannelGroup = groupName; } 67 | 68 | nanogui::Vector2i getImageCoords(const Image* image, nanogui::Vector2i mousePos); 69 | nanogui::Vector2i getDisplayWindowCoords(const Image* image, nanogui::Vector2i mousePos); 70 | 71 | void getValuesAtNanoPos(nanogui::Vector2i nanoPos, std::vector& result, std::span channels); 72 | std::vector getValuesAtNanoPos(nanogui::Vector2i nanoPos, std::span channels) { 73 | std::vector result; 74 | getValuesAtNanoPos(nanoPos, result, channels); 75 | return result; 76 | } 77 | 78 | ETonemap tonemap() const { return mTonemap; } 79 | void setTonemap(ETonemap tonemap) { mTonemap = tonemap; } 80 | 81 | static nanogui::Vector3f applyTonemap(const nanogui::Vector3f& value, float gamma, ETonemap tonemap); 82 | nanogui::Vector3f applyTonemap(const nanogui::Vector3f& value) const { return applyTonemap(value, mGamma, mTonemap); } 83 | 84 | EMetric metric() const { return mMetric; } 85 | void setMetric(EMetric metric) { mMetric = metric; } 86 | 87 | static float applyMetric(float value, float reference, EMetric metric); 88 | float applyMetric(float value, float reference) const { return applyMetric(value, reference, mMetric); } 89 | 90 | std::optional crop() { return mCrop; } 91 | void setCrop(const std::optional& crop) { mCrop = crop; } 92 | Box2i cropInImageCoords() const; 93 | 94 | void fitImageToScreen(const Image& image); 95 | void resetTransform(); 96 | 97 | std::optional whiteLevelOverride() const { return mWhiteLevelOverride; } 98 | void setWhiteLevelOverride(std::optional value) { mWhiteLevelOverride = value; } 99 | 100 | bool clipToLdr() const { return mClipToLdr; } 101 | void setClipToLdr(bool value) { mClipToLdr = value; } 102 | 103 | auto backgroundColor() { return mBackgroundColor; } 104 | void setBackgroundColor(const nanogui::Color& color) { mBackgroundColor = color; } 105 | 106 | EInterpolationMode minFilter() const { return mMinFilter; } 107 | void setMinFilter(EInterpolationMode value) { mMinFilter = value; } 108 | 109 | EInterpolationMode magFilter() const { return mMagFilter; } 110 | void setMagFilter(EInterpolationMode value) { mMagFilter = value; } 111 | 112 | // The following functions return four values per pixel in RGBA order. The number of pixels is given by `imageDataSize()`. If the canvas 113 | // does not currently hold an image, or no channels are displayed, then zero pixels are returned. 114 | nanogui::Vector2i imageDataSize() const { return cropInImageCoords().size(); } 115 | std::vector getHdrImageData(bool divideAlpha, int priority) const; 116 | std::vector getLdrImageData(bool divideAlpha, int priority) const; 117 | 118 | void saveImage(const fs::path& filename) const; 119 | 120 | std::shared_ptr>> canvasStatistics(); 121 | 122 | void purgeCanvasStatistics(int imageId); 123 | 124 | float pixelRatio() const { return mPixelRatio; } 125 | void setPixelRatio(float ratio) { mPixelRatio = ratio; } 126 | 127 | private: 128 | static std::vector channelsFromImages( 129 | std::shared_ptr image, std::shared_ptr reference, std::string_view requestedChannelGroup, EMetric metric, int priority 130 | ); 131 | 132 | static Task> computeCanvasStatistics( 133 | std::shared_ptr image, 134 | std::shared_ptr reference, 135 | std::string_view requestedChannelGroup, 136 | EMetric metric, 137 | const Box2i& region, 138 | int priority 139 | ); 140 | 141 | void drawPixelValuesAsText(NVGcontext* ctx); 142 | void drawCoordinateSystem(NVGcontext* ctx); 143 | void drawEdgeShadows(NVGcontext* ctx); 144 | 145 | nanogui::Vector2f pixelOffset(const nanogui::Vector2i& size) const; 146 | 147 | // Assembles the transform from canonical space to the [-1, 1] square for the current image. 148 | nanogui::Matrix3f transform(const Image* image); 149 | nanogui::Matrix3f textureToNanogui(const Image* image); 150 | nanogui::Matrix3f displayWindowToNanogui(const Image* image); 151 | 152 | float mPixelRatio = 1; 153 | float mExposure = 0; 154 | float mOffset = 0; 155 | float mGamma = 2.2f; 156 | 157 | std::optional mWhiteLevelOverride = std::nullopt; 158 | bool mClipToLdr = false; 159 | nanogui::Color mBackgroundColor = nanogui::Color(0, 0, 0, 0); 160 | 161 | EInterpolationMode mMinFilter = EInterpolationMode::Trilinear; 162 | EInterpolationMode mMagFilter = EInterpolationMode::Nearest; 163 | 164 | std::shared_ptr mImage; 165 | std::shared_ptr mReference; 166 | 167 | std::string mRequestedChannelGroup = ""; 168 | 169 | nanogui::Matrix3f mTransform = nanogui::Matrix3f::scale(nanogui::Vector3f(1.0f)); 170 | 171 | std::unique_ptr mShader; 172 | 173 | ETonemap mTonemap = ETonemap::None; 174 | EMetric mMetric = EMetric::Error; 175 | std::optional mCrop; 176 | 177 | std::map>>> mCanvasStatistics; 178 | std::map> mImageIdToCanvasStatisticsKey; 179 | }; 180 | 181 | } // namespace tev 182 | -------------------------------------------------------------------------------- /scripts/turbo_colormap.py: -------------------------------------------------------------------------------- 1 | # Copyright 2019 Google LLC. 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | # Author: Anton Mikhailov 5 | 6 | turbo_colormap_data = [[0.18995,0.07176,0.23217],[0.19483,0.08339,0.26149],[0.19956,0.09498,0.29024],[0.20415,0.10652,0.31844],[0.20860,0.11802,0.34607],[0.21291,0.12947,0.37314],[0.21708,0.14087,0.39964],[0.22111,0.15223,0.42558],[0.22500,0.16354,0.45096],[0.22875,0.17481,0.47578],[0.23236,0.18603,0.50004],[0.23582,0.19720,0.52373],[0.23915,0.20833,0.54686],[0.24234,0.21941,0.56942],[0.24539,0.23044,0.59142],[0.24830,0.24143,0.61286],[0.25107,0.25237,0.63374],[0.25369,0.26327,0.65406],[0.25618,0.27412,0.67381],[0.25853,0.28492,0.69300],[0.26074,0.29568,0.71162],[0.26280,0.30639,0.72968],[0.26473,0.31706,0.74718],[0.26652,0.32768,0.76412],[0.26816,0.33825,0.78050],[0.26967,0.34878,0.79631],[0.27103,0.35926,0.81156],[0.27226,0.36970,0.82624],[0.27334,0.38008,0.84037],[0.27429,0.39043,0.85393],[0.27509,0.40072,0.86692],[0.27576,0.41097,0.87936],[0.27628,0.42118,0.89123],[0.27667,0.43134,0.90254],[0.27691,0.44145,0.91328],[0.27701,0.45152,0.92347],[0.27698,0.46153,0.93309],[0.27680,0.47151,0.94214],[0.27648,0.48144,0.95064],[0.27603,0.49132,0.95857],[0.27543,0.50115,0.96594],[0.27469,0.51094,0.97275],[0.27381,0.52069,0.97899],[0.27273,0.53040,0.98461],[0.27106,0.54015,0.98930],[0.26878,0.54995,0.99303],[0.26592,0.55979,0.99583],[0.26252,0.56967,0.99773],[0.25862,0.57958,0.99876],[0.25425,0.58950,0.99896],[0.24946,0.59943,0.99835],[0.24427,0.60937,0.99697],[0.23874,0.61931,0.99485],[0.23288,0.62923,0.99202],[0.22676,0.63913,0.98851],[0.22039,0.64901,0.98436],[0.21382,0.65886,0.97959],[0.20708,0.66866,0.97423],[0.20021,0.67842,0.96833],[0.19326,0.68812,0.96190],[0.18625,0.69775,0.95498],[0.17923,0.70732,0.94761],[0.17223,0.71680,0.93981],[0.16529,0.72620,0.93161],[0.15844,0.73551,0.92305],[0.15173,0.74472,0.91416],[0.14519,0.75381,0.90496],[0.13886,0.76279,0.89550],[0.13278,0.77165,0.88580],[0.12698,0.78037,0.87590],[0.12151,0.78896,0.86581],[0.11639,0.79740,0.85559],[0.11167,0.80569,0.84525],[0.10738,0.81381,0.83484],[0.10357,0.82177,0.82437],[0.10026,0.82955,0.81389],[0.09750,0.83714,0.80342],[0.09532,0.84455,0.79299],[0.09377,0.85175,0.78264],[0.09287,0.85875,0.77240],[0.09267,0.86554,0.76230],[0.09320,0.87211,0.75237],[0.09451,0.87844,0.74265],[0.09662,0.88454,0.73316],[0.09958,0.89040,0.72393],[0.10342,0.89600,0.71500],[0.10815,0.90142,0.70599],[0.11374,0.90673,0.69651],[0.12014,0.91193,0.68660],[0.12733,0.91701,0.67627],[0.13526,0.92197,0.66556],[0.14391,0.92680,0.65448],[0.15323,0.93151,0.64308],[0.16319,0.93609,0.63137],[0.17377,0.94053,0.61938],[0.18491,0.94484,0.60713],[0.19659,0.94901,0.59466],[0.20877,0.95304,0.58199],[0.22142,0.95692,0.56914],[0.23449,0.96065,0.55614],[0.24797,0.96423,0.54303],[0.26180,0.96765,0.52981],[0.27597,0.97092,0.51653],[0.29042,0.97403,0.50321],[0.30513,0.97697,0.48987],[0.32006,0.97974,0.47654],[0.33517,0.98234,0.46325],[0.35043,0.98477,0.45002],[0.36581,0.98702,0.43688],[0.38127,0.98909,0.42386],[0.39678,0.99098,0.41098],[0.41229,0.99268,0.39826],[0.42778,0.99419,0.38575],[0.44321,0.99551,0.37345],[0.45854,0.99663,0.36140],[0.47375,0.99755,0.34963],[0.48879,0.99828,0.33816],[0.50362,0.99879,0.32701],[0.51822,0.99910,0.31622],[0.53255,0.99919,0.30581],[0.54658,0.99907,0.29581],[0.56026,0.99873,0.28623],[0.57357,0.99817,0.27712],[0.58646,0.99739,0.26849],[0.59891,0.99638,0.26038],[0.61088,0.99514,0.25280],[0.62233,0.99366,0.24579],[0.63323,0.99195,0.23937],[0.64362,0.98999,0.23356],[0.65394,0.98775,0.22835],[0.66428,0.98524,0.22370],[0.67462,0.98246,0.21960],[0.68494,0.97941,0.21602],[0.69525,0.97610,0.21294],[0.70553,0.97255,0.21032],[0.71577,0.96875,0.20815],[0.72596,0.96470,0.20640],[0.73610,0.96043,0.20504],[0.74617,0.95593,0.20406],[0.75617,0.95121,0.20343],[0.76608,0.94627,0.20311],[0.77591,0.94113,0.20310],[0.78563,0.93579,0.20336],[0.79524,0.93025,0.20386],[0.80473,0.92452,0.20459],[0.81410,0.91861,0.20552],[0.82333,0.91253,0.20663],[0.83241,0.90627,0.20788],[0.84133,0.89986,0.20926],[0.85010,0.89328,0.21074],[0.85868,0.88655,0.21230],[0.86709,0.87968,0.21391],[0.87530,0.87267,0.21555],[0.88331,0.86553,0.21719],[0.89112,0.85826,0.21880],[0.89870,0.85087,0.22038],[0.90605,0.84337,0.22188],[0.91317,0.83576,0.22328],[0.92004,0.82806,0.22456],[0.92666,0.82025,0.22570],[0.93301,0.81236,0.22667],[0.93909,0.80439,0.22744],[0.94489,0.79634,0.22800],[0.95039,0.78823,0.22831],[0.95560,0.78005,0.22836],[0.96049,0.77181,0.22811],[0.96507,0.76352,0.22754],[0.96931,0.75519,0.22663],[0.97323,0.74682,0.22536],[0.97679,0.73842,0.22369],[0.98000,0.73000,0.22161],[0.98289,0.72140,0.21918],[0.98549,0.71250,0.21650],[0.98781,0.70330,0.21358],[0.98986,0.69382,0.21043],[0.99163,0.68408,0.20706],[0.99314,0.67408,0.20348],[0.99438,0.66386,0.19971],[0.99535,0.65341,0.19577],[0.99607,0.64277,0.19165],[0.99654,0.63193,0.18738],[0.99675,0.62093,0.18297],[0.99672,0.60977,0.17842],[0.99644,0.59846,0.17376],[0.99593,0.58703,0.16899],[0.99517,0.57549,0.16412],[0.99419,0.56386,0.15918],[0.99297,0.55214,0.15417],[0.99153,0.54036,0.14910],[0.98987,0.52854,0.14398],[0.98799,0.51667,0.13883],[0.98590,0.50479,0.13367],[0.98360,0.49291,0.12849],[0.98108,0.48104,0.12332],[0.97837,0.46920,0.11817],[0.97545,0.45740,0.11305],[0.97234,0.44565,0.10797],[0.96904,0.43399,0.10294],[0.96555,0.42241,0.09798],[0.96187,0.41093,0.09310],[0.95801,0.39958,0.08831],[0.95398,0.38836,0.08362],[0.94977,0.37729,0.07905],[0.94538,0.36638,0.07461],[0.94084,0.35566,0.07031],[0.93612,0.34513,0.06616],[0.93125,0.33482,0.06218],[0.92623,0.32473,0.05837],[0.92105,0.31489,0.05475],[0.91572,0.30530,0.05134],[0.91024,0.29599,0.04814],[0.90463,0.28696,0.04516],[0.89888,0.27824,0.04243],[0.89298,0.26981,0.03993],[0.88691,0.26152,0.03753],[0.88066,0.25334,0.03521],[0.87422,0.24526,0.03297],[0.86760,0.23730,0.03082],[0.86079,0.22945,0.02875],[0.85380,0.22170,0.02677],[0.84662,0.21407,0.02487],[0.83926,0.20654,0.02305],[0.83172,0.19912,0.02131],[0.82399,0.19182,0.01966],[0.81608,0.18462,0.01809],[0.80799,0.17753,0.01660],[0.79971,0.17055,0.01520],[0.79125,0.16368,0.01387],[0.78260,0.15693,0.01264],[0.77377,0.15028,0.01148],[0.76476,0.14374,0.01041],[0.75556,0.13731,0.00942],[0.74617,0.13098,0.00851],[0.73661,0.12477,0.00769],[0.72686,0.11867,0.00695],[0.71692,0.11268,0.00629],[0.70680,0.10680,0.00571],[0.69650,0.10102,0.00522],[0.68602,0.09536,0.00481],[0.67535,0.08980,0.00449],[0.66449,0.08436,0.00424],[0.65345,0.07902,0.00408],[0.64223,0.07380,0.00401],[0.63082,0.06868,0.00401],[0.61923,0.06367,0.00410],[0.60746,0.05878,0.00427],[0.59550,0.05399,0.00453],[0.58336,0.04931,0.00486],[0.57103,0.04474,0.00529],[0.55852,0.04028,0.00579],[0.54583,0.03593,0.00638],[0.53295,0.03169,0.00705],[0.51989,0.02756,0.00780],[0.50664,0.02354,0.00863],[0.49321,0.01963,0.00955],[0.47960,0.01583,0.01055]] 7 | 8 | # The look-up table contains 256 entries. Each entry is a floating point sRGB triplet. 9 | # To use it with matplotlib, pass cmap=ListedColormap(turbo_colormap_data) as an arg to imshow() (don't forget "from matplotlib.colors import ListedColormap"). 10 | # If you have a typical 8-bit greyscale image, you can use the 8-bit value to index into this LUT directly. 11 | # The floating point color values can be converted to 8-bit sRGB via multiplying by 255 and casting/flooring to an integer. Saturation should not be required for IEEE-754 compliant arithmetic. 12 | # If you have a floating point value in the range [0,1], you can use interpolate() to linearly interpolate between the entries. 13 | # If you have 16-bit or 32-bit integer values, convert them to floating point values on the [0,1] range and then use interpolate(). Doing the interpolation in floating point will reduce banding. 14 | # If some of your values may lie outside the [0,1] range, use interpolate_or_clip() to highlight them. 15 | 16 | def interpolate(colormap, x): 17 | x = max(0.0, min(1.0, x)) 18 | a = int(x*255.0) 19 | b = min(255, a + 1) 20 | f = x*255.0 - a 21 | return [colormap[a][0] + (colormap[b][0] - colormap[a][0]) * f, 22 | colormap[a][1] + (colormap[b][1] - colormap[a][1]) * f, 23 | colormap[a][2] + (colormap[b][2] - colormap[a][2]) * f] 24 | 25 | def interpolate_or_clip(colormap, x): 26 | if x < 0.0: return [0.0, 0.0, 0.0] 27 | elif x > 1.0: return [1.0, 1.0, 1.0] 28 | else: return interpolate(colormap, x) 29 | --------------------------------------------------------------------------------