├── resources ├── icon.ico ├── icon.png ├── icon.rc ├── icon.icns ├── 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.manifest └── Info.plist ├── scripts ├── mac-run-tev.sh ├── pull.sh ├── update-submodules.sh ├── mac-post-install.cmake ├── create-dmg │ ├── sample │ ├── builder │ │ └── create-dmg.builder │ ├── LICENSE │ ├── support │ │ ├── template.applescript │ │ └── dmg-license.py │ ├── README.md │ └── create-dmg ├── create-exe-ninja.bat ├── create-exe.bat ├── create-dmg.sh ├── create-dmg-backwards.sh ├── sample-colormap.py ├── parse-include-paths.py └── turbo_colormap.py ├── .editorconfig ├── src ├── Box.cpp ├── Lazy.cpp ├── Task.cpp ├── SharedQueue.cpp ├── imageio │ ├── StbiHdrImageSaver.cpp │ ├── ImageSaver.cpp │ ├── QoiImageSaver.cpp │ ├── StbiLdrImageSaver.cpp │ ├── ImageLoader.cpp │ ├── EmptyImageLoader.cpp │ ├── ExrImageSaver.cpp │ ├── StbiImageLoader.cpp │ ├── QoiImageLoader.cpp │ ├── PfmImageLoader.cpp │ └── ClipboardImageLoader.cpp ├── python │ ├── ipc-example.py │ └── ipc.py ├── Channel.cpp ├── ThreadPool.cpp ├── MultiGraph.cpp ├── Common.cpp ├── ImageButton.cpp └── HelpWindow.cpp ├── include └── tev │ ├── FalseColor.h │ ├── imageio │ ├── DdsImageLoader.h │ ├── PfmImageLoader.h │ ├── StbiImageLoader.h │ ├── EmptyImageLoader.h │ ├── ExrImageLoader.h │ ├── ClipboardImageLoader.h │ ├── QoiImageLoader.h │ ├── ExrImageSaver.h │ ├── StbiHdrImageSaver.h │ ├── QoiImageSaver.h │ ├── StbiLdrImageSaver.h │ ├── ImageLoader.h │ └── ImageSaver.h │ ├── HelpWindow.h │ ├── SharedQueue.h │ ├── Box.h │ ├── ImageButton.h │ ├── UberShader.h │ ├── MultiGraph.h │ ├── Channel.h │ ├── Lazy.h │ ├── ThreadPool.h │ ├── ImageCanvas.h │ ├── Task.h │ ├── ImageViewer.h │ ├── Ipc.h │ ├── Image.h │ └── Common.h ├── .gitignore ├── .gitmodules ├── LICENSE ├── .github └── workflows │ └── main.yml ├── dependencies └── CMakeLists.txt └── README.md /resources/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anderslanglands/tev/master/resources/icon.ico -------------------------------------------------------------------------------- /resources/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anderslanglands/tev/master/resources/icon.png -------------------------------------------------------------------------------- /resources/icon.rc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anderslanglands/tev/master/resources/icon.rc -------------------------------------------------------------------------------- /scripts/mac-run-tev.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | /Applications/tev.app/Contents/MacOS/tev "$@" 4 | -------------------------------------------------------------------------------- /resources/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anderslanglands/tev/master/resources/icon.icns -------------------------------------------------------------------------------- /resources/icon-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anderslanglands/tev/master/resources/icon-128.png -------------------------------------------------------------------------------- /resources/icon-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anderslanglands/tev/master/resources/icon-16.png -------------------------------------------------------------------------------- /resources/icon-24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anderslanglands/tev/master/resources/icon-24.png -------------------------------------------------------------------------------- /resources/icon-256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anderslanglands/tev/master/resources/icon-256.png -------------------------------------------------------------------------------- /resources/icon-32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anderslanglands/tev/master/resources/icon-32.png -------------------------------------------------------------------------------- /resources/icon-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anderslanglands/tev/master/resources/icon-48.png -------------------------------------------------------------------------------- /resources/icon-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anderslanglands/tev/master/resources/icon-512.png -------------------------------------------------------------------------------- /resources/icon-64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anderslanglands/tev/master/resources/icon-64.png -------------------------------------------------------------------------------- /resources/icon-96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anderslanglands/tev/master/resources/icon-96.png -------------------------------------------------------------------------------- /resources/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anderslanglands/tev/master/resources/screenshot.png -------------------------------------------------------------------------------- /scripts/pull.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | cd "$(dirname ${BASH_SOURCE[0]})/.." 4 | git pull 5 | ./scripts/update-submodules.sh 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 | trim_trailing_whitespace = true 9 | -------------------------------------------------------------------------------- /scripts/update-submodules.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | cd "$(dirname ${BASH_SOURCE[0]})/.." 4 | git submodule update --recursive 5 | git submodule sync --recursive 6 | git submodule update --recursive 7 | -------------------------------------------------------------------------------- /src/Box.cpp: -------------------------------------------------------------------------------- 1 | // This file was developed by Thomas Müller . 2 | // It is published under the BSD 3-Clause License within the LICENSE file. 3 | 4 | #include 5 | 6 | TEV_NAMESPACE_BEGIN 7 | 8 | TEV_NAMESPACE_END 9 | -------------------------------------------------------------------------------- /src/Lazy.cpp: -------------------------------------------------------------------------------- 1 | // This file was developed by Thomas Müller . 2 | // It is published under the BSD 3-Clause License within the LICENSE file. 3 | 4 | #include 5 | 6 | TEV_NAMESPACE_BEGIN 7 | 8 | TEV_NAMESPACE_END 9 | -------------------------------------------------------------------------------- /src/Task.cpp: -------------------------------------------------------------------------------- 1 | // This file was developed by Thomas Müller . 2 | // It is published under the BSD 3-Clause License within the LICENSE file. 3 | 4 | #include 5 | 6 | TEV_NAMESPACE_BEGIN 7 | 8 | TEV_NAMESPACE_END 9 | -------------------------------------------------------------------------------- /src/SharedQueue.cpp: -------------------------------------------------------------------------------- 1 | // This file was developed by Thomas Müller . 2 | // It is published under the BSD 3-Clause License within the LICENSE file. 3 | 4 | #include 5 | 6 | TEV_NAMESPACE_BEGIN 7 | 8 | TEV_NAMESPACE_END 9 | -------------------------------------------------------------------------------- /resources/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 comparison tool. 7 | Exec=${CMAKE_INSTALL_FULL_BINDIR}/tev %F 8 | Icon=tev.png 9 | Terminal=false 10 | MimeType=image/*; 11 | Categories=Graphics; 12 | -------------------------------------------------------------------------------- /scripts/mac-post-install.cmake: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.12) 2 | 3 | # We want to explicitly symlink the newly installed app bundle 4 | # such that the icon and executable match up correctly. 5 | execute_process(COMMAND rm -f /usr/local/bin/tev) 6 | execute_process(COMMAND ln -s /Applications/tev.app/Contents/Resources/mac-run-tev.sh /usr/local/bin/tev) 7 | -------------------------------------------------------------------------------- /scripts/create-dmg/sample: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | test -f test2.dmg && rm test2.dmg 3 | ./create-dmg --window-size 500 300 --background ~/Projects/eclipse-osx-repackager/build/background.gif --icon-size 96 --volname "Hyper Foo" --app-drop-link 380 205 --icon "Eclipse OS X Repackager" 110 205 test2.dmg /Users/andreyvit/Projects/eclipse-osx-repackager/temp/Eclipse\ OS\ X\ Repackager\ r10/ 4 | -------------------------------------------------------------------------------- /resources/tev.manifest: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | true 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /include/tev/FalseColor.h: -------------------------------------------------------------------------------- 1 | // This file was developed by Thomas Müller . 2 | // It is published under the BSD 3-Clause License within the LICENSE file. 3 | 4 | #pragma once 5 | 6 | #include 7 | 8 | #include 9 | #include 10 | #include 11 | 12 | TEV_NAMESPACE_BEGIN 13 | 14 | namespace colormap { 15 | const std::vector& turbo(); 16 | const std::vector& viridis(); 17 | } 18 | 19 | TEV_NAMESPACE_END 20 | -------------------------------------------------------------------------------- /scripts/create-exe-ninja.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | set cwd=%cd% 4 | cd /D %~dp0 5 | 6 | set DevCmd="C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\VC\Auxiliary\Build\vcvars64.bat" 7 | set BuildDir64="build-exe-64" 8 | 9 | call %DevCmd% 10 | 11 | echo Building tev with ninja... 12 | mkdir %BuildDir64% 13 | cd %BuildDir64% 14 | cmake -DTEV_DEPLOY=1 -DCMAKE_BUILD_TYPE=Release -GNinja ..\.. 15 | ninja 16 | move "tev.exe" "..\..\tev.exe" 17 | cd .. 18 | 19 | echo Returning to original directory. 20 | cd /D %cwd% 21 | pause 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Build artifacts 2 | /build 3 | /x64 4 | 5 | # Automatically generated files by .vscode 6 | /.vscode 7 | 8 | # Python cache 9 | __pycache__ 10 | 11 | # macOS 12 | .DS_Store 13 | 14 | # Development project files 15 | *.sublime-project 16 | 17 | # Compiled Object files 18 | *.slo 19 | *.lo 20 | *.o 21 | *.obj 22 | 23 | # Precompiled Headers 24 | *.gch 25 | *.pch 26 | 27 | # Compiled Dynamic libraries 28 | *.so 29 | *.dylib 30 | *.dll 31 | 32 | # Fortran module files 33 | *.mod 34 | 35 | # Compiled Static libraries 36 | *.lai 37 | *.la 38 | *.a 39 | *.lib 40 | 41 | # Executables 42 | *.exe 43 | *.out 44 | *.app 45 | -------------------------------------------------------------------------------- /scripts/create-exe.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | set cwd=%cd% 4 | cd /D %~dp0 5 | 6 | set DevCmd="C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\VC\Auxiliary\Build\vcvars64.bat" 7 | set MSBuildOptions=/v:m /p:Configuration=Release 8 | set BuildDir64="build-exe-64" 9 | 10 | call %DevCmd% 11 | 12 | echo Building 64-bit tev... 13 | mkdir %BuildDir64% 14 | cd %BuildDir64% 15 | cmake -DTEV_DEPLOY=1 -G "Visual Studio 16 2019" ..\.. 16 | msbuild %MSBuildOptions% tev.sln 17 | move "Release\tev.exe" "..\..\tev.exe" 18 | cd .. 19 | rmdir /S /Q %BuildDir64% 20 | 21 | echo Returning to original directory. 22 | cd /D %cwd% 23 | pause 24 | -------------------------------------------------------------------------------- /scripts/create-dmg/builder/create-dmg.builder: -------------------------------------------------------------------------------- 1 | SET app_name create-dmg 2 | 3 | VERSION create-dmg.cur create-dmg heads/master 4 | 5 | NEWDIR build.dir temp %-build - 6 | 7 | NEWFILE create-dmg.zip featured %.zip % 8 | 9 | 10 | COPYTO [build.dir] 11 | INTO create-dmg [create-dmg.cur]/create-dmg 12 | INTO sample [create-dmg.cur]/sample 13 | INTO support [create-dmg.cur]/support 14 | 15 | SUBSTVARS [build.dir]/create-dmg [[]] 16 | 17 | 18 | ZIP [create-dmg.zip] 19 | INTO [build-files-prefix] [build.dir] 20 | 21 | 22 | PUT megabox-builds create-dmg.zip 23 | PUT megabox-builds build.log 24 | 25 | PUT s3-builds create-dmg.zip 26 | PUT s3-builds build.log 27 | -------------------------------------------------------------------------------- /scripts/create-dmg.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | cd "$(dirname ${BASH_SOURCE[0]})" 4 | 5 | echo "Building tev..." 6 | 7 | BUILD_DIR="build-dmg" 8 | 9 | mkdir $BUILD_DIR 10 | cd $BUILD_DIR 11 | MACOSX_DEPLOYMENT_TARGET=10.15 12 | cmake \ 13 | -DCMAKE_OSX_DEPLOYMENT_TARGET=10.15 \ 14 | -DTEV_DEPLOY=1 \ 15 | -DCMAKE_OSX_ARCHITECTURES="arm64;x86_64" \ 16 | ../.. 17 | make -j 18 | cd .. 19 | 20 | echo "Creating dmg..." 21 | RESULT="../tev.dmg" 22 | test -f $RESULT 23 | rm $RESULT 24 | ./create-dmg/create-dmg --window-size 500 300 --icon-size 96 --volname "tev Installer" --app-drop-link 360 105 --icon tev.app 130 105 $RESULT $BUILD_DIR/tev.app 25 | 26 | echo "Removing temporary build dir..." 27 | rm -rf $BUILD_DIR 28 | -------------------------------------------------------------------------------- /include/tev/imageio/DdsImageLoader.h: -------------------------------------------------------------------------------- 1 | // This file was developed by Thomas Müller . 2 | // It is published under the BSD 3-Clause License within the LICENSE file. 3 | 4 | #pragma once 5 | 6 | #include 7 | #include 8 | 9 | #include 10 | 11 | TEV_NAMESPACE_BEGIN 12 | 13 | class DdsImageLoader : public ImageLoader { 14 | public: 15 | bool canLoadFile(std::istream& iStream) const override; 16 | Task> load(std::istream& iStream, const fs::path& path, const std::string& channelSelector, int priority) const override; 17 | 18 | std::string name() const override { 19 | return "DDS"; 20 | } 21 | }; 22 | 23 | TEV_NAMESPACE_END 24 | -------------------------------------------------------------------------------- /include/tev/imageio/PfmImageLoader.h: -------------------------------------------------------------------------------- 1 | // This file was developed by Thomas Müller . 2 | // It is published under the BSD 3-Clause License within the LICENSE file. 3 | 4 | #pragma once 5 | 6 | #include 7 | #include 8 | 9 | #include 10 | 11 | TEV_NAMESPACE_BEGIN 12 | 13 | class PfmImageLoader : public ImageLoader { 14 | public: 15 | bool canLoadFile(std::istream& iStream) const override; 16 | Task> load(std::istream& iStream, const fs::path& path, const std::string& channelSelector, int priority) const override; 17 | 18 | std::string name() const override { 19 | return "PFM"; 20 | } 21 | }; 22 | 23 | TEV_NAMESPACE_END 24 | -------------------------------------------------------------------------------- /include/tev/imageio/StbiImageLoader.h: -------------------------------------------------------------------------------- 1 | // This file was developed by Thomas Müller . 2 | // It is published under the BSD 3-Clause License within the LICENSE file. 3 | 4 | #pragma once 5 | 6 | #include 7 | #include 8 | 9 | #include 10 | 11 | TEV_NAMESPACE_BEGIN 12 | 13 | class StbiImageLoader : public ImageLoader { 14 | public: 15 | bool canLoadFile(std::istream& iStream) const override; 16 | Task> load(std::istream& iStream, const fs::path& path, const std::string& channelSelector, int priority) const override; 17 | 18 | std::string name() const override { 19 | return "STBI"; 20 | } 21 | }; 22 | 23 | TEV_NAMESPACE_END 24 | -------------------------------------------------------------------------------- /include/tev/imageio/EmptyImageLoader.h: -------------------------------------------------------------------------------- 1 | // This file was developed by Thomas Müller . 2 | // It is published under the BSD 3-Clause License within the LICENSE file. 3 | 4 | #pragma once 5 | 6 | #include 7 | #include 8 | 9 | #include 10 | 11 | TEV_NAMESPACE_BEGIN 12 | 13 | class EmptyImageLoader : public ImageLoader { 14 | public: 15 | bool canLoadFile(std::istream& iStream) const override; 16 | Task> load(std::istream& iStream, const fs::path& path, const std::string& channelSelector, int priority) const override; 17 | 18 | std::string name() const override { 19 | return "IPC"; 20 | } 21 | }; 22 | 23 | TEV_NAMESPACE_END 24 | -------------------------------------------------------------------------------- /include/tev/imageio/ExrImageLoader.h: -------------------------------------------------------------------------------- 1 | // This file was developed by Thomas Müller . 2 | // It is published under the BSD 3-Clause License within the LICENSE file. 3 | 4 | #pragma once 5 | 6 | #include 7 | #include 8 | 9 | #include 10 | 11 | TEV_NAMESPACE_BEGIN 12 | 13 | class ExrImageLoader : public ImageLoader { 14 | public: 15 | bool canLoadFile(std::istream& iStream) const override; 16 | Task> load(std::istream& iStream, const fs::path& path, const std::string& channelSelector, int priority) const override; 17 | 18 | std::string name() const override { 19 | return "OpenEXR"; 20 | } 21 | }; 22 | 23 | TEV_NAMESPACE_END 24 | -------------------------------------------------------------------------------- /include/tev/HelpWindow.h: -------------------------------------------------------------------------------- 1 | // This file was developed by Thomas Müller . 2 | // It is published under the BSD 3-Clause License within the LICENSE file. 3 | 4 | #pragma once 5 | 6 | #include 7 | 8 | #include 9 | 10 | #include 11 | 12 | TEV_NAMESPACE_BEGIN 13 | 14 | class HelpWindow : public nanogui::Window { 15 | public: 16 | HelpWindow(nanogui::Widget* parent, bool supportsHdr, std::function closeCallback); 17 | 18 | bool keyboard_event(int key, int scancode, int action, int modifiers) override; 19 | 20 | static std::string COMMAND; 21 | static std::string ALT; 22 | 23 | private: 24 | std::function mCloseCallback; 25 | }; 26 | 27 | TEV_NAMESPACE_END 28 | -------------------------------------------------------------------------------- /include/tev/imageio/ClipboardImageLoader.h: -------------------------------------------------------------------------------- 1 | // This file was developed by Thomas Müller . 2 | // It is published under the BSD 3-Clause License within the LICENSE file. 3 | 4 | #pragma once 5 | 6 | #include 7 | #include 8 | 9 | #include 10 | 11 | TEV_NAMESPACE_BEGIN 12 | 13 | class ClipboardImageLoader : public ImageLoader { 14 | public: 15 | bool canLoadFile(std::istream& iStream) const override; 16 | Task> load(std::istream& iStream, const fs::path& path, const std::string& channelSelector, int priority) const override; 17 | 18 | std::string name() const override { 19 | return "clipboard"; 20 | } 21 | }; 22 | 23 | TEV_NAMESPACE_END 24 | -------------------------------------------------------------------------------- /include/tev/imageio/QoiImageLoader.h: -------------------------------------------------------------------------------- 1 | // This file was developed by Tiago Chaves & Thomas Müller . 2 | // It is published under the BSD 3-Clause License within the LICENSE file. 3 | 4 | #pragma once 5 | 6 | #include 7 | #include 8 | 9 | #include 10 | 11 | TEV_NAMESPACE_BEGIN 12 | 13 | class QoiImageLoader : public ImageLoader { 14 | public: 15 | bool canLoadFile(std::istream& iStream) const override; 16 | Task> load(std::istream& iStream, const fs::path& path, const std::string& channelSelector, int priority) const override; 17 | 18 | std::string name() const override { 19 | return "QOI"; 20 | } 21 | }; 22 | 23 | TEV_NAMESPACE_END 24 | -------------------------------------------------------------------------------- /scripts/create-dmg-backwards.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | cd "$(dirname ${BASH_SOURCE[0]})" 4 | 5 | echo "Building backwards-compatible tev..." 6 | 7 | BUILD_DIR="build-dmg" 8 | 9 | mkdir $BUILD_DIR 10 | cd $BUILD_DIR 11 | MACOSX_DEPLOYMENT_TARGET=10.14 12 | cmake \ 13 | -DCMAKE_OSX_SYSROOT=/Users/tom94/Projects/MacOSX-SDKs/MacOSX10.14.sdk/ \ 14 | -DCMAKE_OSX_DEPLOYMENT_TARGET=10.14 \ 15 | -DTEV_DEPLOY=1 \ 16 | ../.. 17 | make -j 18 | cd .. 19 | 20 | echo "Creating dmg..." 21 | RESULT="../tev-pre-catalina.dmg" 22 | test -f $RESULT 23 | rm $RESULT 24 | ./create-dmg/create-dmg --window-size 500 300 --icon-size 96 --volname "tev Installer" --app-drop-link 360 105 --icon tev.app 130 105 $RESULT $BUILD_DIR/tev.app 25 | 26 | echo "Removing temporary build dir..." 27 | rm -rf $BUILD_DIR 28 | -------------------------------------------------------------------------------- /include/tev/imageio/ExrImageSaver.h: -------------------------------------------------------------------------------- 1 | // This file was developed by Thomas Müller . 2 | // It is published under the BSD 3-Clause License within the LICENSE file. 3 | 4 | #pragma once 5 | 6 | #include 7 | 8 | #include 9 | 10 | TEV_NAMESPACE_BEGIN 11 | 12 | class ExrImageSaver : public TypedImageSaver { 13 | public: 14 | void save(std::ostream& oStream, const fs::path& path, const std::vector& data, const nanogui::Vector2i& imageSize, int nChannels) const override; 15 | 16 | bool hasPremultipliedAlpha() const override { 17 | return true; 18 | } 19 | 20 | virtual bool canSaveFile(const std::string& extension) const override { 21 | return toLower(extension) == ".exr"; 22 | } 23 | }; 24 | 25 | TEV_NAMESPACE_END 26 | -------------------------------------------------------------------------------- /include/tev/imageio/StbiHdrImageSaver.h: -------------------------------------------------------------------------------- 1 | // This file was developed by Thomas Müller . 2 | // It is published under the BSD 3-Clause License within the LICENSE file. 3 | 4 | #pragma once 5 | 6 | #include 7 | 8 | #include 9 | 10 | TEV_NAMESPACE_BEGIN 11 | 12 | class StbiHdrImageSaver : public TypedImageSaver { 13 | public: 14 | void save(std::ostream& oStream, const fs::path& path, const std::vector& data, const nanogui::Vector2i& imageSize, int nChannels) const override; 15 | 16 | bool hasPremultipliedAlpha() const override { 17 | return false; 18 | } 19 | 20 | virtual bool canSaveFile(const std::string& extension) const override { 21 | return toLower(extension) == ".hdr"; 22 | } 23 | }; 24 | 25 | TEV_NAMESPACE_END 26 | -------------------------------------------------------------------------------- /include/tev/imageio/QoiImageSaver.h: -------------------------------------------------------------------------------- 1 | // This file was developed by Tiago Chaves & Thomas Müller . 2 | // It is published under the BSD 3-Clause License within the LICENSE file. 3 | 4 | #pragma once 5 | 6 | #include 7 | 8 | #include 9 | 10 | TEV_NAMESPACE_BEGIN 11 | 12 | class QoiImageSaver : public TypedImageSaver { 13 | public: 14 | void save(std::ostream& oStream, const fs::path& path, const std::vector& data, const nanogui::Vector2i& imageSize, int nChannels) const override; 15 | 16 | bool hasPremultipliedAlpha() const override { 17 | return false; 18 | } 19 | 20 | virtual bool canSaveFile(const std::string& extension) const override { 21 | return toLower(extension) == ".qoi"; 22 | } 23 | }; 24 | 25 | TEV_NAMESPACE_END 26 | -------------------------------------------------------------------------------- /src/imageio/StbiHdrImageSaver.cpp: -------------------------------------------------------------------------------- 1 | // This file was developed by Thomas Müller . 2 | // It is published under the BSD 3-Clause License within the LICENSE file. 3 | 4 | #include 5 | 6 | #include 7 | 8 | #include 9 | #include 10 | 11 | using namespace nanogui; 12 | using namespace std; 13 | 14 | TEV_NAMESPACE_BEGIN 15 | 16 | void StbiHdrImageSaver::save(ostream& oStream, const fs::path&, const vector& data, const Vector2i& imageSize, int nChannels) const { 17 | static const auto stbiOStreamWrite = [](void* context, void* data, int size) { 18 | reinterpret_cast(context)->write(reinterpret_cast(data), size); 19 | }; 20 | 21 | stbi_write_hdr_to_func(stbiOStreamWrite, &oStream, imageSize.x(), imageSize.y(), nChannels, data.data()); 22 | } 23 | 24 | TEV_NAMESPACE_END 25 | -------------------------------------------------------------------------------- /src/python/ipc-example.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # This file was developed by Tomáš Iser & Thomas Müller . 4 | # It is published under the BSD 3-Clause License within the LICENSE file. 5 | 6 | """ 7 | Example usage of tev's Python IPC implementation. 8 | """ 9 | 10 | from ipc import TevIpc 11 | import numpy as np 12 | 13 | if __name__ == "__main__": 14 | with TevIpc() as tev: 15 | image_data = np.full((300,200,3), 1.0) 16 | image_data[40:61,:,0] = 0.0 17 | image_data[:,40:61,1] = 0.0 18 | image_data[50:71,50:71,2] = 0.0 19 | 20 | bonus_data = image_data[:,:,0] + image_data[:,:,1] + image_data[:,:,2] 21 | 22 | tev.create_image("Test Image", width=200, height=300, channel_names=["R","G","B","Bonus"]) 23 | tev.update_image("Test Image", image_data, ["R", "G", "B"]) 24 | tev.update_image("Test Image", bonus_data, ["Bonus"]) 25 | -------------------------------------------------------------------------------- /src/imageio/ImageSaver.cpp: -------------------------------------------------------------------------------- 1 | // This file was developed by Thomas Müller . 2 | // It is published under the BSD 3-Clause License within the LICENSE file. 3 | 4 | #include 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | #include 12 | 13 | using namespace std; 14 | 15 | TEV_NAMESPACE_BEGIN 16 | 17 | const vector>& ImageSaver::getSavers() { 18 | auto makeSavers = [] { 19 | vector> imageSavers; 20 | imageSavers.emplace_back(new ExrImageSaver()); 21 | imageSavers.emplace_back(new QoiImageSaver()); 22 | imageSavers.emplace_back(new StbiHdrImageSaver()); 23 | imageSavers.emplace_back(new StbiLdrImageSaver()); 24 | return imageSavers; 25 | }; 26 | 27 | static const vector imageSavers = makeSavers(); 28 | return imageSavers; 29 | } 30 | 31 | TEV_NAMESPACE_END 32 | -------------------------------------------------------------------------------- /include/tev/imageio/StbiLdrImageSaver.h: -------------------------------------------------------------------------------- 1 | // This file was developed by Thomas Müller . 2 | // It is published under the BSD 3-Clause License within the LICENSE file. 3 | 4 | #pragma once 5 | 6 | #include 7 | 8 | #include 9 | 10 | TEV_NAMESPACE_BEGIN 11 | 12 | class StbiLdrImageSaver : public TypedImageSaver { 13 | public: 14 | void save(std::ostream& oStream, const fs::path& path, const std::vector& data, const nanogui::Vector2i& imageSize, int nChannels) const override; 15 | 16 | bool hasPremultipliedAlpha() const override { 17 | return false; 18 | } 19 | 20 | virtual bool canSaveFile(const std::string& extension) const override { 21 | std::string lowerExtension = toLower(extension); 22 | return lowerExtension == ".jpg" 23 | || lowerExtension == ".jpeg" 24 | || lowerExtension == ".png" 25 | || lowerExtension == ".bmp" 26 | || lowerExtension == ".tga" 27 | ; 28 | } 29 | }; 30 | 31 | TEV_NAMESPACE_END 32 | -------------------------------------------------------------------------------- /include/tev/imageio/ImageLoader.h: -------------------------------------------------------------------------------- 1 | // This file was developed by Thomas Müller . 2 | // It is published under the BSD 3-Clause License within the LICENSE file. 3 | 4 | #pragma once 5 | 6 | #include 7 | #include 8 | #include 9 | 10 | #include 11 | 12 | #include 13 | #include 14 | #include 15 | 16 | TEV_NAMESPACE_BEGIN 17 | 18 | class ImageLoader { 19 | public: 20 | virtual ~ImageLoader() {} 21 | 22 | virtual bool canLoadFile(std::istream& iStream) const = 0; 23 | 24 | // Return loaded image data as well as whether that data has the alpha channel pre-multiplied or not. 25 | virtual Task> load(std::istream& iStream, const fs::path& path, const std::string& channelSelector, int priority) const = 0; 26 | 27 | virtual std::string name() const = 0; 28 | 29 | static const std::vector>& getLoaders(); 30 | 31 | protected: 32 | static std::vector makeNChannels(int numChannels, const nanogui::Vector2i& size); 33 | }; 34 | 35 | TEV_NAMESPACE_END 36 | -------------------------------------------------------------------------------- /include/tev/imageio/ImageSaver.h: -------------------------------------------------------------------------------- 1 | // This file was developed by Thomas Müller . 2 | // It is published under the BSD 3-Clause License within the LICENSE file. 3 | 4 | #pragma once 5 | 6 | #include 7 | 8 | #include 9 | 10 | #include 11 | #include 12 | #include 13 | 14 | TEV_NAMESPACE_BEGIN 15 | 16 | template 17 | class TypedImageSaver; 18 | 19 | class ImageSaver { 20 | public: 21 | virtual ~ImageSaver() {} 22 | 23 | virtual bool hasPremultipliedAlpha() const = 0; 24 | 25 | virtual bool canSaveFile(const std::string& extension) const = 0; 26 | bool canSaveFile(const fs::path& path) const { 27 | return canSaveFile(toLower(toString(path.extension()))); 28 | } 29 | 30 | static const std::vector>& getSavers(); 31 | }; 32 | 33 | template 34 | class TypedImageSaver : public ImageSaver { 35 | public: 36 | virtual void save(std::ostream& oStream, const fs::path& path, const std::vector& data, const nanogui::Vector2i& imageSize, int nChannels) const = 0; 37 | }; 38 | 39 | TEV_NAMESPACE_END 40 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "dependencies/args"] 2 | path = dependencies/args 3 | url = https://github.com/Taywee/args 4 | [submodule "dependencies/openexr"] 5 | path = dependencies/openexr 6 | url = https://github.com/mitsuba-renderer/openexr 7 | [submodule "dependencies/zlib"] 8 | path = dependencies/zlib 9 | url = https://github.com/Tom94/zlib 10 | [submodule "dependencies/tinyformat"] 11 | path = dependencies/tinyformat 12 | url = https://github.com/c42f/tinyformat 13 | [submodule "dependencies/utfcpp"] 14 | path = dependencies/utfcpp 15 | url = https://github.com/Tom94/utfcpp 16 | [submodule "dependencies/tinylogger"] 17 | path = dependencies/tinylogger 18 | url = https://github.com/Tom94/tinylogger 19 | [submodule "dependencies/clip"] 20 | path = dependencies/clip 21 | url = https://github.com/Tom94/clip 22 | [submodule "dependencies/DirectXTex"] 23 | path = dependencies/DirectXTex 24 | url = https://github.com/microsoft/DirectXTex.git 25 | [submodule "dependencies/nanogui"] 26 | path = dependencies/nanogui 27 | url = https://github.com/Tom94/nanogui-1 28 | [submodule "dependencies/qoi"] 29 | path = dependencies/qoi 30 | url = https://github.com/phoboslab/qoi.git 31 | -------------------------------------------------------------------------------- /scripts/create-dmg/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2008-2014 Andrey Tarantsov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/imageio/QoiImageSaver.cpp: -------------------------------------------------------------------------------- 1 | // This file was developed by Tiago Chaves & Thomas Müller . 2 | // It is published under the BSD 3-Clause License within the LICENSE file. 3 | 4 | #include 5 | 6 | #define QOI_NO_STDIO 7 | #include 8 | 9 | #include 10 | #include 11 | 12 | using namespace nanogui; 13 | using namespace std; 14 | 15 | TEV_NAMESPACE_BEGIN 16 | 17 | void QoiImageSaver::save(ostream& oStream, const fs::path&, const vector& data, const Vector2i& imageSize, int nChannels) const { 18 | // The QOI image format expects nChannels to be either 3 for RGB data or 4 for RGBA. 19 | if (nChannels != 4 && nChannels != 3) { 20 | throw invalid_argument{tfm::format("Invalid number of channels %d.", nChannels)}; 21 | } 22 | 23 | const qoi_desc desc{ 24 | .width = static_cast(imageSize.x()), 25 | .height = static_cast(imageSize.y()), 26 | .channels = static_cast(nChannels), 27 | .colorspace = QOI_SRGB, 28 | }; 29 | int sizeInBytes = 0; 30 | void *encodedData = qoi_encode(data.data(), &desc, &sizeInBytes); 31 | 32 | ScopeGuard encodedDataGuard{[encodedData] { free(encodedData); }}; 33 | 34 | if (!encodedData) { 35 | throw invalid_argument{"Failed to encode data into the QOI format."}; 36 | } 37 | 38 | oStream.write(reinterpret_cast(encodedData), sizeInBytes); 39 | } 40 | 41 | TEV_NAMESPACE_END 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2017-2020, Thomas Müller 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | * Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /include/tev/SharedQueue.h: -------------------------------------------------------------------------------- 1 | // This file was developed by Thomas Müller . 2 | // It is published under the BSD 3-Clause License within the LICENSE file. 3 | 4 | #pragma once 5 | 6 | #include 7 | 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | TEV_NAMESPACE_BEGIN 14 | 15 | template 16 | class SharedQueue { 17 | public: 18 | bool empty() const { 19 | std::lock_guard lock{mMutex}; 20 | return mRawQueue.empty(); 21 | } 22 | 23 | size_t size() const { 24 | std::lock_guard lock{mMutex}; 25 | return mRawQueue.size(); 26 | } 27 | 28 | void push(T newElem) { 29 | std::lock_guard lock{mMutex}; 30 | mRawQueue.push_back(newElem); 31 | mDataCondition.notify_one(); 32 | } 33 | 34 | T waitAndPop() { 35 | std::unique_lock lock{mMutex}; 36 | 37 | while (mRawQueue.empty()) { 38 | mDataCondition.wait(lock); 39 | } 40 | 41 | T result = std::move(mRawQueue.front()); 42 | mRawQueue.pop_front(); 43 | 44 | return result; 45 | } 46 | 47 | std::optional tryPop() { 48 | std::unique_lock lock{mMutex}; 49 | 50 | if (mRawQueue.empty()) { 51 | return {}; 52 | } 53 | 54 | T result = std::move(mRawQueue.front()); 55 | mRawQueue.pop_front(); 56 | 57 | return result; 58 | } 59 | 60 | private: 61 | std::deque mRawQueue; 62 | mutable std::mutex mMutex; 63 | std::condition_variable mDataCondition; 64 | }; 65 | 66 | TEV_NAMESPACE_END 67 | -------------------------------------------------------------------------------- /src/imageio/StbiLdrImageSaver.cpp: -------------------------------------------------------------------------------- 1 | // This file was developed by Thomas Müller . 2 | // It is published under the BSD 3-Clause License within the LICENSE file. 3 | 4 | #include 5 | 6 | #define STB_IMAGE_WRITE_IMPLEMENTATION 7 | #include 8 | 9 | #include 10 | #include 11 | 12 | using namespace nanogui; 13 | using namespace std; 14 | 15 | TEV_NAMESPACE_BEGIN 16 | 17 | void StbiLdrImageSaver::save(ostream& oStream, const fs::path& path, const vector& data, const Vector2i& imageSize, int nChannels) const { 18 | static const auto stbiOStreamWrite = [](void* context, void* data, int size) { 19 | reinterpret_cast(context)->write(reinterpret_cast(data), size); 20 | }; 21 | 22 | auto extension = toLower(toString(path.extension())); 23 | 24 | if (extension == ".jpg" || extension == ".jpeg") { 25 | stbi_write_jpg_to_func(stbiOStreamWrite, &oStream, imageSize.x(), imageSize.y(), nChannels, data.data(), 100); 26 | } else if (extension == ".png") { 27 | stbi_write_png_to_func(stbiOStreamWrite, &oStream, imageSize.x(), imageSize.y(), nChannels, data.data(), 0); 28 | } else if (extension == ".bmp") { 29 | stbi_write_bmp_to_func(stbiOStreamWrite, &oStream, imageSize.x(), imageSize.y(), nChannels, data.data()); 30 | } else if (extension == ".tga") { 31 | stbi_write_tga_to_func(stbiOStreamWrite, &oStream, imageSize.x(), imageSize.y(), nChannels, data.data()); 32 | } else { 33 | throw invalid_argument{tfm::format("Image '%s' has unknown format.", path)}; 34 | } 35 | } 36 | 37 | TEV_NAMESPACE_END 38 | -------------------------------------------------------------------------------- /include/tev/Box.h: -------------------------------------------------------------------------------- 1 | // This file was developed by Thomas Müller . 2 | // It is published under the BSD 3-Clause License within the LICENSE file. 3 | 4 | #pragma once 5 | 6 | #include 7 | 8 | 9 | TEV_NAMESPACE_BEGIN 10 | 11 | template 12 | struct Box { 13 | using Vector = nanogui::Array; 14 | 15 | Box(const Vector& min, const Vector& max) : min{min}, max{max} {} 16 | Box(const Vector& max) : Box{Vector{(T)0}, max} {} 17 | Box() : Box{Vector{std::numeric_limits::max()}, Vector{std::numeric_limits::min()}} {} 18 | 19 | // Casting boxes of other types to this one 20 | template 21 | Box(const Box& other) : min{other.min}, max{other.max} {} 22 | 23 | Vector size() const { 24 | return max - min; 25 | } 26 | 27 | Vector middle() const { 28 | return (min + max) / (T)2; 29 | } 30 | 31 | bool isValid() const { 32 | bool result = true; 33 | for (uint32_t i = 0; i < N_DIMS; ++i) { 34 | result &= max[i] >= min[i]; 35 | } 36 | return result; 37 | } 38 | 39 | bool operator==(const Box& other) const { 40 | return min == other.min && max == other.max; 41 | } 42 | 43 | Box inflate(T amount) const { 44 | return {min - Vector{amount}, max + Vector{amount}}; 45 | } 46 | 47 | Vector min, max; 48 | }; 49 | 50 | using Box2f = Box; 51 | using Box3f = Box; 52 | using Box4f = Box; 53 | using Box2i = Box; 54 | using Box3i = Box; 55 | using Box4i = Box; 56 | 57 | TEV_NAMESPACE_END 58 | -------------------------------------------------------------------------------- /scripts/parse-include-paths.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | """ 4 | Parses .vscode/.cmaketools.json to obtain a list of include paths. 5 | These can then be subsequently pasted into .vscode/c_cpp_properties.json 6 | to make intellisense work. This is script exists purely for convenience 7 | and only needs to be used when the include paths change (e.g. when a new 8 | dependency is added). 9 | """ 10 | 11 | import json 12 | import os 13 | import sys 14 | 15 | def iterate_over(dict_or_list, result): 16 | """ 17 | Iterates recursively over nested lists and dictionaries 18 | keeping track of all "path" values with the key "includePath" 19 | within nested dictionaries. 20 | """ 21 | if isinstance(dict_or_list, list): 22 | for child in dict_or_list: 23 | iterate_over(child, result) 24 | elif isinstance(dict_or_list, dict): 25 | for key, value in dict_or_list.items(): 26 | if key == "includePath": 27 | for child in value: 28 | result.add(child["path"]) 29 | else: 30 | iterate_over(value, result) 31 | 32 | def main(arguments): 33 | """Main function of this program.""" 34 | 35 | workspace = os.path.realpath(os.path.join(__file__, os.pardir, os.pardir)) 36 | print("Workspace root: '{}'".format(workspace)) 37 | 38 | with open(os.path.join(workspace, ".vscode", ".cmaketools.json")) as f: 39 | data = json.loads(f.read()) 40 | 41 | result = set() 42 | 43 | iterate_over(data, result) 44 | 45 | result = [x.replace(workspace, "${workspaceRoot}") for x in result] 46 | 47 | print(json.dumps(result, indent=0)) 48 | 49 | 50 | if __name__ == "__main__": 51 | sys.exit(main(sys.argv[1:])) 52 | -------------------------------------------------------------------------------- /.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 | jobs: 12 | build_linux: 13 | name: Build on linux systems 14 | runs-on: ${{ matrix.os }} 15 | strategy: 16 | matrix: 17 | os: [ubuntu-20.04] 18 | env: 19 | build_dir: "build" 20 | config: "Release" 21 | CC: gcc-10 22 | CXX: g++-10 23 | steps: 24 | - name: Install dependencies 25 | run: sudo apt-get update && sudo apt-get install cmake gcc-10 g++-10 libglu1-mesa-dev xorg-dev zenity zlib1g-dev 26 | - uses: actions/checkout@v1 27 | with: 28 | submodules: recursive 29 | - name: CMake 30 | run: cmake . -B ${{ env.build_dir }} -DCMAKE_BUILD_TYPE=${{ env.config }} 31 | - name: Build 32 | working-directory: ${{ env.build_dir }} 33 | run: cmake --build . --target all --verbose -j 34 | 35 | build_macos: 36 | name: Build on macOS 37 | runs-on: macos-11 38 | env: 39 | build_dir: "build" 40 | config: "Release" 41 | steps: 42 | - uses: actions/checkout@v1 43 | with: 44 | submodules: recursive 45 | - name: CMake 46 | run: cmake . -B ${{ env.build_dir }} -DCMAKE_BUILD_TYPE=${{ env.config }} 47 | - name: Build 48 | working-directory: ${{ env.build_dir }} 49 | run: cmake --build . --target all --verbose -j 50 | 51 | build_windows: 52 | name: Build on Windows 53 | runs-on: windows-2022 54 | env: 55 | build_dir: "build" 56 | config: "Release" 57 | steps: 58 | - uses: actions/checkout@v1 59 | with: 60 | submodules: recursive 61 | - name: CMake 62 | run: cmake . -B ${{ env.build_dir }} 63 | - name: Build 64 | working-directory: ${{ env.build_dir }} 65 | run: cmake --build . --config ${{ env.config }} --target ALL_BUILD --verbose 66 | -------------------------------------------------------------------------------- /src/imageio/ImageLoader.cpp: -------------------------------------------------------------------------------- 1 | // This file was developed by Thomas Müller . 2 | // It is published under the BSD 3-Clause License within the LICENSE file. 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #ifdef _WIN32 12 | # include 13 | #endif 14 | 15 | using namespace nanogui; 16 | using namespace std; 17 | 18 | TEV_NAMESPACE_BEGIN 19 | 20 | const vector>& ImageLoader::getLoaders() { 21 | auto makeLoaders = [] { 22 | vector> imageLoaders; 23 | imageLoaders.emplace_back(new ExrImageLoader()); 24 | imageLoaders.emplace_back(new PfmImageLoader()); 25 | imageLoaders.emplace_back(new ClipboardImageLoader()); 26 | imageLoaders.emplace_back(new EmptyImageLoader()); 27 | #ifdef _WIN32 28 | imageLoaders.emplace_back(new DdsImageLoader()); 29 | #endif 30 | imageLoaders.emplace_back(new QoiImageLoader()); 31 | imageLoaders.emplace_back(new StbiImageLoader()); 32 | return imageLoaders; 33 | }; 34 | 35 | static const vector imageLoaders = makeLoaders(); 36 | return imageLoaders; 37 | } 38 | 39 | vector ImageLoader::makeNChannels(int numChannels, const Vector2i& size) { 40 | vector channels; 41 | if (numChannels > 1) { 42 | const vector channelNames = {"R", "G", "B", "A"}; 43 | for (int c = 0; c < numChannels; ++c) { 44 | string name = c < (int)channelNames.size() ? channelNames[c] : to_string(c); 45 | channels.emplace_back(name, size); 46 | } 47 | } else { 48 | channels.emplace_back("L", size); 49 | } 50 | 51 | return channels; 52 | } 53 | 54 | TEV_NAMESPACE_END 55 | -------------------------------------------------------------------------------- /resources/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | English 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 | CFBundleGetInfoString 25 | High dynamic range (HDR) image comparison tool. 26 | CFBundleIconFile 27 | icon.icns 28 | CFBundleIdentifier 29 | org.tom94.tev 30 | CFBundleInfoDictionaryVersion 31 | 6.0 32 | CFBundleName 33 | tev 34 | CFBundlePackageType 35 | APPL 36 | CFBundleSignature 37 | ???? 38 | CFBundleVersion 39 | @MACOSX_BUNDLE_BUNDLE_VERSION@ 40 | CFBundleLongVersionString 41 | @MACOSX_BUNDLE_LONG_VERSION_STRING@ 42 | CFBundleShortVersionString 43 | @MACOSX_BUNDLE_SHORT_VERSION_STRING@ 44 | CSResourcesFileMapped 45 | 46 | NSHumanReadableCopyright 47 | © 2017 Thomas Müller 48 | NSPrincipalClass 49 | NSApplication 50 | NSHighResolutionCapable 51 | True 52 | 53 | 54 | -------------------------------------------------------------------------------- /src/imageio/EmptyImageLoader.cpp: -------------------------------------------------------------------------------- 1 | // This file was developed by Thomas Müller . 2 | // It is published under the BSD 3-Clause License within the LICENSE file. 3 | 4 | #include 5 | 6 | #include 7 | 8 | using namespace nanogui; 9 | using namespace std; 10 | 11 | TEV_NAMESPACE_BEGIN 12 | 13 | bool EmptyImageLoader::canLoadFile(istream& iStream) const { 14 | char b[5]; 15 | iStream.read(b, sizeof(b)); 16 | 17 | bool result = !!iStream && iStream.gcount() == sizeof(b) && string(b, sizeof(b)) == "empty"; 18 | 19 | iStream.clear(); 20 | iStream.seekg(0); 21 | return result; 22 | } 23 | 24 | Task> EmptyImageLoader::load(istream& iStream, const fs::path&, const string&, int priority) const { 25 | vector result(1); 26 | ImageData& data = result.front(); 27 | 28 | string magic; 29 | Vector2i size; 30 | int nChannels; 31 | iStream >> magic >> size.x() >> size.y() >> nChannels; 32 | 33 | if (magic != "empty") { 34 | throw invalid_argument{tfm::format("Invalid magic empty string %s", magic)}; 35 | } 36 | 37 | auto numPixels = (size_t)size.x() * size.y(); 38 | if (numPixels == 0) { 39 | throw invalid_argument{"Image has zero pixels."}; 40 | } 41 | 42 | for (int i = 0; i < nChannels; ++i) { 43 | // The following lines decode strings by prefix length. 44 | // The reason for using sthis encoding is to allow arbitrary characters, 45 | // including whitespaces, in the channel names. 46 | std::vector channelNameData; 47 | int length; 48 | iStream >> length; 49 | channelNameData.resize(length+1, 0); 50 | iStream.read(channelNameData.data(), length); 51 | 52 | string channelName = channelNameData.data(); 53 | 54 | data.channels.emplace_back(Channel{channelName, size}); 55 | data.channels.back().setZero(); 56 | } 57 | 58 | data.hasPremultipliedAlpha = true; 59 | 60 | co_return result; 61 | } 62 | 63 | TEV_NAMESPACE_END 64 | -------------------------------------------------------------------------------- /include/tev/ImageButton.h: -------------------------------------------------------------------------------- 1 | // This file was developed by Thomas Müller . 2 | // It is published under the BSD 3-Clause License within the LICENSE file. 3 | 4 | #pragma once 5 | 6 | #include 7 | 8 | #include 9 | 10 | #include 11 | 12 | TEV_NAMESPACE_BEGIN 13 | 14 | class ImageButton : public nanogui::Widget { 15 | public: 16 | ImageButton(nanogui::Widget* parent, const std::string& caption, bool canBeReference); 17 | 18 | nanogui::Vector2i preferred_size(NVGcontext *ctx) const override; 19 | 20 | bool mouse_button_event(const nanogui::Vector2i &p, int button, bool down, int modifiers) override; 21 | 22 | void draw(NVGcontext *ctx) override; 23 | 24 | const std::string& caption() const { 25 | return mCaption; 26 | } 27 | 28 | void setReferenceCallback(const std::function &callback) { 29 | mReferenceCallback = callback; 30 | } 31 | 32 | void setIsReference(bool isReference) { 33 | mIsReference = isReference; 34 | } 35 | 36 | bool isReference() const { 37 | return mIsReference; 38 | } 39 | 40 | void setSelectedCallback(const std::function &callback) { 41 | mSelectedCallback = callback; 42 | } 43 | 44 | void setIsSelected(bool isSelected) { 45 | mIsSelected = isSelected; 46 | } 47 | 48 | bool isSelected() const { 49 | return mIsSelected; 50 | } 51 | 52 | void setId(size_t id) { 53 | mId = id; 54 | } 55 | 56 | void setHighlightRange(size_t begin, size_t end); 57 | 58 | private: 59 | std::string mCaption; 60 | bool mCanBeReference; 61 | 62 | bool mIsReference = false; 63 | std::function mReferenceCallback; 64 | 65 | bool mIsSelected = false; 66 | std::function mSelectedCallback; 67 | 68 | size_t mId = 0; 69 | size_t mCutoff = 0; 70 | nanogui::Vector2i mSizeForWhichCutoffWasComputed = {0}; 71 | 72 | size_t mHighlightBegin = 0; 73 | size_t mHighlightEnd = 0; 74 | }; 75 | 76 | TEV_NAMESPACE_END 77 | -------------------------------------------------------------------------------- /scripts/create-dmg/support/template.applescript: -------------------------------------------------------------------------------- 1 | on run (volumeName) 2 | tell application "Finder" 3 | tell disk (volumeName as string) 4 | open 5 | 6 | set theXOrigin to WINX 7 | set theYOrigin to WINY 8 | set theWidth to WINW 9 | set theHeight to WINH 10 | 11 | set theBottomRightX to (theXOrigin + theWidth) 12 | set theBottomRightY to (theYOrigin + theHeight) 13 | set dsStore to "\"" & "/Volumes/" & volumeName & "/" & ".DS_STORE\"" 14 | 15 | tell container window 16 | set current view to icon view 17 | set toolbar visible to false 18 | set statusbar visible to false 19 | set the bounds to {theXOrigin, theYOrigin, theBottomRightX, theBottomRightY} 20 | set statusbar visible to false 21 | REPOSITION_HIDDEN_FILES_CLAUSE 22 | end tell 23 | 24 | set opts to the icon view options of container window 25 | tell opts 26 | set icon size to ICON_SIZE 27 | set text size to TEXT_SIZE 28 | set arrangement to not arranged 29 | end tell 30 | BACKGROUND_CLAUSE 31 | 32 | -- Positioning 33 | POSITION_CLAUSE 34 | 35 | -- Hiding 36 | HIDING_CLAUSE 37 | 38 | -- Application Link Clause 39 | APPLICATION_CLAUSE 40 | close 41 | open 42 | 43 | update without registering applications 44 | -- Force saving of the size 45 | delay 1 46 | 47 | tell container window 48 | set statusbar visible to false 49 | set the bounds to {theXOrigin, theYOrigin, theBottomRightX - 10, theBottomRightY - 10} 50 | end tell 51 | 52 | update without registering applications 53 | end tell 54 | 55 | delay 1 56 | 57 | tell disk (volumeName as string) 58 | tell container window 59 | set statusbar visible to false 60 | set the bounds to {theXOrigin, theYOrigin, theBottomRightX, theBottomRightY} 61 | end tell 62 | 63 | update without registering applications 64 | end tell 65 | 66 | --give the finder some time to write the .DS_Store file 67 | delay 3 68 | 69 | set waitTime to 0 70 | set ejectMe to false 71 | repeat while ejectMe is false 72 | delay 1 73 | set waitTime to waitTime + 1 74 | 75 | if (do shell script "[ -f " & dsStore & " ]; echo $?") = "0" then set ejectMe to true 76 | end repeat 77 | log "waited " & waitTime & " seconds for .DS_STORE to be created." 78 | end tell 79 | end run 80 | -------------------------------------------------------------------------------- /include/tev/UberShader.h: -------------------------------------------------------------------------------- 1 | // This file was developed by Thomas Müller . 2 | // It is published under the BSD 3-Clause License within the LICENSE file. 3 | 4 | #pragma once 5 | 6 | #include 7 | #include 8 | #include 9 | 10 | TEV_NAMESPACE_BEGIN 11 | 12 | class UberShader { 13 | public: 14 | UberShader(nanogui::RenderPass* renderPass); 15 | virtual ~UberShader(); 16 | 17 | // Draws just a checkerboard. 18 | void draw(const nanogui::Vector2f& pixelSize, const nanogui::Vector2f& checkerSize); 19 | 20 | // Draws an image. 21 | void draw( 22 | const nanogui::Vector2f& pixelSize, 23 | const nanogui::Vector2f& checkerSize, 24 | nanogui::Texture* textureImage, 25 | const nanogui::Matrix3f& transformImage, 26 | float exposure, 27 | float offset, 28 | float gamma, 29 | bool clipToLdr, 30 | ETonemap tonemap 31 | ); 32 | 33 | // Draws a difference between a reference and an image. 34 | void draw( 35 | const nanogui::Vector2f& pixelSize, 36 | const nanogui::Vector2f& checkerSize, 37 | nanogui::Texture* textureImage, 38 | const nanogui::Matrix3f& transformImage, 39 | nanogui::Texture* textureReference, 40 | const nanogui::Matrix3f& transformReference, 41 | float exposure, 42 | float offset, 43 | float gamma, 44 | bool clipToLdr, 45 | ETonemap tonemap, 46 | EMetric metric 47 | ); 48 | 49 | const nanogui::Color& backgroundColor() { 50 | return mBackgroundColor; 51 | } 52 | 53 | void setBackgroundColor(const nanogui::Color& color) { 54 | mBackgroundColor = color; 55 | } 56 | 57 | private: 58 | void bindCheckerboardData(const nanogui::Vector2f& pixelSize, const nanogui::Vector2f& checkerSize); 59 | 60 | void bindImageData( 61 | nanogui::Texture* textureImage, 62 | const nanogui::Matrix3f& transformImage, 63 | float exposure, 64 | float offset, 65 | float gamma, 66 | ETonemap tonemap 67 | ); 68 | 69 | void bindReferenceData( 70 | nanogui::Texture* textureReference, 71 | const nanogui::Matrix3f& transformReference, 72 | EMetric metric 73 | ); 74 | 75 | nanogui::ref mShader; 76 | nanogui::ref mColorMap; 77 | 78 | nanogui::Color mBackgroundColor = nanogui::Color(0, 0, 0, 0); 79 | }; 80 | 81 | TEV_NAMESPACE_END 82 | -------------------------------------------------------------------------------- /include/tev/MultiGraph.h: -------------------------------------------------------------------------------- 1 | // This file was adapted from the nanogui::Graph class, which was developed 2 | // by Wenzel Jakob and based on the NanoVG demo application 3 | // by Mikko Mononen. Modifications were developed by Thomas Müller . 4 | // This file is published under the BSD 3-Clause License within the LICENSE file. 5 | 6 | #pragma once 7 | 8 | #include 9 | 10 | #include 11 | 12 | TEV_NAMESPACE_BEGIN 13 | 14 | class MultiGraph : public nanogui::Widget { 15 | public: 16 | MultiGraph(nanogui::Widget *parent, const std::string &caption = "Untitled"); 17 | 18 | const std::string &caption() const { return mCaption; } 19 | void setCaption(const std::string &caption) { mCaption = caption; } 20 | 21 | const std::string &header() const { return mHeader; } 22 | void setHeader(const std::string &header) { mHeader = header; } 23 | 24 | const std::string &footer() const { return mFooter; } 25 | void setFooter(const std::string &footer) { mFooter = footer; } 26 | 27 | const nanogui::Color &backgroundColor() const { return mBackgroundColor; } 28 | void setBackgroundColor(const nanogui::Color &backgroundColor) { mBackgroundColor = backgroundColor; } 29 | 30 | const nanogui::Color &foregroundColor() const { return mForegroundColor; } 31 | void setForegroundColor(const nanogui::Color &foregroundColor) { mForegroundColor = foregroundColor; } 32 | 33 | const nanogui::Color &textColor() const { return mTextColor; } 34 | void setTextColor(const nanogui::Color &textColor) { mTextColor = textColor; } 35 | 36 | const std::vector &values() const { return mValues; } 37 | std::vector &values() { return mValues; } 38 | void setValues(const std::vector &values) { mValues = values; } 39 | 40 | void setNChannels(int nChannels) { mNChannels = nChannels; } 41 | 42 | virtual nanogui::Vector2i preferred_size(NVGcontext *ctx) const override; 43 | virtual void draw(NVGcontext *ctx) override; 44 | 45 | void setMinimum(float minimum) { 46 | mMinimum = minimum; 47 | } 48 | 49 | void setMean(float mean) { 50 | mMean = mean; 51 | } 52 | 53 | void setMaximum(float maximum) { 54 | mMaximum = maximum; 55 | } 56 | 57 | void setZero(int zeroBin) { 58 | mZeroBin = zeroBin; 59 | } 60 | 61 | protected: 62 | std::string mCaption, mHeader, mFooter; 63 | nanogui::Color mBackgroundColor, mForegroundColor, mTextColor; 64 | std::vector mValues; 65 | int mNChannels = 1; 66 | float mMinimum = 0, mMean = 0, mMaximum = 0; 67 | int mZeroBin = 0; 68 | }; 69 | 70 | TEV_NAMESPACE_END 71 | -------------------------------------------------------------------------------- /src/Channel.cpp: -------------------------------------------------------------------------------- 1 | // This file was developed by Thomas Müller . 2 | // It is published under the BSD 3-Clause License within the LICENSE file. 3 | 4 | #include 5 | #include 6 | 7 | #include 8 | 9 | using namespace nanogui; 10 | using namespace std; 11 | 12 | TEV_NAMESPACE_BEGIN 13 | 14 | Channel::Channel(const std::string& name, const nanogui::Vector2i& size) 15 | : mName{name}, mSize{size} { 16 | mData.resize((size_t)mSize.x() * mSize.y()); 17 | } 18 | 19 | Task Channel::divideByAsync(const Channel& other, int priority) { 20 | co_await ThreadPool::global().parallelForAsync(0, other.numPixels(), [&](size_t i) { 21 | if (other.at(i) != 0) { 22 | at(i) /= other.at(i); 23 | } else { 24 | at(i) = 0; 25 | } 26 | }, priority); 27 | } 28 | 29 | Task Channel::multiplyWithAsync(const Channel& other, int priority) { 30 | co_await ThreadPool::global().parallelForAsync(0, other.numPixels(), [&](size_t i) { 31 | at(i) *= other.at(i); 32 | }, priority); 33 | } 34 | 35 | void Channel::updateTile(int x, int y, int width, int height, const vector& newData) { 36 | if (x < 0 || y < 0 || x + width > size().x() || y + height > size().y()) { 37 | tlog::warning() << "Tile [" << x << "," << y << "," << width << "," << height << "] could not be updated because it does not fit into the channel's size " << size(); 38 | return; 39 | } 40 | 41 | for (int posY = 0; posY < height; ++posY) { 42 | for (int posX = 0; posX < width; ++posX) { 43 | at({x + posX, y + posY}) = newData[posX + posY * width]; 44 | } 45 | } 46 | } 47 | 48 | pair Channel::split(const string& channel) { 49 | size_t dotPosition = channel.rfind("."); 50 | if (dotPosition != string::npos) { 51 | return {channel.substr(0, dotPosition + 1), channel.substr(dotPosition + 1)}; 52 | } 53 | 54 | return {"", channel}; 55 | } 56 | 57 | string Channel::tail(const string& channel) { 58 | return split(channel).second; 59 | } 60 | 61 | string Channel::head(const string& channel) { 62 | return split(channel).first; 63 | } 64 | 65 | bool Channel::isTopmost(const string& channel) { 66 | return tail(channel) == channel; 67 | } 68 | 69 | Color Channel::color(string channel) { 70 | channel = toLower(tail(channel)); 71 | 72 | if (channel == "r") { 73 | return Color(0.8f, 0.2f, 0.2f, 1.0f); 74 | } else if (channel == "g") { 75 | return Color(0.2f, 0.8f, 0.2f, 1.0f); 76 | } else if (channel == "b") { 77 | return Color(0.2f, 0.3f, 1.0f, 1.0f); 78 | } 79 | 80 | return Color(1.0f, 1.0f); 81 | } 82 | 83 | TEV_NAMESPACE_END 84 | -------------------------------------------------------------------------------- /src/imageio/ExrImageSaver.cpp: -------------------------------------------------------------------------------- 1 | // This file was developed by Thomas Müller . 2 | // It is published under the BSD 3-Clause License within the LICENSE file. 3 | 4 | #include 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | #include 13 | #include 14 | #include 15 | 16 | using namespace nanogui; 17 | using namespace std; 18 | 19 | TEV_NAMESPACE_BEGIN 20 | 21 | class StdOStream: public Imf::OStream 22 | { 23 | public: 24 | StdOStream(ostream& stream, const char fileName[]) 25 | : Imf::OStream{fileName}, mStream{stream} { } 26 | 27 | void write(const char c[/*n*/], int n) { 28 | clearError(); 29 | mStream.write (c, n); 30 | checkError(mStream); 31 | } 32 | 33 | Imf::Int64 tellp() { 34 | return std::streamoff(mStream.tellp()); 35 | } 36 | 37 | void seekp(Imf::Int64 pos) { 38 | mStream.seekp(pos); 39 | checkError(mStream); 40 | } 41 | 42 | private: 43 | // The following error-checking functions were copy&pasted from the OpenEXR source code 44 | static void clearError() { 45 | errno = 0; 46 | } 47 | 48 | static void checkError(ostream& os) { 49 | if (!os) { 50 | if (errno) { 51 | IEX_NAMESPACE::throwErrnoExc(); 52 | } 53 | 54 | throw IEX_NAMESPACE::ErrnoExc("File output failed."); 55 | } 56 | } 57 | 58 | ostream& mStream; 59 | }; 60 | 61 | void ExrImageSaver::save(ostream& oStream, const fs::path& path, const vector& data, const Vector2i& imageSize, int nChannels) const { 62 | vector channelNames = { 63 | "R", "G", "B", "A", 64 | }; 65 | 66 | if (nChannels <= 0 || nChannels > 4) { 67 | throw invalid_argument{tfm::format("Invalid number of channels %d.", nChannels)}; 68 | } 69 | 70 | Imf::Header header{imageSize.x(), imageSize.y()}; 71 | Imf::FrameBuffer frameBuffer; 72 | 73 | for (int i = 0; i < nChannels; ++i) { 74 | header.channels().insert(channelNames[i], Imf::Channel(Imf::FLOAT)); 75 | frameBuffer.insert(channelNames[i], Imf::Slice( 76 | Imf::FLOAT, // Type 77 | (char*)(data.data() + i), // Base pointer 78 | sizeof(float) * nChannels, // x-stride in bytes 79 | sizeof(float) * imageSize.x() * nChannels // y-stride in bytes 80 | )); 81 | } 82 | 83 | StdOStream imfOStream{oStream, toString(path).c_str()}; 84 | Imf::OutputFile file{imfOStream, header}; 85 | file.setFrameBuffer(frameBuffer); 86 | file.writePixels(imageSize.y()); 87 | } 88 | 89 | TEV_NAMESPACE_END 90 | -------------------------------------------------------------------------------- /scripts/create-dmg/README.md: -------------------------------------------------------------------------------- 1 | create-dmg 2 | ========== 3 | 4 | A shell script to build fancy DMGs. 5 | 6 | 7 | Status and contribution policy 8 | ------------------------------ 9 | 10 | This project is maintained thanks to the contributors who send pull requests. The original author has no use for the project, so his only role is reviewing and merging pull requests. 11 | 12 | I will merge any pull request that adds something useful and does not break existing things. 13 | 14 | Starting in January 2015, everyone who gets a pull request merged gets commit access to the repository. 15 | 16 | 17 | Installation 18 | ------------ 19 | 20 | By being a shell script, yoursway-create-dmg installation is very simple. Simply download and run. 21 | 22 | > git clone https://github.com/andreyvit/yoursway-create-dmg.git 23 | > cd yoursway-create-dmg 24 | > ./create-dmg [options] 25 | 26 | 27 | Usage 28 | ----- 29 | 30 | > create-dmg [options...] [output\_name.dmg] [source\_folder] 31 | 32 | All contents of source\_folder will be copied into the disk image. 33 | 34 | **Options:** 35 | 36 | * **--volname [name]:** set volume name (displayed in the Finder sidebar and window title) 37 | * **--volicon [icon.icns]:** set volume icon 38 | * **--background [pic.png]:** set folder background image (provide png, gif, jpg) 39 | * **--window-pos [x y]:** set position the folder window 40 | * **--window-size [width height]:** set size of the folder window 41 | * **--text-size [text size]:** set window text size (10-16) 42 | * **--icon-size [icon size]:** set window icons size (up to 128) 43 | * **--icon [file name] [x y]:** set position of the file's icon 44 | * **--hide-extension [file name]:** hide the extension of file 45 | * **--custom-icon [file name]/[custom icon]/[sample file] [x y]:** set position and custom icon 46 | * **--app-drop-link [x y]:** make a drop link to Applications, at location x, y 47 | * **--eula [eula file]:** attach a license file to the dmg 48 | * **--no-internet-enable:** disable automatic mount© 49 | * **--version:** show tool version number 50 | * **-h, --help:** display the help 51 | 52 | 53 | Example 54 | ------- 55 | 56 | > \#!/bin/sh 57 | > test -f Application-Installer.dmg && rm Application-Installer.dmg 58 | > create-dmg \ 59 | > --volname "Application Installer" \ 60 | > --volicon "application\_icon.icns" \ 61 | > --background "installer\_background.png" \ 62 | > --window-pos 200 120 \ 63 | > --window-size 800 400 \ 64 | > --icon-size 100 \ 65 | > --icon Application.app 200 190 \ 66 | > --hide-extension Application.app \ 67 | > --app-drop-link 600 185 \ 68 | > Application-Installer.dmg \ 69 | > source\_folder/ 70 | 71 | 72 | Alternatives 73 | ------------ 74 | 75 | * [node-appdmg](https://github.com/LinusU/node-appdmg) 76 | * [dmgbuild](https://pypi.python.org/pypi/dmgbuild) 77 | * see the [StackOverflow question](http://stackoverflow.com/questions/96882/how-do-i-create-a-nice-looking-dmg-for-mac-os-x-using-command-line-tools) 78 | -------------------------------------------------------------------------------- /include/tev/Channel.h: -------------------------------------------------------------------------------- 1 | // This file was developed by Thomas Müller . 2 | // It is published under the BSD 3-Clause License within the LICENSE file. 3 | 4 | #pragma once 5 | 6 | #include 7 | #include 8 | 9 | #include 10 | 11 | #include 12 | #include 13 | #include 14 | 15 | TEV_NAMESPACE_BEGIN 16 | 17 | class Channel { 18 | public: 19 | Channel(const std::string& name, const nanogui::Vector2i& size); 20 | 21 | const std::string& name() const { 22 | return mName; 23 | } 24 | 25 | const std::vector& data() const { 26 | return mData; 27 | } 28 | 29 | float eval(size_t index) const { 30 | if (index >= mData.size()) { 31 | return 0; 32 | } 33 | return mData[index]; 34 | } 35 | 36 | float eval(nanogui::Vector2i index) const { 37 | if (index.x() < 0 || index.x() >= mSize.x() || 38 | index.y() < 0 || index.y() >= mSize.y()) { 39 | return 0; 40 | } 41 | 42 | return mData[index.x() + index.y() * mSize.x()]; 43 | } 44 | 45 | float& at(size_t index) { 46 | return mData[index]; 47 | } 48 | 49 | float at(size_t index) const { 50 | return mData[index]; 51 | } 52 | 53 | float& at(nanogui::Vector2i index) { 54 | return at(index.x() + index.y() * mSize.x()); 55 | } 56 | 57 | float at(nanogui::Vector2i index) const { 58 | return at(index.x() + index.y() * mSize.x()); 59 | } 60 | 61 | size_t numPixels() const { 62 | return mData.size(); 63 | } 64 | 65 | const nanogui::Vector2i& size() const { 66 | return mSize; 67 | } 68 | 69 | std::tuple minMaxMean() const { 70 | float min = std::numeric_limits::infinity(); 71 | float max = -std::numeric_limits::infinity(); 72 | float mean = 0; 73 | for (float f : mData) { 74 | mean += f; 75 | if (f < min) { 76 | min = f; 77 | } 78 | if (f > max) { 79 | max = f; 80 | } 81 | } 82 | return {min, max, mean/numPixels()}; 83 | } 84 | 85 | Task divideByAsync(const Channel& other, int priority); 86 | 87 | Task multiplyWithAsync(const Channel& other, int priority); 88 | 89 | void setZero() { memset(mData.data(), 0, mData.size()*sizeof(float)); } 90 | 91 | void updateTile(int x, int y, int width, int height, const std::vector& newData); 92 | 93 | static std::pair split(const std::string& fullChannel); 94 | 95 | static std::string tail(const std::string& fullChannel); 96 | static std::string head(const std::string& fullChannel); 97 | 98 | static bool isTopmost(const std::string& fullChannel); 99 | 100 | static nanogui::Color color(std::string fullChannel); 101 | 102 | private: 103 | std::string mName; 104 | nanogui::Vector2i mSize; 105 | std::vector mData; 106 | }; 107 | 108 | TEV_NAMESPACE_END 109 | -------------------------------------------------------------------------------- /include/tev/Lazy.h: -------------------------------------------------------------------------------- 1 | // This file was developed by Thomas Müller . 2 | // It is published under the BSD 3-Clause License within the LICENSE file. 3 | 4 | #pragma once 5 | 6 | #include 7 | #include 8 | 9 | #include 10 | #include 11 | #include 12 | 13 | TEV_NAMESPACE_BEGIN 14 | 15 | // Encapsulates a lazy, potentially asynchronous computation 16 | // of some value. The public interface of this object is not 17 | // thread-safe, i.e. it is expected to never be used from 18 | // multiple threads at once. 19 | template 20 | class Lazy { 21 | public: 22 | Lazy(std::function compute) 23 | : Lazy{compute, nullptr} { 24 | } 25 | 26 | Lazy(std::function compute, ThreadPool* threadPool) 27 | : mThreadPool{threadPool}, mCompute{compute} { 28 | } 29 | 30 | Lazy(std::future&& future) 31 | : mAsyncValue{std::move(future)} { 32 | } 33 | 34 | T get() { 35 | if (mIsComputed) { 36 | return mValue; 37 | } 38 | 39 | if (mAsyncValue.valid()) { 40 | mValue = mAsyncValue.get(); 41 | } else { 42 | mValue = compute(); 43 | } 44 | 45 | mIsComputed = true; 46 | mBecameReadyAt = std::chrono::steady_clock::now(); 47 | return mValue; 48 | } 49 | 50 | bool isReady() const { 51 | if (mIsComputed) { 52 | TEV_ASSERT( 53 | !mAsyncValue.valid(), 54 | "There should never be a background computation while the result is already available." 55 | ); 56 | 57 | return true; 58 | } 59 | 60 | if (!mAsyncValue.valid()) { 61 | return false; 62 | } 63 | 64 | return mAsyncValue.wait_for(std::chrono::seconds{0}) == std::future_status::ready; 65 | } 66 | 67 | std::chrono::steady_clock::time_point becameReadyAt() const { 68 | if (!isReady()) { 69 | return std::chrono::steady_clock::now(); 70 | } else { 71 | return mBecameReadyAt; 72 | } 73 | } 74 | 75 | void computeAsync(int priority) { 76 | // No need to perform an async computation if we 77 | // already computed the value before or if one is 78 | // already running. 79 | if (mAsyncValue.valid() || mIsComputed) { 80 | return; 81 | } 82 | 83 | if (mThreadPool) { 84 | mAsyncValue = mThreadPool->enqueueTask([this]() { return compute(); }, priority); 85 | } else { 86 | mAsyncValue = std::async(std::launch::async, [this]() { return compute(); }); 87 | } 88 | } 89 | 90 | private: 91 | T compute() { 92 | T result = mCompute(); 93 | mCompute = std::function{}; 94 | return result; 95 | } 96 | 97 | // If this thread pool is present, use it to run tasks 98 | // 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 | TEV_NAMESPACE_END 109 | -------------------------------------------------------------------------------- /src/ThreadPool.cpp: -------------------------------------------------------------------------------- 1 | // This file was developed by Thomas Müller . 2 | // It is published under the BSD 3-Clause License within the LICENSE file. 3 | 4 | #include 5 | 6 | #include 7 | 8 | using namespace std; 9 | 10 | TEV_NAMESPACE_BEGIN 11 | 12 | ThreadPool::ThreadPool() 13 | : ThreadPool{thread::hardware_concurrency()} { 14 | } 15 | 16 | ThreadPool::ThreadPool(size_t maxNumThreads, bool force) { 17 | if (!force) { 18 | maxNumThreads = min((size_t)thread::hardware_concurrency(), maxNumThreads); 19 | } 20 | startThreads(maxNumThreads); 21 | mNumTasksInSystem.store(0); 22 | } 23 | 24 | ThreadPool::~ThreadPool() { 25 | waitUntilFinished(); 26 | shutdownThreads(mThreads.size()); 27 | } 28 | 29 | void ThreadPool::startThreads(size_t num) { 30 | mNumThreads += num; 31 | for (size_t i = mThreads.size(); i < mNumThreads; ++i) { 32 | mThreads.emplace_back([this, i] { 33 | while (true) { 34 | unique_lock lock{mTaskQueueMutex}; 35 | 36 | // look for a work item 37 | while (i < mNumThreads && mTaskQueue.empty()) { 38 | // if there are none wait for notification 39 | mWorkerCondition.wait(lock); 40 | } 41 | 42 | if (i >= mNumThreads) { 43 | break; 44 | } 45 | 46 | function task{move(mTaskQueue.top().fun)}; 47 | mTaskQueue.pop(); 48 | 49 | // Unlock the lock, so we can process the task without blocking other threads 50 | lock.unlock(); 51 | 52 | task(); 53 | 54 | mNumTasksInSystem--; 55 | 56 | { 57 | unique_lock localLock{mSystemBusyMutex}; 58 | 59 | if (mNumTasksInSystem == 0) { 60 | mSystemBusyCondition.notify_all(); 61 | } 62 | } 63 | } 64 | }); 65 | } 66 | } 67 | 68 | void ThreadPool::shutdownThreads(size_t num) { 69 | auto numToClose = min(num, mNumThreads); 70 | 71 | { 72 | lock_guard lock{mTaskQueueMutex}; 73 | mNumThreads -= numToClose; 74 | } 75 | 76 | // Wake up all the threads to have them quit 77 | mWorkerCondition.notify_all(); 78 | for (auto i = 0u; i < numToClose; ++i) { 79 | mThreads.back().join(); 80 | mThreads.pop_back(); 81 | } 82 | } 83 | 84 | void ThreadPool::waitUntilFinished() { 85 | unique_lock lock{mSystemBusyMutex}; 86 | 87 | if (mNumTasksInSystem == 0) { 88 | return; 89 | } 90 | 91 | mSystemBusyCondition.wait(lock); 92 | } 93 | 94 | void ThreadPool::waitUntilFinishedFor(const chrono::microseconds Duration) { 95 | unique_lock lock{mSystemBusyMutex}; 96 | 97 | if (mNumTasksInSystem == 0) { 98 | return; 99 | } 100 | 101 | mSystemBusyCondition.wait_for(lock, Duration); 102 | } 103 | 104 | void ThreadPool::flushQueue() { 105 | lock_guard lock{mTaskQueueMutex}; 106 | 107 | mNumTasksInSystem -= mTaskQueue.size(); 108 | while (!mTaskQueue.empty()) { 109 | mTaskQueue.pop(); 110 | } 111 | } 112 | 113 | TEV_NAMESPACE_END 114 | -------------------------------------------------------------------------------- /src/imageio/StbiImageLoader.cpp: -------------------------------------------------------------------------------- 1 | // This file was developed by Thomas Müller . 2 | // It is published under the BSD 3-Clause License within the LICENSE file. 3 | 4 | #include 5 | #include 6 | 7 | #include 8 | 9 | using namespace nanogui; 10 | using namespace std; 11 | 12 | TEV_NAMESPACE_BEGIN 13 | 14 | bool StbiImageLoader::canLoadFile(istream&) const { 15 | // Pretend you can load any file and throw exception on failure. 16 | // TODO: Add proper check. 17 | return true; 18 | } 19 | 20 | Task> StbiImageLoader::load(istream& iStream, const fs::path&, const string& channelSelector, int priority) const { 21 | vector result(1); 22 | ImageData& resultData = result.front(); 23 | 24 | static const stbi_io_callbacks callbacks = { 25 | // Read 26 | [](void* context, char* data, int size) { 27 | auto stream = reinterpret_cast(context); 28 | stream->read(data, size); 29 | return (int)stream->gcount(); 30 | }, 31 | // Seek 32 | [](void* context, int size) { 33 | reinterpret_cast(context)->seekg(size, ios_base::cur); 34 | }, 35 | // EOF 36 | [](void* context) { 37 | return (int)!!(*reinterpret_cast(context)); 38 | }, 39 | }; 40 | 41 | void* data; 42 | int numChannels; 43 | Vector2i size; 44 | bool isHdr = stbi_is_hdr_from_callbacks(&callbacks, &iStream) != 0; 45 | iStream.clear(); 46 | iStream.seekg(0); 47 | 48 | if (isHdr) { 49 | data = stbi_loadf_from_callbacks(&callbacks, &iStream, &size.x(), &size.y(), &numChannels, 0); 50 | } else { 51 | data = stbi_load_from_callbacks(&callbacks, &iStream, &size.x(), &size.y(), &numChannels, 0); 52 | } 53 | 54 | if (!data) { 55 | throw invalid_argument{tfm::format("%s", stbi_failure_reason())}; 56 | } 57 | 58 | if (size.x() == 0 || size.y() == 0) { 59 | throw invalid_argument{"Image has zero pixels."}; 60 | } 61 | 62 | ScopeGuard dataGuard{[data] { stbi_image_free(data); }}; 63 | 64 | resultData.channels = makeNChannels(numChannels, size); 65 | int alphaChannelIndex = 3; 66 | 67 | auto numPixels = (size_t)size.x() * size.y(); 68 | if (isHdr) { 69 | co_await ThreadPool::global().parallelForAsync(0, numPixels, [&](size_t i) { 70 | auto typedData = reinterpret_cast(data); 71 | size_t baseIdx = i * numChannels; 72 | for (int c = 0; c < numChannels; ++c) { 73 | resultData.channels[c].at(i) = typedData[baseIdx + c]; 74 | } 75 | }, priority); 76 | } else { 77 | co_await ThreadPool::global().parallelForAsync(0, numPixels, [&](size_t i) { 78 | auto typedData = reinterpret_cast(data); 79 | size_t baseIdx = i * numChannels; 80 | for (int c = 0; c < numChannels; ++c) { 81 | if (c == alphaChannelIndex) { 82 | resultData.channels[c].at(i) = (typedData[baseIdx + c]) / 255.0f; 83 | } else { 84 | resultData.channels[c].at(i) = toLinear((typedData[baseIdx + c]) / 255.0f); 85 | } 86 | } 87 | }, priority); 88 | } 89 | 90 | resultData.hasPremultipliedAlpha = false; 91 | 92 | co_return result; 93 | } 94 | 95 | TEV_NAMESPACE_END 96 | -------------------------------------------------------------------------------- /src/imageio/QoiImageLoader.cpp: -------------------------------------------------------------------------------- 1 | // This file was developed by Tiago Chaves & Thomas Müller . 2 | // It is published under the BSD 3-Clause License within the LICENSE file. 3 | 4 | #include 5 | #include 6 | 7 | #define QOI_NO_STDIO 8 | #define QOI_IMPLEMENTATION 9 | #include 10 | 11 | using namespace nanogui; 12 | using namespace std; 13 | 14 | TEV_NAMESPACE_BEGIN 15 | 16 | bool QoiImageLoader::canLoadFile(istream& iStream) const { 17 | char b[4]; 18 | iStream.read(b, sizeof(b)); 19 | 20 | bool result = !!iStream && iStream.gcount() == sizeof(b) && string(b, sizeof(b)) == "qoif"; 21 | 22 | iStream.clear(); 23 | iStream.seekg(0); 24 | return result; 25 | } 26 | 27 | Task> QoiImageLoader::load(istream& iStream, const fs::path&, const string& channelSelector, int priority) const { 28 | vector result(1); 29 | ImageData& resultData = result.front(); 30 | 31 | char magic[4]; 32 | iStream.read(magic, 4); 33 | string magicString(magic, 4); 34 | 35 | if (magicString != "qoif") { 36 | throw invalid_argument{tfm::format("Invalid magic QOI string %s", magicString)}; 37 | } 38 | 39 | iStream.clear(); 40 | iStream.seekg(0, iStream.end); 41 | size_t dataSize = iStream.tellg(); 42 | iStream.seekg(0, iStream.beg); 43 | vector data(dataSize); 44 | iStream.read(data.data(), dataSize); 45 | 46 | qoi_desc desc; 47 | void* decodedData = qoi_decode(data.data(), static_cast(dataSize), &desc, 0); 48 | 49 | ScopeGuard decodedDataGuard{[decodedData] { free(decodedData); }}; 50 | 51 | if (!decodedData) { 52 | throw invalid_argument{"Failed to decode data from the QOI format."}; 53 | } 54 | 55 | Vector2i size{static_cast(desc.width), static_cast(desc.height)}; 56 | auto numPixels = (size_t)size.x() * size.y(); 57 | if (numPixels == 0) { 58 | throw invalid_argument{"Image has zero pixels."}; 59 | } 60 | 61 | int numChannels = static_cast(desc.channels); 62 | if (numChannels != 4 && numChannels != 3) { 63 | throw invalid_argument{tfm::format("Invalid number of channels %d.", numChannels)}; 64 | } 65 | 66 | resultData.channels = makeNChannels(numChannels, size); 67 | resultData.hasPremultipliedAlpha = false; 68 | 69 | if (desc.colorspace == QOI_LINEAR) { 70 | co_await ThreadPool::global().parallelForAsync(0, numPixels, [&](size_t i) { 71 | auto typedData = reinterpret_cast(decodedData); 72 | size_t baseIdx = i * numChannels; 73 | for (int c = 0; c < numChannels; ++c) { 74 | resultData.channels[c].at(i) = (typedData[baseIdx + c]) / 255.0f; 75 | } 76 | }, priority); 77 | } else { 78 | co_await ThreadPool::global().parallelForAsync(0, numPixels, [&](size_t i) { 79 | auto typedData = reinterpret_cast(decodedData); 80 | size_t baseIdx = i * numChannels; 81 | for (int c = 0; c < numChannels; ++c) { 82 | if (c == 3) { 83 | resultData.channels[c].at(i) = (typedData[baseIdx + c]) / 255.0f; 84 | } else { 85 | resultData.channels[c].at(i) = toLinear((typedData[baseIdx + c]) / 255.0f); 86 | } 87 | } 88 | }, priority); 89 | } 90 | 91 | co_return result; 92 | } 93 | 94 | TEV_NAMESPACE_END 95 | -------------------------------------------------------------------------------- /src/imageio/PfmImageLoader.cpp: -------------------------------------------------------------------------------- 1 | // This file was developed by Thomas Müller . 2 | // It is published under the BSD 3-Clause License within the LICENSE file. 3 | 4 | #include 5 | #include 6 | 7 | #include 8 | 9 | using namespace nanogui; 10 | using namespace std; 11 | 12 | TEV_NAMESPACE_BEGIN 13 | 14 | bool PfmImageLoader::canLoadFile(istream& iStream) const { 15 | char b[2]; 16 | iStream.read(b, sizeof(b)); 17 | 18 | bool result = !!iStream && iStream.gcount() == sizeof(b) && b[0] == 'P' && (b[1] == 'F' || b[1] == 'f'); 19 | 20 | iStream.clear(); 21 | iStream.seekg(0); 22 | return result; 23 | } 24 | 25 | Task> PfmImageLoader::load(istream& iStream, const fs::path&, const string& channelSelector, int priority) const { 26 | vector result(1); 27 | ImageData& resultData = result.front(); 28 | 29 | string magic; 30 | Vector2i size; 31 | float scale; 32 | 33 | iStream >> magic >> size.x() >> size.y() >> scale; 34 | 35 | int numChannels; 36 | if (magic == "Pf") { 37 | numChannels = 1; 38 | } else if (magic == "PF") { 39 | numChannels = 3; 40 | } else if (magic == "PF4") { 41 | numChannels = 4; 42 | } else { 43 | throw invalid_argument{tfm::format("Invalid magic PFM string %s", magic)}; 44 | } 45 | 46 | if (!isfinite(scale) || scale == 0) { 47 | throw invalid_argument{tfm::format("Invalid PFM scale %f", scale)}; 48 | } 49 | 50 | bool isPfmLittleEndian = scale < 0; 51 | scale = abs(scale); 52 | 53 | resultData.channels = makeNChannels(numChannels, size); 54 | 55 | auto numPixels = (size_t)size.x() * size.y(); 56 | if (numPixels == 0) { 57 | throw invalid_argument{"Image has zero pixels."}; 58 | } 59 | 60 | auto numFloats = numPixels * numChannels; 61 | auto numBytes = numFloats * sizeof(float); 62 | 63 | // Skip last newline at the end of the header. 64 | char c; 65 | while (iStream.get(c) && c != '\r' && c != '\n'); 66 | 67 | // Read entire file in binary mode. 68 | vector data(numFloats); 69 | iStream.read(reinterpret_cast(data.data()), numBytes); 70 | if (iStream.gcount() < (streamsize)numBytes) { 71 | throw invalid_argument{tfm::format("Not sufficient bytes to read (%d vs %d)", iStream.gcount(), numBytes)}; 72 | } 73 | 74 | // Reverse bytes of every float if endianness does not match up with system 75 | const bool shallSwapBytes = (std::endian::native == std::endian::little) != isPfmLittleEndian; 76 | 77 | co_await ThreadPool::global().parallelForAsync(0, size.y(), [&](int y) { 78 | for (int x = 0; x < size.x(); ++x) { 79 | int baseIdx = (y * size.x() + x) * numChannels; 80 | for (int c = 0; c < numChannels; ++c) { 81 | float val = data[baseIdx + c]; 82 | 83 | // Thankfully, due to branch prediction, the "if" in the 84 | // inner loop is no significant overhead. 85 | if (shallSwapBytes) { 86 | val = swapBytes(val); 87 | } 88 | 89 | // Flip image vertically due to PFM format 90 | resultData.channels[c].at({x, size.y() - (int)y - 1}) = scale * val; 91 | } 92 | } 93 | }, priority); 94 | 95 | resultData.hasPremultipliedAlpha = false; 96 | 97 | co_return result; 98 | } 99 | 100 | TEV_NAMESPACE_END 101 | -------------------------------------------------------------------------------- /src/imageio/ClipboardImageLoader.cpp: -------------------------------------------------------------------------------- 1 | // This file was developed by Thomas Müller . 2 | // It is published under the BSD 3-Clause License within the LICENSE file. 3 | 4 | #include 5 | #include 6 | 7 | #include 8 | 9 | using namespace nanogui; 10 | using namespace std; 11 | 12 | TEV_NAMESPACE_BEGIN 13 | 14 | bool ClipboardImageLoader::canLoadFile(istream& iStream) const { 15 | char b[4]; 16 | iStream.read(b, sizeof(b)); 17 | 18 | bool result = !!iStream && iStream.gcount() == sizeof(b) && string(b, sizeof(b)) == "clip"; 19 | 20 | iStream.clear(); 21 | iStream.seekg(0); 22 | return result; 23 | } 24 | 25 | Task> ClipboardImageLoader::load(istream& iStream, const fs::path&, const string& channelSelector, int priority) const { 26 | vector result(1); 27 | ImageData& resultData = result.front(); 28 | 29 | char magic[4]; 30 | clip::image_spec spec; 31 | 32 | iStream.read(magic, 4); 33 | string magicString(magic, 4); 34 | 35 | if (magicString != "clip") { 36 | throw invalid_argument{tfm::format("Invalid magic clipboard string %s", magicString)}; 37 | } 38 | 39 | iStream.read(reinterpret_cast(&spec), sizeof(clip::image_spec)); 40 | if (iStream.gcount() < (streamsize)sizeof(clip::image_spec)) { 41 | throw invalid_argument{tfm::format("Not sufficient bytes to read image spec (%d vs %d)", iStream.gcount(), sizeof(clip::image_spec))}; 42 | } 43 | 44 | Vector2i size{(int)spec.width, (int)spec.height}; 45 | 46 | auto numPixels = (size_t)size.x() * size.y(); 47 | if (numPixels == 0) { 48 | throw invalid_argument{"Image has zero pixels."}; 49 | } 50 | 51 | auto numChannels = (int)(spec.bits_per_pixel / 8); 52 | if (numChannels > 4) { 53 | throw invalid_argument{"Image has too many channels."}; 54 | } 55 | 56 | 57 | auto numBytesPerRow = numChannels * size.x(); 58 | auto numBytes = (size_t)numBytesPerRow * size.y(); 59 | int alphaChannelIndex = 3; 60 | 61 | resultData.channels = makeNChannels(numChannels, size); 62 | 63 | vector data(numBytes); 64 | iStream.read(reinterpret_cast(data.data()), numBytes); 65 | if (iStream.gcount() < (streamsize)numBytes) { 66 | throw invalid_argument{tfm::format("Not sufficient bytes to read image data (%d vs %d)", iStream.gcount(), numBytes)}; 67 | } 68 | 69 | int shifts[4] = { 70 | (int)(spec.red_shift / 8), 71 | (int)(spec.green_shift / 8), 72 | (int)(spec.blue_shift / 8), 73 | (int)(spec.alpha_shift / 8), 74 | }; 75 | 76 | for (int c = 0; c < numChannels; ++c) { 77 | if (shifts[c] >= numChannels) { 78 | throw invalid_argument{"Invalid shift encountered in clipboard image."}; 79 | } 80 | } 81 | 82 | // TODO: figure out when alpha is already premultiplied (prior to tonemapping). 83 | // clip doesn't properly handle this... so copy&pasting transparent images 84 | // from browsers tends to produce incorrect color values in alpha!=1/0 regions. 85 | bool premultipliedAlpha = false && numChannels >= 4; 86 | co_await ThreadPool::global().parallelForAsync(0, size.y(), [&](int y) { 87 | for (int x = 0; x < size.x(); ++x) { 88 | int baseIdx = y * numBytesPerRow + x * numChannels; 89 | for (int c = numChannels-1; c >= 0; --c) { 90 | unsigned char val = data[baseIdx + shifts[c]]; 91 | if (c == alphaChannelIndex) { 92 | resultData.channels[c].at({x, y}) = val / 255.0f; 93 | } else { 94 | float alpha = premultipliedAlpha ? resultData.channels[alphaChannelIndex].at({x, y}) : 1.0f; 95 | float alphaFactor = alpha == 0 ? 0 : (1.0f / alpha); 96 | resultData.channels[c].at({x, y}) = toLinear(val / 255.0f * alphaFactor); 97 | } 98 | } 99 | } 100 | }, priority); 101 | 102 | resultData.hasPremultipliedAlpha = false; 103 | 104 | co_return result; 105 | } 106 | 107 | TEV_NAMESPACE_END 108 | -------------------------------------------------------------------------------- /dependencies/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # Some components of this file were taken from PBRT-v3 (BSD 2-clause "Simplified" License) 2 | # PBRT-v3 can be found here: https://github.com/mmp/pbrt-v3 3 | cmake_minimum_required(VERSION 3.13) 4 | 5 | if (NOT EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/nanogui/CMakeLists.txt") 6 | message(FATAL_ERROR 7 | "Some tev dependencies are missing. " 8 | "If you forgot the '--recursive' flag when cloning this project, " 9 | "this can be fixed by calling 'git submodule update --init --recursive'." 10 | ) 11 | endif() 12 | 13 | # Compile nanogui 14 | set(NANOGUI_BUILD_EXAMPLES OFF CACHE BOOL " " FORCE) 15 | set(NANOGUI_BUILD_SHARED OFF CACHE BOOL " " FORCE) 16 | set(NANOGUI_BUILD_PYTHON OFF CACHE BOOL " " FORCE) 17 | set(NANOGUI_INSTALL OFF CACHE BOOL " " FORCE) 18 | add_subdirectory(nanogui) 19 | 20 | set(NANOGUI_TARGETS nanogui glfw glfw_objects) 21 | set_property(TARGET ${NANOGUI_TARGETS} PROPERTY FOLDER "dependencies") 22 | 23 | # Compile zlib (only on Windows) 24 | if (WIN32) 25 | set(ZLIB_BUILD_STATIC_LIBS ON CACHE BOOL " " FORCE) 26 | set(ZLIB_BUILD_SHARED_LIBS OFF CACHE BOOL " " FORCE) 27 | set(SKIP_INSTALL_ALL ON CACHE BOOL " " FORCE) 28 | add_subdirectory(zlib) 29 | set_property(TARGET zlibstatic PROPERTY FOLDER "dependencies") 30 | 31 | set(ZLIB_INCLUDE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/zlib" CACHE PATH " " FORCE) 32 | set(ZLIB_LIBRARY zlibstatic) 33 | 34 | include_directories(${ZLIB_INCLUDE_DIR} "${CMAKE_CURRENT_BINARY_DIR}/zlib") 35 | endif() 36 | 37 | # Compile DirectXTex (only on Windows) 38 | if (WIN32) 39 | set(BUILD_TOOLS OFF CACHE BOOL " " FORCE) 40 | set(BUILD_DX11 OFF CACHE BOOL " " FORCE) 41 | set(BUILD_DX12 OFF CACHE BOOL " " FORCE) 42 | set(BC_USE_OPENMP OFF CACHE BOOL " " FORCE) 43 | add_subdirectory(DirectXTex) 44 | set_property(TARGET DirectXTex PROPERTY FOLDER "dependencies") 45 | 46 | set(DIRECTXTEX_INCLUDE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/DirectXTex/DirectXTex" CACHE PATH " " FORCE) 47 | set(DIRECTXTEX_LIBRARY DirectXTex) 48 | 49 | include_directories(${DIRECTXTEX_INCLUDE_DIR}) 50 | endif() 51 | 52 | # Compile OpenEXR 53 | set(PYILMBASE_ENABLE OFF CACHE BOOL " " FORCE) 54 | set(ILMBASE_BUILD_SHARED_LIBS OFF CACHE BOOL " " FORCE) 55 | set(OPENEXR_BUILD_SHARED_LIBS OFF CACHE BOOL " " FORCE) 56 | set(ILMBASE_NAMESPACE_VERSIONING OFF CACHE BOOL " " FORCE) 57 | set(OPENEXR_NAMESPACE_VERSIONING OFF CACHE BOOL " " FORCE) 58 | add_subdirectory(openexr EXCLUDE_FROM_ALL) 59 | set_property(TARGET Half Iex IlmImf IlmThread Imath PROPERTY FOLDER "dependencies") 60 | 61 | # Compile clip 62 | set(CLIP_EXAMPLES OFF CACHE BOOL " " FORCE) 63 | set(CLIP_TESTS OFF CACHE BOOL " " FORCE) 64 | add_subdirectory(clip) 65 | 66 | # Manually populate locations of our included dependencies. 67 | set(ARGS_INCLUDE ${CMAKE_CURRENT_SOURCE_DIR}/args PARENT_SCOPE) 68 | 69 | set(CLIP_INCLUDE ${CMAKE_CURRENT_SOURCE_DIR}/clip PARENT_SCOPE) 70 | 71 | if (NOT ${CMAKE_SYSTEM_NAME} MATCHES "Emscripten") 72 | set(GLFW_INCLUDE ${CMAKE_CURRENT_SOURCE_DIR}/nanogui/ext/glfw/include PARENT_SCOPE) 73 | endif() 74 | 75 | set(NANOGUI_INCLUDE 76 | ${CMAKE_CURRENT_SOURCE_DIR}/nanogui/ext/nanovg/src 77 | ${CMAKE_CURRENT_SOURCE_DIR}/nanogui/include 78 | PARENT_SCOPE) 79 | set(NANOGUI_EXTRA_INCS ${NANOGUI_EXTRA_INCS} PARENT_SCOPE) 80 | set(NANOGUI_EXTRA_DEFS ${NANOGUI_EXTRA_DEFS} PARENT_SCOPE) 81 | set(NANOGUI_EXTRA_LIBS ${NANOGUI_EXTRA_LIBS} PARENT_SCOPE) 82 | 83 | set(OPENEXR_INCLUDE_DIRS 84 | ${CMAKE_CURRENT_SOURCE_DIR}/openexr/IlmBase/Imath 85 | ${CMAKE_CURRENT_SOURCE_DIR}/openexr/IlmBase/Iex 86 | ${CMAKE_CURRENT_SOURCE_DIR}/openexr/IlmBase/Half 87 | ${CMAKE_CURRENT_SOURCE_DIR}/openexr/OpenEXR/IlmImf 88 | ${CMAKE_CURRENT_BINARY_DIR}/openexr/OpenEXR/config 89 | ${CMAKE_CURRENT_BINARY_DIR}/openexr/IlmBase/config 90 | PARENT_SCOPE) 91 | 92 | set(QOI_INCLUDE ${CMAKE_CURRENT_SOURCE_DIR}/qoi PARENT_SCOPE) 93 | 94 | set(STB_INCLUDE ${CMAKE_CURRENT_SOURCE_DIR}/stb PARENT_SCOPE) 95 | 96 | set(TINYFORMAT_INCLUDE ${CMAKE_CURRENT_SOURCE_DIR}/tinyformat PARENT_SCOPE) 97 | 98 | set(TINYLOGGER_INCLUDE ${CMAKE_CURRENT_SOURCE_DIR}/tinylogger PARENT_SCOPE) 99 | 100 | set(UTFCPP_INCLUDE ${CMAKE_CURRENT_SOURCE_DIR}/utfcpp/source PARENT_SCOPE) 101 | -------------------------------------------------------------------------------- /include/tev/ThreadPool.h: -------------------------------------------------------------------------------- 1 | // This file was developed by Thomas Müller . 2 | // It is published under the BSD 3-Clause License within the LICENSE file. 3 | 4 | #pragma once 5 | 6 | #include 7 | #include 8 | 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | 16 | 17 | TEV_NAMESPACE_BEGIN 18 | 19 | class ThreadPool { 20 | public: 21 | ThreadPool(); 22 | ThreadPool(size_t maxNumThreads, bool force = false); 23 | virtual ~ThreadPool(); 24 | 25 | static ThreadPool& global() { 26 | static ThreadPool pool; 27 | return pool; 28 | } 29 | 30 | template 31 | auto enqueueTask(F&& f, int priority) { 32 | using return_type = std::invoke_result_t; 33 | 34 | ++mNumTasksInSystem; 35 | 36 | auto task = std::make_shared>(std::forward(f)); 37 | 38 | auto res = task->get_future(); 39 | 40 | { 41 | std::lock_guard lock{mTaskQueueMutex}; 42 | mTaskQueue.push({priority, [task]() { (*task)(); }}); 43 | } 44 | 45 | mWorkerCondition.notify_one(); 46 | return res; 47 | } 48 | 49 | inline auto enqueueCoroutine(int priority) noexcept { 50 | class Awaiter { 51 | public: 52 | Awaiter(ThreadPool* pool, int priority) 53 | : mPool{pool}, mPriority{priority} {} 54 | 55 | bool await_ready() const noexcept { return false; } 56 | 57 | // Suspend and enqueue coroutine continuation onto the threadpool 58 | void await_suspend(COROUTINE_NAMESPACE::coroutine_handle<> coroutine) noexcept { 59 | mPool->enqueueTask(coroutine, mPriority); 60 | } 61 | 62 | void await_resume() const noexcept {} 63 | 64 | private: 65 | ThreadPool* mPool; 66 | int mPriority; 67 | }; 68 | 69 | return Awaiter{this, priority}; 70 | } 71 | 72 | void startThreads(size_t num); 73 | void shutdownThreads(size_t num); 74 | 75 | size_t numTasksInSystem() const { 76 | return mNumTasksInSystem; 77 | } 78 | 79 | void waitUntilFinished(); 80 | void waitUntilFinishedFor(const std::chrono::microseconds Duration); 81 | void flushQueue(); 82 | 83 | template 84 | Task parallelForAsync(Int start, Int end, F body, int priority) { 85 | Int range = end - start; 86 | Int nTasks = std::min((Int)mNumThreads, range); 87 | 88 | std::vector> tasks; 89 | for (Int i = 0; i < nTasks; ++i) { 90 | Int taskStart = start + (range * i / nTasks); 91 | Int taskEnd = start + (range * (i+1) / nTasks); 92 | TEV_ASSERT(taskStart != taskEnd, "Should not produce tasks with empty range."); 93 | 94 | tasks.emplace_back([](Int start, Int end, F body, int priority, ThreadPool* pool) -> Task { 95 | co_await pool->enqueueCoroutine(priority); 96 | for (Int j = start; j < end; ++j) { 97 | body(j); 98 | } 99 | }(taskStart, taskEnd, body, priority, this)); 100 | } 101 | 102 | for (auto& task : tasks) { 103 | co_await task; 104 | } 105 | } 106 | 107 | template 108 | void parallelFor(Int start, Int end, F body, int priority) { 109 | parallelForAsync(start, end, body, priority).get(); 110 | } 111 | 112 | private: 113 | size_t mNumThreads = 0; 114 | std::vector mThreads; 115 | 116 | struct QueuedTask { 117 | int priority; 118 | std::function fun; 119 | 120 | struct Comparator { 121 | bool operator()(const QueuedTask& a, const QueuedTask& b) { 122 | return a.priority < b.priority; 123 | } 124 | }; 125 | }; 126 | 127 | std::priority_queue, QueuedTask::Comparator> mTaskQueue; 128 | std::mutex mTaskQueueMutex; 129 | std::condition_variable mWorkerCondition; 130 | 131 | std::atomic mNumTasksInSystem; 132 | std::mutex mSystemBusyMutex; 133 | std::condition_variable mSystemBusyCondition; 134 | }; 135 | 136 | TEV_NAMESPACE_END 137 | -------------------------------------------------------------------------------- /src/MultiGraph.cpp: -------------------------------------------------------------------------------- 1 | // This file was adapted from the nanogui::Graph class, which was developed 2 | // by Wenzel Jakob and based on the NanoVG demo application 3 | // by Mikko Mononen. Modifications were developed by Thomas Müller . 4 | // This file is published under the BSD 3-Clause License within the LICENSE file. 5 | 6 | #include 7 | 8 | #include 9 | #include 10 | 11 | #include 12 | 13 | using namespace nanogui; 14 | using namespace std; 15 | 16 | TEV_NAMESPACE_BEGIN 17 | 18 | MultiGraph::MultiGraph(Widget *parent, const std::string &caption) 19 | : Widget(parent), mCaption(caption) { 20 | mBackgroundColor = Color(20, 128); 21 | mForegroundColor = Color(255, 192, 0, 128); 22 | mTextColor = Color(240, 192); 23 | } 24 | 25 | Vector2i MultiGraph::preferred_size(NVGcontext *) const { 26 | return Vector2i(180, 80); 27 | } 28 | 29 | void MultiGraph::draw(NVGcontext *ctx) { 30 | Widget::draw(ctx); 31 | 32 | NVGpaint bg = nvgBoxGradient(ctx, 33 | m_pos.x() + 1, m_pos.y() + 1 + 1.0f, m_size.x() - 2, m_size.y() - 2, 34 | 3, 4, Color(120, 32), Color(32, 32)); 35 | 36 | nvgBeginPath(ctx); 37 | nvgRoundedRect(ctx, m_pos.x() + 1, m_pos.y() + 1 + 1.0f, m_size.x() - 2, 38 | m_size.y() - 2, 3); 39 | 40 | nvgFillPaint(ctx, bg); 41 | 42 | nvgFill(ctx); 43 | 44 | if (mValues.size() >= 2) { 45 | array colors = {{ 46 | Color{255, 0, 0, 200}, 47 | Color{0, 255, 0, 200}, 48 | Color{0, 0, 255, 200}, 49 | }}; 50 | 51 | nvgSave(ctx); 52 | // Additive blending 53 | nvgGlobalCompositeBlendFunc(ctx, NVGblendFactor::NVG_SRC_ALPHA, NVGblendFactor::NVG_ONE); 54 | 55 | size_t nBins = mValues.size() / mNChannels; 56 | 57 | for (size_t i = 0; i < (size_t)mNChannels; i++) { 58 | nvgBeginPath(ctx); 59 | nvgMoveTo(ctx, m_pos.x(), m_pos.y() + m_size.y()); 60 | 61 | for (size_t j = 0; j < (size_t)nBins; j++) { 62 | float value = mValues[j + i * nBins]; 63 | float vx = m_pos.x() + 2 + j * (m_size.x() - 4) / (float)(nBins - 1); 64 | float vy = m_pos.y() + (1 - value) * m_size.y(); 65 | nvgLineTo(ctx, vx, vy); 66 | } 67 | 68 | auto color = i < colors.size() ? colors[i] : mForegroundColor; 69 | nvgLineTo(ctx, m_pos.x() + m_size.x(), m_pos.y() + m_size.y()); 70 | nvgFillColor(ctx, color); 71 | nvgFill(ctx); 72 | } 73 | 74 | nvgRestore(ctx); 75 | 76 | if (mZeroBin > 0) { 77 | nvgBeginPath(ctx); 78 | nvgRect(ctx, m_pos.x() + 1 + mZeroBin * (m_size.x() - 4) / (float)(nBins - 1), m_pos.y() + 15, 4, m_size.y() - 15); 79 | nvgFillColor(ctx, Color(0, 128)); 80 | nvgFill(ctx); 81 | nvgBeginPath(ctx); 82 | nvgRect(ctx, m_pos.x() + 2 + mZeroBin * (m_size.x() - 4) / (float)(nBins - 1), m_pos.y() + 15, 2, m_size.y() - 15); 83 | nvgFillColor(ctx, Color(200, 255)); 84 | nvgFill(ctx); 85 | } 86 | 87 | nvgFontFace(ctx, "sans"); 88 | 89 | nvgFontSize(ctx, 15.0f); 90 | nvgTextAlign(ctx, NVG_ALIGN_LEFT | NVG_ALIGN_TOP); 91 | nvgFillColor(ctx, mTextColor); 92 | drawTextWithShadow(ctx, m_pos.x() + 3, m_pos.y() + 1, tfm::format("%.3f", mMinimum)); 93 | 94 | nvgTextAlign(ctx, NVG_ALIGN_MIDDLE | NVG_ALIGN_TOP); 95 | nvgFillColor(ctx, mTextColor); 96 | string meanString = tfm::format("%.3f", mMean); 97 | float textWidth = nvgTextBounds(ctx, 0, 0, meanString.c_str(), nullptr, nullptr); 98 | drawTextWithShadow(ctx, m_pos.x() + m_size.x() / 2 - textWidth / 2, m_pos.y() + 1, meanString); 99 | 100 | nvgTextAlign(ctx, NVG_ALIGN_RIGHT | NVG_ALIGN_TOP); 101 | nvgFillColor(ctx, mTextColor); 102 | drawTextWithShadow(ctx, m_pos.x() + m_size.x() - 3, m_pos.y() + 1, tfm::format("%.3f", mMaximum)); 103 | 104 | if (!mCaption.empty()) { 105 | nvgFontSize(ctx, 14.0f); 106 | nvgTextAlign(ctx, NVG_ALIGN_LEFT | NVG_ALIGN_TOP); 107 | nvgFillColor(ctx, mTextColor); 108 | nvgText(ctx, m_pos.x() + 3, m_pos.y() + 1, mCaption.c_str(), NULL); 109 | } 110 | 111 | if (!mHeader.empty()) { 112 | nvgFontSize(ctx, 18.0f); 113 | nvgTextAlign(ctx, NVG_ALIGN_RIGHT | NVG_ALIGN_TOP); 114 | nvgFillColor(ctx, mTextColor); 115 | nvgText(ctx, m_pos.x() + m_size.x() - 3, m_pos.y() + 1, mHeader.c_str(), NULL); 116 | } 117 | 118 | if (!mFooter.empty()) { 119 | nvgFontSize(ctx, 15.0f); 120 | nvgTextAlign(ctx, NVG_ALIGN_RIGHT | NVG_ALIGN_BOTTOM); 121 | nvgFillColor(ctx, mTextColor); 122 | nvgText(ctx, m_pos.x() + m_size.x() - 3, m_pos.y() + m_size.y() - 1, mFooter.c_str(), NULL); 123 | } 124 | } 125 | 126 | nvgBeginPath(ctx); 127 | nvgRect(ctx, m_pos.x(), m_pos.y(), m_size.x(), m_size.y()); 128 | nvgRoundedRect(ctx, m_pos.x(), m_pos.y(), m_size.x(), m_size.y(), 2.5f); 129 | nvgPathWinding(ctx, NVG_HOLE); 130 | nvgFillColor(ctx, Color(0.23f, 1.0f)); 131 | nvgFill(ctx); 132 | 133 | nvgBeginPath(ctx); 134 | nvgRoundedRect(ctx, m_pos.x() + 0.5f, m_pos.y() + 0.5f, m_size.x() - 1, 135 | m_size.y() - 1, 2.5f); 136 | nvgStrokeColor(ctx, Color(0, 48)); 137 | nvgStroke(ctx); 138 | } 139 | 140 | TEV_NAMESPACE_END 141 | -------------------------------------------------------------------------------- /include/tev/ImageCanvas.h: -------------------------------------------------------------------------------- 1 | // This file was developed by Thomas Müller . 2 | // It is published under the BSD 3-Clause License within the LICENSE file. 3 | 4 | #pragma once 5 | 6 | #include 7 | #include 8 | #include 9 | 10 | #include 11 | 12 | #include 13 | 14 | TEV_NAMESPACE_BEGIN 15 | 16 | struct CanvasStatistics { 17 | float mean; 18 | float maximum; 19 | float minimum; 20 | std::vector histogram; 21 | int nChannels; 22 | int histogramZero; 23 | }; 24 | 25 | class ImageCanvas : public nanogui::Canvas { 26 | public: 27 | ImageCanvas(nanogui::Widget* parent, float pixelRatio); 28 | 29 | bool scroll_event(const nanogui::Vector2i& p, const nanogui::Vector2f& rel) override; 30 | 31 | void draw_contents() override; 32 | 33 | void draw(NVGcontext *ctx) override; 34 | 35 | void translate(const nanogui::Vector2f& amount); 36 | void scale(float amount, const nanogui::Vector2f& origin); 37 | float extractScale() const { 38 | float det = mTransform.m[0][0] * mTransform.m[1][1] - mTransform.m[0][1] * mTransform.m[1][0]; 39 | return std::sqrt(det); 40 | } 41 | 42 | void setExposure(float exposure) { 43 | mExposure = exposure; 44 | } 45 | 46 | void setOffset(float offset) { 47 | mOffset = offset; 48 | } 49 | 50 | void setGamma(float gamma) { 51 | mGamma = gamma; 52 | } 53 | 54 | float applyExposureAndOffset(float value) const; 55 | 56 | void setImage(std::shared_ptr image) { 57 | mImage = image; 58 | } 59 | 60 | void setReference(std::shared_ptr reference) { 61 | mReference = reference; 62 | } 63 | 64 | void setRequestedChannelGroup(const std::string& groupName) { 65 | mRequestedChannelGroup = groupName; 66 | } 67 | 68 | nanogui::Vector2i getImageCoords(const Image& image, nanogui::Vector2i mousePos); 69 | 70 | void getValuesAtNanoPos(nanogui::Vector2i nanoPos, std::vector& result, const std::vector& channels); 71 | std::vector getValuesAtNanoPos(nanogui::Vector2i nanoPos, const std::vector& channels) { 72 | std::vector result; 73 | getValuesAtNanoPos(nanoPos, result, channels); 74 | return result; 75 | } 76 | 77 | ETonemap tonemap() const { 78 | return mTonemap; 79 | } 80 | 81 | void setTonemap(ETonemap tonemap) { 82 | mTonemap = tonemap; 83 | } 84 | 85 | static nanogui::Vector3f applyTonemap(const nanogui::Vector3f& value, float gamma, ETonemap tonemap); 86 | nanogui::Vector3f applyTonemap(const nanogui::Vector3f& value) const { 87 | return applyTonemap(value, mGamma, mTonemap); 88 | } 89 | 90 | EMetric metric() const { 91 | return mMetric; 92 | } 93 | 94 | void setMetric(EMetric metric) { 95 | mMetric = metric; 96 | } 97 | 98 | static float applyMetric(float value, float reference, EMetric metric); 99 | float applyMetric(float value, float reference) const { 100 | return applyMetric(value, reference, mMetric); 101 | } 102 | 103 | auto backgroundColor() { 104 | return mShader->backgroundColor(); 105 | } 106 | 107 | void setBackgroundColor(const nanogui::Color& color) { 108 | mShader->setBackgroundColor(color); 109 | } 110 | 111 | void fitImageToScreen(const Image& image); 112 | void resetTransform(); 113 | 114 | void setClipToLdr(bool value) { 115 | mClipToLdr = value; 116 | } 117 | 118 | bool clipToLdr() const { 119 | return mClipToLdr; 120 | } 121 | 122 | std::vector getHdrImageData(bool divideAlpha, int priority) const; 123 | std::vector getLdrImageData(bool divideAlpha, int priority) const; 124 | 125 | void saveImage(const fs::path& filename) const; 126 | 127 | std::shared_ptr>> canvasStatistics(); 128 | 129 | void purgeCanvasStatistics(int imageId); 130 | 131 | private: 132 | static std::vector channelsFromImages( 133 | std::shared_ptr image, 134 | std::shared_ptr reference, 135 | const std::string& requestedChannelGroup, 136 | EMetric metric, 137 | int priority 138 | ); 139 | 140 | static Task> computeCanvasStatistics( 141 | std::shared_ptr image, 142 | std::shared_ptr reference, 143 | const std::string& requestedChannelGroup, 144 | EMetric metric, 145 | int priority 146 | ); 147 | 148 | void drawPixelValuesAsText(NVGcontext *ctx); 149 | void drawCoordinateSystem(NVGcontext *ctx); 150 | void drawEdgeShadows(NVGcontext *ctx); 151 | 152 | nanogui::Vector2f pixelOffset(const nanogui::Vector2i& size) const; 153 | 154 | // Assembles the transform from canonical space to 155 | // the [-1, 1] square for the current image. 156 | nanogui::Matrix3f transform(const Image* image); 157 | nanogui::Matrix3f textureToNanogui(const Image* image); 158 | nanogui::Matrix3f displayWindowToNanogui(const Image* image); 159 | 160 | float mPixelRatio = 1; 161 | float mExposure = 0; 162 | float mOffset = 0; 163 | float mGamma = 2.2f; 164 | 165 | bool mClipToLdr = false; 166 | 167 | std::shared_ptr mImage; 168 | std::shared_ptr mReference; 169 | 170 | std::string mRequestedChannelGroup = ""; 171 | 172 | nanogui::Matrix3f mTransform = nanogui::Matrix3f::scale(nanogui::Vector3f(1.0f)); 173 | 174 | std::unique_ptr mShader; 175 | 176 | ETonemap mTonemap = SRGB; 177 | EMetric mMetric = Error; 178 | 179 | std::map>>> mCanvasStatistics; 180 | std::map> mImageIdToCanvasStatisticsKey; 181 | }; 182 | 183 | TEV_NAMESPACE_END 184 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # tev — The EXR Viewer    ![](https://github.com/tom94/tev/workflows/CI/badge.svg) 2 | 3 | High dynamic range (HDR) image comparison tool for graphics people. __tev__ allows viewing images through various tonemapping operators and inspecting the values of individual pixels. To compare images, tev can rapidly flip between them and visualize various error metrics. 4 | 5 | __tev__ can display true HDR on Apple extended dynamic range (EDR) and 10-bit displays. 6 | 7 | The following file formats are supported: 8 | - __EXR__ (via [OpenEXR](https://github.com/wjakob/openexr)) 9 | - __HDR__, __PNG__, __JPEG__, BMP, GIF, PIC, PNM, PSD, TGA (via [stb_image](https://github.com/wjakob/nanovg/blob/master/src/stb_image.h)) 10 | - __PFM__ (compatible with [Netbpm](http://www.pauldebevec.com/Research/HDR/PFM/)) 11 | - __QOI__ (via [qoi](https://github.com/phoboslab/qoi). Shoutout to [Tiago Chaves](https://github.com/laurelkeys) for adding support!) 12 | - __DDS__ (via [DirectXTex](https://github.com/microsoft/DirectXTex); Windows only. Shoutout to [Craig Kolb](https://github.com/cek) for adding support!) 13 | - Supports BC1-BC7 compressed formats. 14 | 15 | ## Screenshot 16 | 17 | ![Screenshot](https://raw.githubusercontent.com/Tom94/tev/master/resources/screenshot.png) 18 | _A false-color comparison of two multi-layer OpenEXR images of a beach ball. Image courtesy of [openexr-images](https://github.com/openexr/openexr-images)._ 19 | 20 | ## Usage 21 | 22 | ### Graphical User Interface 23 | 24 | Images can be opened via a file dialog or by dragging them into __tev__. 25 | They can be reloaded, closed, or filtered at any time, so don't worry about opening more images than exactly needed. 26 | 27 | Select an image by left-clicking it, and optionally select a reference image to compare the current selection to by right-clicking. 28 | For convenience, the current selection can be moved with the Up/Down or the 1-9 keys. For a comprehensive list of keyboard shortcuts click the little "?" icon at the top (or press "h"). 29 | 30 | If the interface seems overwhelming, you can hover any controls to view an explanatory tooltip. 31 | 32 | ### Command Line 33 | 34 | __tev__ takes images as positional command-line arguments: 35 | ```sh 36 | $ tev foo.exr bar.exr 37 | ``` 38 | 39 | By default, all layers and channels are loaded, but individual layers or channels can also be specified. In the following example, the *depth* layer of *foo.exr* and the *r*, *g*, and *b* channels of *foo.exr* and *bar.exr* are loaded. 40 | ```sh 41 | $ tev :depth foo.exr :r,g,b foo.exr bar.exr 42 | ``` 43 | 44 | Other command-line arguments exist (e.g. for starting __tev__ with a pre-set exposure value). For a list of all arguments simply invoke 45 | ```sh 46 | $ tev -h 47 | ``` 48 | 49 | ### Over the Network 50 | 51 | __tev__ can also be controlled remotely over the network using a simple TCP-based protocol. 52 | 53 | The `--host` argument specifies the IP and port __tev__ is listening to. By default, __tev__ only accepts connections from localhost (`127.0.0.1:14158`), which is useful, for example, as frontend for a supported renderer like [pbrt version 4](https://github.com/mmp/pbrt-v4). 54 | 55 | The following operations exist: 56 | 57 | | Operation | Function 58 | | :--- | :---------- 59 | | `OpenImage` | Opens an image from a specified path on the machine __tev__ is running on. 60 | | `CreateImage` | Creates a blank image with a specified name, size, and set of channels. If an image with the specified name already exists, it is overwritten. 61 | | `UpdateImage` | Updates the pixels in a rectangular region. 62 | | `CloseImage` | Closes a specified image. 63 | | `ReloadImage` | Reloads an image from a specified path on the machine __tev__ is running on. 64 | 65 | __tev__'s network protocol is already implemented in the following languages: 66 | - [Python](src/python/ipc.py) by Tomáš Iser 67 | - [Rust](https://crates.io/crates/tev_client) by Karel Peeters 68 | 69 | 70 | If using these implementations is not an option, it's easy to write your own one. Each packet has the simple form 71 | ``` 72 | [uint32_t total_length_in_bytes][byte operation_type][byte[] operation_specific_payload] 73 | ``` 74 | where integers are encoded in little endian. 75 | 76 | There are helper functions in [Ipc.cpp](src/Ipc.cpp) (`IpcPacket::set*`) that show exactly how each packet has to be assembled. These functions do not rely on external dependencies, so it is recommended to copy and paste them into your project for interfacing with __tev__. 77 | 78 | 79 | ## Obtaining tev 80 | 81 | ### macOS / Windows 82 | 83 | Pre-built binaries for Windows (32-bit and 64-bit) and macOS (64-bit) are available on the [releases page](https://github.com/Tom94/tev/releases). 84 | 85 | On macOS, __tev__ can also be installed via homebrew: 86 | ```bash 87 | brew install --cask tev 88 | ``` 89 | 90 | ### Linux 91 | 92 | - Archlinux: available on the [Arch User Repository](https://aur.archlinux.org/packages/tev/) 93 | 94 | ## Building tev 95 | 96 | All that is required for building __tev__ is a C++20-compatible compiler. Begin by cloning this repository and all its submodules using the following command: 97 | ```sh 98 | $ git clone --recursive https://github.com/Tom94/tev 99 | ``` 100 | 101 | __tev__ uses [CMake](https://cmake.org/) as its build system. The following sections detail how it should be used on various operating systems. 102 | 103 | ### macOS / Linux 104 | 105 | On macOS and most Linux distributions [CMake](https://cmake.org/) can be obtained via a package manager ([Homebrew](https://brew.sh/) on macOS, apt on Ubuntu/Debian, etc.). Most Linux distributions additionally require _xorg_, _gl_, and _zlib_ development packages and _zenity_. On Ubuntu/Debian simply call 106 | ```sh 107 | $ apt-get install cmake xorg-dev libglu1-mesa-dev zlib1g-dev zenity 108 | ``` 109 | 110 | Once all dependencies are installed, create a new directory to contain build artifacts, enter it, and then invoke [CMake](https://cmake.org/) with the root __tev__ folder as argument as shown in the following example: 111 | ```sh 112 | $ mkdir build 113 | $ cd build 114 | $ cmake .. 115 | ``` 116 | 117 | Afterwards, __tev__ can be built and installed via 118 | ```sh 119 | $ make -j 120 | $ make install 121 | ``` 122 | 123 | ### Windows 124 | 125 | On Windows, install [CMake](https://cmake.org/download/), open the included GUI application, and point it to the root directory of __tev__. CMake will then generate [Visual Studio](https://www.visualstudio.com/) project files for compiling __tev__. Make sure you select at least Visual Studio 2019 or higher! 126 | 127 | ## License 128 | 129 | __tev__ is available under the BSD 3-clause license, which you can find in the `LICENSE.md` file. [TL;DR](https://tldrlegal.com/license/bsd-3-clause-license-(revised)) you can do almost whatever you want as long as you include the original copyright and license notice in any copy of the software and the source code. 130 | -------------------------------------------------------------------------------- /scripts/create-dmg/support/dmg-license.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | """ 3 | This script adds a license file to a DMG. Requires Xcode and a plain ascii text 4 | license file. 5 | Obviously only runs on a Mac. 6 | 7 | Copyright (C) 2011-2013 Jared Hobbs 8 | 9 | Permission is hereby granted, free of charge, to any person obtaining a copy 10 | of this software and associated documentation files (the "Software"), to deal 11 | in the Software without restriction, including without limitation the rights 12 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | copies of the Software, and to permit persons to whom the Software is 14 | furnished to do so, subject to the following conditions: 15 | 16 | The above copyright notice and this permission notice shall be included in 17 | all copies or substantial portions of the Software. 18 | 19 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 25 | THE SOFTWARE. 26 | """ 27 | import os 28 | import sys 29 | import tempfile 30 | import optparse 31 | 32 | 33 | class Path(str): 34 | def __enter__(self): 35 | return self 36 | 37 | def __exit__(self, type, value, traceback): 38 | os.unlink(self) 39 | 40 | 41 | def mktemp(dir=None, suffix=''): 42 | (fd, filename) = tempfile.mkstemp(dir=dir, suffix=suffix) 43 | os.close(fd) 44 | return Path(filename) 45 | 46 | 47 | def main(options, args): 48 | dmgFile, license = args 49 | with mktemp('.') as tmpFile: 50 | with open(tmpFile, 'w') as f: 51 | f.write("""data 'TMPL' (128, "LPic") { 52 | $"1344 6566 6175 6C74 204C 616E 6775 6167" 53 | $"6520 4944 4457 5244 0543 6F75 6E74 4F43" 54 | $"4E54 042A 2A2A 2A4C 5354 430B 7379 7320" 55 | $"6C61 6E67 2049 4444 5752 441E 6C6F 6361" 56 | $"6C20 7265 7320 4944 2028 6F66 6673 6574" 57 | $"2066 726F 6D20 3530 3030 4457 5244 1032" 58 | $"2D62 7974 6520 6C61 6E67 7561 6765 3F44" 59 | $"5752 4404 2A2A 2A2A 4C53 5445" 60 | }; 61 | 62 | data 'LPic' (5000) { 63 | $"0000 0002 0000 0000 0000 0000 0004 0000" 64 | }; 65 | 66 | data 'STR#' (5000, "English buttons") { 67 | $"0006 0D45 6E67 6C69 7368 2074 6573 7431" 68 | $"0541 6772 6565 0844 6973 6167 7265 6505" 69 | $"5072 696E 7407 5361 7665 2E2E 2E7A 4966" 70 | $"2079 6F75 2061 6772 6565 2077 6974 6820" 71 | $"7468 6520 7465 726D 7320 6F66 2074 6869" 72 | $"7320 6C69 6365 6E73 652C 2063 6C69 636B" 73 | $"2022 4167 7265 6522 2074 6F20 6163 6365" 74 | $"7373 2074 6865 2073 6F66 7477 6172 652E" 75 | $"2020 4966 2079 6F75 2064 6F20 6E6F 7420" 76 | $"6167 7265 652C 2070 7265 7373 2022 4469" 77 | $"7361 6772 6565 2E22" 78 | }; 79 | 80 | data 'STR#' (5002, "English") { 81 | $"0006 0745 6E67 6C69 7368 0541 6772 6565" 82 | $"0844 6973 6167 7265 6505 5072 696E 7407" 83 | $"5361 7665 2E2E 2E7B 4966 2079 6F75 2061" 84 | $"6772 6565 2077 6974 6820 7468 6520 7465" 85 | $"726D 7320 6F66 2074 6869 7320 6C69 6365" 86 | $"6E73 652C 2070 7265 7373 2022 4167 7265" 87 | $"6522 2074 6F20 696E 7374 616C 6C20 7468" 88 | $"6520 736F 6674 7761 7265 2E20 2049 6620" 89 | $"796F 7520 646F 206E 6F74 2061 6772 6565" 90 | $"2C20 7072 6573 7320 2244 6973 6167 7265" 91 | $"6522 2E" 92 | };\n\n""") 93 | with open(license, 'r') as l: 94 | kind = 'RTF ' if license.lower().endswith('.rtf') else 'TEXT' 95 | f.write('data \'%s\' (5000, "English") {\n' % kind) 96 | def escape(s): 97 | return s.strip().replace('\\', '\\\\').replace('"', '\\"') 98 | 99 | for line in l: 100 | if len(line) < 1000: 101 | f.write(' "' + escape(line) + '\\n"\n') 102 | else: 103 | for liner in line.split('.'): 104 | f.write(' "' + escape(liner) + '. \\n"\n') 105 | f.write('};\n\n') 106 | f.write("""data 'styl' (5000, "English") { 107 | $"0003 0000 0000 000C 0009 0014 0000 0000" 108 | $"0000 0000 0000 0000 0027 000C 0009 0014" 109 | $"0100 0000 0000 0000 0000 0000 002A 000C" 110 | $"0009 0014 0000 0000 0000 0000 0000" 111 | };\n""") 112 | os.system('hdiutil unflatten -quiet "%s"' % dmgFile) 113 | ret = os.system('%s -a %s -o "%s"' % 114 | (options.rez, tmpFile, dmgFile)) 115 | os.system('hdiutil flatten -quiet "%s"' % dmgFile) 116 | if options.compression is not None: 117 | os.system('cp %s %s.temp.dmg' % (dmgFile, dmgFile)) 118 | os.remove(dmgFile) 119 | if options.compression == "bz2": 120 | os.system('hdiutil convert %s.temp.dmg -format UDBZ -o %s' % 121 | (dmgFile, dmgFile)) 122 | elif options.compression == "gz": 123 | os.system('hdiutil convert %s.temp.dmg -format ' % dmgFile + 124 | 'UDZO -imagekey zlib-devel=9 -o %s' % dmgFile) 125 | os.remove('%s.temp.dmg' % dmgFile) 126 | if ret == 0: 127 | print "Successfully added license to '%s'" % dmgFile 128 | else: 129 | print "Failed to add license to '%s'" % dmgFile 130 | 131 | if __name__ == '__main__': 132 | parser = optparse.OptionParser() 133 | parser.set_usage("""%prog [OPTIONS] 134 | This program adds a software license agreement to a DMG file. 135 | It requires Xcode and either a plain ascii text 136 | or a with the RTF contents. 137 | 138 | See --help for more details.""") 139 | parser.add_option( 140 | '--rez', 141 | '-r', 142 | action='store', 143 | default='/Applications/Xcode.app/Contents/Developer/Tools/Rez', 144 | help='The path to the Rez tool. Defaults to %default' 145 | ) 146 | parser.add_option( 147 | '--compression', 148 | '-c', 149 | action='store', 150 | choices=['bz2', 'gz'], 151 | default=None, 152 | help='Optionally compress dmg using specified compression type. ' 153 | 'Choices are bz2 and gz.' 154 | ) 155 | options, args = parser.parse_args() 156 | cond = len(args) != 2 157 | if not os.path.exists(options.rez): 158 | print 'Failed to find Rez at "%s"!\n' % options.rez 159 | cond = True 160 | if cond: 161 | parser.print_usage() 162 | sys.exit(1) 163 | main(options, args) 164 | -------------------------------------------------------------------------------- /src/Common.cpp: -------------------------------------------------------------------------------- 1 | // This file was developed by Thomas Müller . 2 | // It is published under the BSD 3-Clause License within the LICENSE file. 3 | 4 | #include 5 | 6 | #include 7 | 8 | #include 9 | 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | 16 | #ifdef _WIN32 17 | # include 18 | #else 19 | # include 20 | # include 21 | # include 22 | #endif 23 | 24 | using namespace nanogui; 25 | using namespace std; 26 | 27 | TEV_NAMESPACE_BEGIN 28 | 29 | u8string toU8string(const string& str) { 30 | u8string temp; 31 | utf8::replace_invalid(begin(str), end(str), back_inserter(temp)); 32 | return temp; 33 | } 34 | 35 | string fromU8string(const u8string& str) { 36 | string temp; 37 | utf8::replace_invalid(begin(str), end(str), back_inserter(temp)); 38 | return temp; 39 | } 40 | 41 | string ensureUtf8(const string& str) { 42 | string temp; 43 | utf8::replace_invalid(begin(str), end(str), back_inserter(temp)); 44 | return temp; 45 | } 46 | 47 | string utf16to8(const wstring& utf16) { 48 | string utf8; 49 | utf8::utf16to8(begin(utf16), end(utf16), back_inserter(utf8)); 50 | return utf8; 51 | } 52 | 53 | fs::path toPath(const string& utf8) { 54 | // tev's strings are always utf8 encoded, however fs::path does 55 | // not know this. Therefore: convert the string to a std::u8string 56 | // and pass _that_ string to the fs::path constructor, which will 57 | // then take care of converting the utf8 string to the native 58 | // file name encoding. 59 | return toU8string(utf8); 60 | } 61 | 62 | string toString(const fs::path& path) { 63 | // Conversely to `toPath`, ensure that the returned string is 64 | // utf8 encoded by requesting a std::u8string from the path 65 | // object and then convert _that_ string to a regular char string. 66 | return fromU8string(path.u8string()); 67 | } 68 | 69 | vector split(string text, const string& delim) { 70 | vector result; 71 | size_t begin = 0; 72 | while (true) { 73 | size_t end = text.find_first_of(delim, begin); 74 | if (end == string::npos) { 75 | result.emplace_back(text.substr(begin)); 76 | return result; 77 | } else { 78 | result.emplace_back(text.substr(begin, end - begin)); 79 | begin = end + 1; 80 | } 81 | } 82 | 83 | return result; 84 | } 85 | 86 | string toLower(string str) { 87 | transform(begin(str), end(str), begin(str), [](unsigned char c) { return (char)tolower(c); }); 88 | return str; 89 | } 90 | 91 | string toUpper(string str) { 92 | transform(begin(str), end(str), begin(str), [](unsigned char c) { return (char)toupper(c); }); 93 | return str; 94 | } 95 | 96 | bool matchesFuzzy(string text, string filter, size_t* matchedPartId) { 97 | if (matchedPartId) { 98 | // Default value of 0. Is actually returned when the filter 99 | // is empty or when there is no match. 100 | *matchedPartId = 0; 101 | } 102 | 103 | if (filter.empty()) { 104 | return true; 105 | } 106 | 107 | // Perform matching on lowercase strings 108 | text = toLower(text); 109 | filter = toLower(filter); 110 | 111 | auto words = split(filter, ", "); 112 | // We don't want people entering multiple spaces in a row to match everything. 113 | words.erase(remove(begin(words), end(words), ""), end(words)); 114 | 115 | if (words.empty()) { 116 | return true; 117 | } 118 | 119 | // Match every word of the filter separately. 120 | for (size_t i = 0; i < words.size(); ++i) { 121 | if (text.find(words[i]) != string::npos) { 122 | if (matchedPartId) { 123 | *matchedPartId = i; 124 | } 125 | return true; 126 | } 127 | } 128 | 129 | return false; 130 | } 131 | 132 | bool matchesRegex(string text, string filter) { 133 | if (filter.empty()) { 134 | return true; 135 | } 136 | 137 | try { 138 | regex searchRegex{filter, std::regex_constants::ECMAScript | std::regex_constants::icase}; 139 | return regex_search(text, searchRegex); 140 | } catch (const regex_error&) { 141 | return false; 142 | } 143 | } 144 | 145 | void drawTextWithShadow(NVGcontext* ctx, float x, float y, string text, float shadowAlpha) { 146 | nvgSave(ctx); 147 | nvgFontBlur(ctx, 2); 148 | nvgFillColor(ctx, Color{0.0f, shadowAlpha}); 149 | nvgText(ctx, x + 1, y + 1, text.c_str(), NULL); 150 | nvgRestore(ctx); 151 | nvgText(ctx, x, y, text.c_str(), NULL); 152 | } 153 | 154 | int lastError() { 155 | #ifdef _WIN32 156 | return GetLastError(); 157 | #else 158 | return errno; 159 | #endif 160 | } 161 | 162 | int lastSocketError() { 163 | #ifdef _WIN32 164 | return WSAGetLastError(); 165 | #else 166 | return errno; 167 | #endif 168 | } 169 | 170 | string errorString(int errorId) { 171 | #ifdef _WIN32 172 | char* s = NULL; 173 | FormatMessage( 174 | FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, 175 | NULL, errorId, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPSTR)&s, 0, NULL); 176 | 177 | string result = tfm::format("%s (%d)", s, errorId); 178 | LocalFree(s); 179 | 180 | return result; 181 | #else 182 | return tfm::format("%s (%d)", strerror(errorId), errno); 183 | #endif 184 | } 185 | 186 | fs::path homeDirectory() { 187 | #ifdef _WIN32 188 | char path[MAX_PATH]; 189 | if (SHGetFolderPath(NULL, CSIDL_PROFILE, NULL, 0, path) != S_OK) { 190 | return ""; 191 | } 192 | 193 | return path; 194 | #else 195 | struct passwd* pw = getpwuid(getuid()); 196 | return pw->pw_dir; 197 | #endif 198 | } 199 | 200 | void toggleConsole() { 201 | #ifdef _WIN32 202 | HWND console = GetConsoleWindow(); 203 | DWORD consoleProcessId; 204 | GetWindowThreadProcessId(console, &consoleProcessId); 205 | 206 | // Only toggle the console if it was actually spawned by tev. If we are 207 | // running in a foreign console, then we should leave it be. 208 | if (GetCurrentProcessId() == consoleProcessId) { 209 | ShowWindow(console, IsWindowVisible(console) ? SW_HIDE : SW_SHOW); 210 | } 211 | #endif 212 | } 213 | 214 | static std::atomic sShuttingDown{false}; 215 | 216 | bool shuttingDown() { 217 | return sShuttingDown; 218 | } 219 | 220 | void setShuttingDown() { 221 | sShuttingDown = true; 222 | } 223 | 224 | ETonemap toTonemap(string name) { 225 | // Perform matching on uppercase strings 226 | name = toUpper(name); 227 | if (name == "SRGB") { 228 | return SRGB; 229 | } else if (name == "GAMMA") { 230 | return Gamma; 231 | } else if (name == "FALSECOLOR" || name == "FC") { 232 | return FalseColor; 233 | } else if (name == "POSITIVENEGATIVE" || name == "POSNEG" || name == "PN" ||name == "+-") { 234 | return PositiveNegative; 235 | } else { 236 | return SRGB; 237 | } 238 | } 239 | 240 | EMetric toMetric(string name) { 241 | // Perform matching on uppercase strings 242 | name = toUpper(name); 243 | if (name == "E") { 244 | return Error; 245 | } else if (name == "AE") { 246 | return AbsoluteError; 247 | } else if (name == "SE") { 248 | return SquaredError; 249 | } else if (name == "RAE") { 250 | return RelativeAbsoluteError; 251 | } else if (name == "RSE") { 252 | return RelativeSquaredError; 253 | } else { 254 | return Error; 255 | } 256 | } 257 | 258 | TEV_NAMESPACE_END 259 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/python/ipc.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # This file was developed by Tomáš Iser & Thomas Müller . 4 | # It is published under the BSD 3-Clause License within the LICENSE file. 5 | 6 | """ 7 | Interfaces with tev's IPC protocol to remote control tev with Python. 8 | """ 9 | 10 | import socket 11 | import struct 12 | import numpy as np 13 | 14 | class TevIpc: 15 | def __init__(self, hostname = "localhost", port = 14158): 16 | self._hostname = hostname 17 | self._port = port 18 | self._socket = None 19 | 20 | def __enter__(self): 21 | if self._socket is not None: 22 | raise Exception("Communication already started") 23 | self._socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # SOCK_STREAM means a TCP socket 24 | self._socket.__enter__() 25 | self._socket.connect((self._hostname, self._port)) 26 | return self 27 | 28 | def __exit__(self, exc_type, exc_val, exc_tb): 29 | if self._socket is None: 30 | raise Exception("Communication was not started") 31 | self._socket.__exit__(exc_type, exc_val, exc_tb) 32 | 33 | """ 34 | Opens an image from a specified path from the disk of the machine tev is running on. 35 | """ 36 | def open_image(self, path: str, channel_selector: str = "", grab_focus = True): 37 | if self._socket is None: 38 | raise Exception("Communication was not started") 39 | 40 | data_bytes = bytearray() 41 | data_bytes.extend(struct.pack(". 2 | // It is published under the BSD 3-Clause License within the LICENSE file. 3 | 4 | #pragma once 5 | 6 | #include 7 | 8 | #ifdef __clang__ 9 | #include 10 | #define COROUTINE_NAMESPACE std::experimental 11 | #else 12 | #include 13 | #define COROUTINE_NAMESPACE std 14 | #endif 15 | 16 | #include 17 | #include 18 | #include 19 | #include 20 | 21 | TEV_NAMESPACE_BEGIN 22 | 23 | class Latch { 24 | public: 25 | Latch(int val) : mCounter{val} {} 26 | bool countDown() noexcept { 27 | int val = --mCounter; 28 | if (val < 0) { 29 | tlog::warning() << "Latch should never count below zero."; 30 | } 31 | 32 | return val <= 0; 33 | } 34 | 35 | int value() const { 36 | return mCounter; 37 | } 38 | 39 | private: 40 | std::atomic mCounter; 41 | }; 42 | 43 | template 44 | void waitAll(std::vector& futures) { 45 | for (auto& f : futures) { 46 | f.get(); 47 | } 48 | } 49 | 50 | struct DetachedTask { 51 | struct promise_type { 52 | DetachedTask get_return_object() noexcept { 53 | return {}; 54 | } 55 | 56 | COROUTINE_NAMESPACE::suspend_never initial_suspend() const noexcept { return {}; } 57 | COROUTINE_NAMESPACE::suspend_never final_suspend() const noexcept { return {}; } 58 | 59 | void return_void() {} 60 | void unhandled_exception() { 61 | try { 62 | std::rethrow_exception(std::current_exception()); 63 | } catch (const std::exception& e) { 64 | tlog::error() << "Unhandled exception in DetachedTask: " << e.what(); 65 | std::terminate(); 66 | } 67 | } 68 | }; 69 | }; 70 | 71 | template 72 | DetachedTask invokeTaskDetached(F&& executor, Args&&... args) { 73 | auto exec = std::move(executor); 74 | co_await exec(args...); 75 | } 76 | 77 | template 78 | class TaskPromiseBase { 79 | public: 80 | void return_value(data_t&& value) noexcept { mPromise.set_value(std::move(value)); } 81 | void return_value(const data_t& value) noexcept { mPromise.set_value(value); } 82 | 83 | protected: 84 | std::promise mPromise; 85 | }; 86 | 87 | template <> 88 | class TaskPromiseBase { 89 | public: 90 | void return_void() noexcept { mPromise.set_value(); } 91 | 92 | protected: 93 | std::promise mPromise; 94 | }; 95 | 96 | struct TaskSharedState { 97 | COROUTINE_NAMESPACE::coroutine_handle<> continuation = nullptr; 98 | Latch latch{2}; 99 | }; 100 | 101 | template 102 | class TaskPromise : public TaskPromiseBase { 103 | public: 104 | future_t get_return_object() noexcept { 105 | return {COROUTINE_NAMESPACE::coroutine_handle>::from_promise(*this), this->mPromise.get_future(), mState}; 106 | } 107 | 108 | COROUTINE_NAMESPACE::suspend_never initial_suspend() const noexcept { return {}; } 109 | 110 | void unhandled_exception() { 111 | this->mPromise.set_exception(std::current_exception()); 112 | } 113 | 114 | // The coroutine is about to complete (via co_return, reaching the end of the coroutine body, 115 | // or an uncaught exception). The awaiter returned here defines what happens next 116 | auto final_suspend() noexcept { 117 | class Awaiter { 118 | public: 119 | Awaiter(COROUTINE_NAMESPACE::coroutine_handle<> continuation) : mContinuation{continuation} {} 120 | 121 | bool await_ready() noexcept { return !mContinuation; } 122 | void await_resume() noexcept {} 123 | 124 | // Returning the continuation has the effect of continuing execution where the parent co_await'ed us. 125 | // It's the parent's job to call destroy on this coroutine's handle. 126 | COROUTINE_NAMESPACE::coroutine_handle<> await_suspend(COROUTINE_NAMESPACE::coroutine_handle>) noexcept { 127 | return mContinuation; 128 | } 129 | 130 | private: 131 | COROUTINE_NAMESPACE::coroutine_handle<> mContinuation; 132 | }; 133 | 134 | bool isLast = mState->latch.countDown(); 135 | return Awaiter{isLast ? mState->continuation : nullptr}; 136 | } 137 | 138 | private: 139 | std::shared_ptr mState = std::make_shared(); 140 | }; 141 | 142 | template 143 | class Task { 144 | public: 145 | using promise_type = TaskPromise, T>; 146 | 147 | Task(COROUTINE_NAMESPACE::coroutine_handle handle, std::future&& future, const std::shared_ptr& state) 148 | : mHandle{handle}, mFuture{std::move(future)}, mState{state} {} 149 | 150 | // No copying allowed! 151 | Task(const Task& other) = delete; 152 | Task& operator=(const Task& other) = delete; 153 | 154 | Task& operator=(Task&& other) { 155 | mHandle = other.mHandle; 156 | other.mHandle = nullptr; 157 | mFuture = std::move(other.mFuture); 158 | mState = std::move(other.mState); 159 | return *this; 160 | } 161 | Task(Task&& other) { 162 | *this = std::move(other); 163 | } 164 | 165 | ~Task() { 166 | // Make sure the coroutine finished and is cleaned up 167 | if (mHandle) { 168 | tlog::warning() << "~Task was invoked before completion."; 169 | } 170 | } 171 | 172 | bool await_ready() noexcept { 173 | // If the latch has already been passed by final_suspend() 174 | // above, the task has already completed and we can skip 175 | // suspension of the coroutine. 176 | if (mState->latch.value() <= 1) { 177 | mState->latch.countDown(); 178 | mHandle = nullptr; 179 | return true; 180 | } 181 | 182 | return false; 183 | } 184 | 185 | T await_resume() { 186 | // If (and only if) a previously suspended coroutine is resumed here, 187 | // this task's own coroutine handle has not been cleaned up (for 188 | // implementation reasons) and needs to be destroyed here. 189 | // (See the behavior of final_suspend() above.) 190 | if (mHandle) { 191 | mHandle.destroy(); 192 | mHandle = nullptr; 193 | } 194 | 195 | // Note: if there occurred an uncaught exception while executing 196 | // this task, it'll get rethrown in the following call. 197 | return mFuture.get(); 198 | } 199 | 200 | T get() { 201 | if (!mHandle) { 202 | tlog::error() << "Cannot get()/co_await a task multiple times."; 203 | } 204 | 205 | mHandle = nullptr; 206 | return await_resume(); 207 | } 208 | 209 | bool await_suspend(COROUTINE_NAMESPACE::coroutine_handle<> coroutine) noexcept { 210 | if (!mHandle) { 211 | tlog::error() << "Cannot co_await/get() a task multiple times."; 212 | std::terminate(); 213 | } 214 | 215 | // If the task is still running (checked by arriving at the latch), 216 | // mark this coroutine as the task's continuation and suspend it until then. 217 | // The task's coroutine `mHandle` remains valid if suspended, implying that it 218 | // needs to be manually cleaned up on resumption of this coroutine (see await_resume()). 219 | mState->continuation = coroutine; 220 | bool shallSuspend = !mState->latch.countDown(); 221 | if (!shallSuspend) { 222 | mHandle = nullptr; 223 | } 224 | return shallSuspend; 225 | } 226 | 227 | private: 228 | COROUTINE_NAMESPACE::coroutine_handle mHandle; 229 | std::future mFuture; 230 | std::shared_ptr mState; 231 | }; 232 | 233 | TEV_NAMESPACE_END 234 | -------------------------------------------------------------------------------- /src/ImageButton.cpp: -------------------------------------------------------------------------------- 1 | // This file was developed by Thomas Müller . 2 | // It is published under the BSD 3-Clause License within the LICENSE file. 3 | 4 | #include 5 | 6 | #include 7 | 8 | #include 9 | 10 | using namespace nanogui; 11 | using namespace std; 12 | 13 | TEV_NAMESPACE_BEGIN 14 | 15 | ImageButton::ImageButton(Widget *parent, const string &caption, bool canBeReference) 16 | : Widget{parent}, mCaption{caption}, mCanBeReference{canBeReference} { 17 | } 18 | 19 | Vector2i ImageButton::preferred_size(NVGcontext *ctx) const { 20 | nvgFontSize(ctx, m_font_size); 21 | nvgFontFace(ctx, "sans-bold"); 22 | string idString = to_string(mId); 23 | float idSize = nvgTextBounds(ctx, 0, 0, idString.c_str(), nullptr, nullptr); 24 | 25 | nvgFontSize(ctx, m_font_size); 26 | nvgFontFace(ctx, "sans"); 27 | float tw = nvgTextBounds(ctx, 0, 0, mCaption.c_str(), nullptr, nullptr); 28 | return Vector2i(static_cast(tw + idSize) + 15, m_font_size + 6); 29 | } 30 | 31 | bool ImageButton::mouse_button_event(const Vector2i &p, int button, bool down, int modifiers) { 32 | if (Widget::mouse_button_event(p, button, down, modifiers)) { 33 | return true; 34 | } 35 | 36 | if (!m_enabled || !down) { 37 | return false; 38 | } 39 | 40 | if (mCanBeReference && (button == GLFW_MOUSE_BUTTON_2 || (button == GLFW_MOUSE_BUTTON_1 && modifiers & GLFW_MOD_SHIFT))) { 41 | // If we already were the reference, then let's disable using us a reference. 42 | mIsReference = !mIsReference; 43 | 44 | // If we newly became the reference, then we need to disable the existing reference 45 | // if it exists. 46 | if (mIsReference) { 47 | for (auto widget : parent()->children()) { 48 | ImageButton* b = dynamic_cast(widget); 49 | if (b && b != this) { 50 | b->mIsReference = false; 51 | } 52 | } 53 | } 54 | 55 | // Invoke the callback in any case, such that the surrounding code can 56 | // react to new references or a loss of a reference image. 57 | if (mReferenceCallback) { 58 | mReferenceCallback(mIsReference); 59 | } 60 | return true; 61 | } else if (button == GLFW_MOUSE_BUTTON_1) { 62 | if (!mIsSelected) { 63 | // Unselect the other, currently selected image. 64 | for (auto widget : parent()->children()) { 65 | ImageButton* b = dynamic_cast(widget); 66 | if (b && b != this) { 67 | b->mIsSelected = false; 68 | } 69 | } 70 | 71 | mIsSelected = true; 72 | if (mSelectedCallback) { 73 | mSelectedCallback(); 74 | } 75 | } 76 | return true; 77 | } 78 | 79 | return false; 80 | } 81 | 82 | void ImageButton::draw(NVGcontext *ctx) { 83 | Widget::draw(ctx); 84 | 85 | if (mIsReference) { 86 | nvgBeginPath(ctx); 87 | nvgRect(ctx, m_pos.x(), m_pos.y(), m_size.x(), m_size.y()); 88 | nvgFillColor(ctx, Color(0.7f, 0.4f, 0.4f, 1.0f)); 89 | nvgFill(ctx); 90 | } 91 | 92 | // Fill the button with color. 93 | if (mIsSelected || m_mouse_focus) { 94 | nvgBeginPath(ctx); 95 | 96 | if (mIsReference) { 97 | nvgRect(ctx, m_pos.x() + 2, m_pos.y() + 2, m_size.x() - 4, m_size.y() - 4); 98 | } else { 99 | nvgRect(ctx, m_pos.x(), m_pos.y(), m_size.x(), m_size.y()); 100 | } 101 | 102 | nvgFillColor(ctx, mIsSelected ? Color(0.35f, 0.35f, 0.8f, 1.0f) : Color(1.0f, 0.1f)); 103 | nvgFill(ctx); 104 | } 105 | 106 | 107 | string idString = to_string(mId); 108 | 109 | if (m_size.x() == preferred_size(ctx).x()) { 110 | mCutoff = 0; 111 | } else if (m_size != mSizeForWhichCutoffWasComputed) { 112 | mCutoff = 0; 113 | 114 | nvgFontSize(ctx, m_font_size + 2); 115 | nvgFontFace(ctx, "sans-bold"); 116 | float idSize = nvgTextBounds(ctx, 0, 0, idString.c_str(), nullptr, nullptr); 117 | 118 | nvgFontSize(ctx, m_font_size); 119 | while (mCutoff < mCaption.size() && nvgTextBounds(ctx, 0, 0, mCaption.substr(mCutoff).c_str(), nullptr, nullptr) > m_size.x() - 25 - idSize) { 120 | mCutoff += codePointLength(mCaption[mCutoff]);; 121 | } 122 | 123 | mSizeForWhichCutoffWasComputed = m_size; 124 | } 125 | 126 | // Image name 127 | string caption = mCaption.substr(mCutoff); 128 | 129 | vector pieces; 130 | if (mHighlightBegin <= mCutoff) { 131 | if (mHighlightEnd <= mCutoff) { 132 | pieces.emplace_back(caption); 133 | } else { 134 | size_t offset = mHighlightEnd - mCutoff; 135 | pieces.emplace_back(caption.substr(offset)); 136 | pieces.emplace_back(caption.substr(0, offset)); 137 | } 138 | } else { 139 | size_t beginOffset = mHighlightBegin - mCutoff; 140 | size_t endOffset = mHighlightEnd - mCutoff; 141 | pieces.emplace_back(caption.substr(endOffset)); 142 | pieces.emplace_back(caption.substr(beginOffset, endOffset - beginOffset)); 143 | pieces.emplace_back(caption.substr(0, beginOffset)); 144 | } 145 | 146 | if (mCutoff > 0 && mCutoff < mCaption.size()) { 147 | pieces.back() = "…"s + pieces.back(); 148 | } 149 | 150 | Vector2f center = Vector2f{m_pos} + Vector2f{m_size} * 0.5f; 151 | Vector2f bottomRight = Vector2f{m_pos} + Vector2f{m_size}; 152 | Vector2f textPos(bottomRight.x() - 5, center.y() + 0.5f * (m_font_size + 1)); 153 | NVGcolor regularTextColor = mCanBeReference ? Color(150, 255) : Color(190, 255); 154 | NVGcolor hightlightedTextColor = Color(190, 255); 155 | if (mIsSelected || mIsReference || m_mouse_focus) { 156 | regularTextColor = hightlightedTextColor = Color(255, 255); 157 | } 158 | 159 | nvgFontSize(ctx, m_font_size); 160 | nvgTextAlign(ctx, NVG_ALIGN_RIGHT | NVG_ALIGN_BOTTOM); 161 | 162 | for (size_t i = 0; i < pieces.size(); ++i) { 163 | nvgFontFace(ctx, i == 1 ? "sans-bold" : "sans"); 164 | nvgFillColor(ctx, i == 1 ? hightlightedTextColor : regularTextColor); 165 | nvgText(ctx, textPos.x(), textPos.y(), pieces[i].c_str(), nullptr); 166 | textPos.x() -= nvgTextBounds(ctx, 0, 0, pieces[i].c_str(), nullptr, nullptr); 167 | } 168 | 169 | // Image number 170 | NVGcolor idColor = Color(200, 255); 171 | if (mIsSelected || mIsReference || m_mouse_focus) { 172 | idColor = Color(255, 255); 173 | } 174 | 175 | nvgFontSize(ctx, m_font_size + 2); 176 | nvgFontFace(ctx, "sans-bold"); 177 | nvgTextAlign(ctx, NVG_ALIGN_LEFT | NVG_ALIGN_BOTTOM); 178 | nvgFillColor(ctx, idColor); 179 | nvgText(ctx, m_pos.x() + 5, textPos.y(), idString.c_str(), nullptr); 180 | } 181 | 182 | void ImageButton::setHighlightRange(size_t begin, size_t end) { 183 | size_t beginIndex = begin; 184 | if (end > mCaption.size()) { 185 | throw std::invalid_argument{tfm::format( 186 | "end (%d) must not be larger than mCaption.size() (%d)", 187 | end, mCaption.size() 188 | )}; 189 | } 190 | 191 | mHighlightBegin = beginIndex; 192 | mHighlightEnd = max(mCaption.size() - end, beginIndex); 193 | 194 | if (mHighlightBegin == mHighlightEnd || mCaption.empty()) { 195 | return; 196 | } 197 | 198 | // Extend beginning and ending of highlighted region to entire word/number 199 | if (isalnum(mCaption[mHighlightBegin])) { 200 | while (mHighlightBegin > 0 && isalnum(mCaption[mHighlightBegin - 1])) { 201 | --mHighlightBegin; 202 | } 203 | } 204 | 205 | if (isalnum(mCaption[mHighlightEnd - 1])) { 206 | while (mHighlightEnd < mCaption.size() && isalnum(mCaption[mHighlightEnd])) { 207 | ++mHighlightEnd; 208 | } 209 | } 210 | } 211 | 212 | TEV_NAMESPACE_END 213 | -------------------------------------------------------------------------------- /scripts/create-dmg/create-dmg: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | 3 | # Create a read-only disk image of the contents of a folder 4 | 5 | set -e; 6 | 7 | function pure_version() { 8 | echo '1.0.0.2' 9 | } 10 | 11 | function version() { 12 | echo "create-dmg $(pure_version)" 13 | } 14 | 15 | function usage() { 16 | version 17 | echo "Creates a fancy DMG file." 18 | echo "Usage: $(basename $0) options... image.dmg source_folder" 19 | echo "All contents of source_folder will be copied into the disk image." 20 | echo "Options:" 21 | echo " --volname name" 22 | echo " set volume name (displayed in the Finder sidebar and window title)" 23 | echo " --volicon icon.icns" 24 | echo " set volume icon" 25 | echo " --background pic.png" 26 | echo " set folder background image (provide png, gif, jpg)" 27 | echo " --window-pos x y" 28 | echo " set position the folder window" 29 | echo " --window-size width height" 30 | echo " set size of the folder window" 31 | echo " --text-size text_size" 32 | echo " set window text size (10-16)" 33 | echo " --icon-size icon_size" 34 | echo " set window icons size (up to 128)" 35 | echo " --icon file_name x y" 36 | echo " set position of the file's icon" 37 | echo " --hide-extension file_name" 38 | echo " hide the extension of file" 39 | echo " --custom-icon file_name custom_icon_or_sample_file x y" 40 | echo " set position and custom icon" 41 | echo " --app-drop-link x y" 42 | echo " make a drop link to Applications, at location x,y" 43 | echo " --eula eula_file" 44 | echo " attach a license file to the dmg" 45 | echo " --no-internet-enable" 46 | echo " disable automatic mount©" 47 | echo " --version show tool version number" 48 | echo " -h, --help display this help" 49 | exit 0 50 | } 51 | 52 | WINX=10 53 | WINY=60 54 | WINW=500 55 | WINH=350 56 | ICON_SIZE=128 57 | TEXT_SIZE=16 58 | 59 | while test "${1:0:1}" = "-"; do 60 | case $1 in 61 | --volname) 62 | VOLUME_NAME="$2" 63 | shift; shift;; 64 | --volicon) 65 | VOLUME_ICON_FILE="$2" 66 | shift; shift;; 67 | --background) 68 | BACKGROUND_FILE="$2" 69 | BACKGROUND_FILE_NAME="$(basename $BACKGROUND_FILE)" 70 | BACKGROUND_CLAUSE="set background picture of opts to file \".background:$BACKGROUND_FILE_NAME\"" 71 | REPOSITION_HIDDEN_FILES_CLAUSE="set position of every item to {theBottomRightX + 100, 100}" 72 | shift; shift;; 73 | --icon-size) 74 | ICON_SIZE="$2" 75 | shift; shift;; 76 | --text-size) 77 | TEXT_SIZE="$2" 78 | shift; shift;; 79 | --window-pos) 80 | WINX=$2; WINY=$3 81 | shift; shift; shift;; 82 | --window-size) 83 | WINW=$2; WINH=$3 84 | shift; shift; shift;; 85 | --icon) 86 | POSITION_CLAUSE="${POSITION_CLAUSE}set position of item \"$2\" to {$3, $4} 87 | " 88 | shift; shift; shift; shift;; 89 | --hide-extension) 90 | HIDING_CLAUSE="${HIDING_CLAUSE}set the extension hidden of item \"$2\" to true 91 | " 92 | shift; shift;; 93 | --custom-icon) 94 | shift; shift; shift; shift; shift;; 95 | -h | --help) 96 | usage;; 97 | --version) 98 | version; exit 0;; 99 | --pure-version) 100 | pure_version; exit 0;; 101 | --app-drop-link) 102 | APPLICATION_LINK=$2 103 | APPLICATION_CLAUSE="set position of item \"Applications\" to {$2, $3} 104 | " 105 | shift; shift; shift;; 106 | --eula) 107 | EULA_RSRC=$2 108 | shift; shift;; 109 | --no-internet-enable) 110 | NOINTERNET=1 111 | shift;; 112 | -*) 113 | echo "Unknown option $1. Run with --help for help." 114 | exit 1;; 115 | esac 116 | done 117 | 118 | test -z "$2" && { 119 | echo "Not enough arguments. Invoke with --help for help." 120 | exit 1 121 | } 122 | 123 | SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" 124 | DMG_PATH="$1" 125 | DMG_DIRNAME="$(dirname "$DMG_PATH")" 126 | DMG_DIR="$(cd "$DMG_DIRNAME" > /dev/null; pwd)" 127 | DMG_NAME="$(basename "$DMG_PATH")" 128 | DMG_TEMP_NAME="$DMG_DIR/rw.${DMG_NAME}" 129 | SRC_FOLDER="$(cd "$2" > /dev/null; pwd)" 130 | test -z "$VOLUME_NAME" && VOLUME_NAME="$(basename "$DMG_PATH" .dmg)" 131 | 132 | AUX_PATH="$SCRIPT_DIR/support" 133 | 134 | test -d "$AUX_PATH" || { 135 | echo "Cannot find support directory: $AUX_PATH" 136 | exit 1 137 | } 138 | 139 | if [ -f "$SRC_FOLDER/.DS_Store" ]; then 140 | echo "Deleting any .DS_Store in source folder" 141 | rm "$SRC_FOLDER/.DS_Store" 142 | fi 143 | 144 | # Create the image 145 | echo "Creating disk image..." 146 | test -f "${DMG_TEMP_NAME}" && rm -f "${DMG_TEMP_NAME}" 147 | ACTUAL_SIZE=`du -sm "$SRC_FOLDER" | sed -e 's/ .*//g'` 148 | DISK_IMAGE_SIZE=$(expr $ACTUAL_SIZE + 20) 149 | hdiutil create -srcfolder "$SRC_FOLDER" -volname "${VOLUME_NAME}" -fs HFS+ -fsargs "-c c=64,a=16,e=16" -format UDRW -size ${DISK_IMAGE_SIZE}m "${DMG_TEMP_NAME}" 150 | 151 | # mount it 152 | echo "Mounting disk image..." 153 | MOUNT_DIR="/Volumes/${VOLUME_NAME}" 154 | 155 | # try unmount dmg if it was mounted previously (e.g. developer mounted dmg, installed app and forgot to unmount it) 156 | echo "Unmounting disk image..." 157 | DEV_NAME=$(hdiutil info | egrep '^/dev/' | sed 1q | awk '{print $1}') 158 | test -d "${MOUNT_DIR}" && hdiutil detach "${DEV_NAME}" 159 | 160 | echo "Mount directory: $MOUNT_DIR" 161 | DEV_NAME=$(hdiutil attach -readwrite -noverify -noautoopen "${DMG_TEMP_NAME}" | egrep '^/dev/' | sed 1q | awk '{print $1}') 162 | echo "Device name: $DEV_NAME" 163 | 164 | if ! test -z "$BACKGROUND_FILE"; then 165 | echo "Copying background file..." 166 | test -d "$MOUNT_DIR/.background" || mkdir "$MOUNT_DIR/.background" 167 | cp "$BACKGROUND_FILE" "$MOUNT_DIR/.background/$BACKGROUND_FILE_NAME" 168 | fi 169 | 170 | if ! test -z "$APPLICATION_LINK"; then 171 | echo "making link to Applications dir" 172 | echo $MOUNT_DIR 173 | ln -s /Applications "$MOUNT_DIR/Applications" 174 | fi 175 | 176 | if ! test -z "$VOLUME_ICON_FILE"; then 177 | echo "Copying volume icon file '$VOLUME_ICON_FILE'..." 178 | cp "$VOLUME_ICON_FILE" "$MOUNT_DIR/.VolumeIcon.icns" 179 | SetFile -c icnC "$MOUNT_DIR/.VolumeIcon.icns" 180 | fi 181 | 182 | # run applescript 183 | APPLESCRIPT=$(mktemp -t createdmg) 184 | cat "$AUX_PATH/template.applescript" | sed -e "s/WINX/$WINX/g" -e "s/WINY/$WINY/g" -e "s/WINW/$WINW/g" -e "s/WINH/$WINH/g" -e "s/BACKGROUND_CLAUSE/$BACKGROUND_CLAUSE/g" -e "s/REPOSITION_HIDDEN_FILES_CLAUSE/$REPOSITION_HIDDEN_FILES_CLAUSE/g" -e "s/ICON_SIZE/$ICON_SIZE/g" -e "s/TEXT_SIZE/$TEXT_SIZE/g" | perl -pe "s/POSITION_CLAUSE/$POSITION_CLAUSE/g" | perl -pe "s/APPLICATION_CLAUSE/$APPLICATION_CLAUSE/g" | perl -pe "s/HIDING_CLAUSE/$HIDING_CLAUSE/" >"$APPLESCRIPT" 185 | 186 | echo "Running Applescript: /usr/bin/osascript \"${APPLESCRIPT}\" \"${VOLUME_NAME}\"" 187 | "/usr/bin/osascript" "${APPLESCRIPT}" "${VOLUME_NAME}" || true 188 | echo "Done running the applescript..." 189 | sleep 4 190 | 191 | rm "$APPLESCRIPT" 192 | 193 | # make sure it's not world writeable 194 | echo "Fixing permissions..." 195 | chmod -Rf go-w "${MOUNT_DIR}" &> /dev/null || true 196 | echo "Done fixing permissions." 197 | 198 | # make the top window open itself on mount: 199 | echo "Blessing started" 200 | bless --folder "${MOUNT_DIR}" --openfolder "${MOUNT_DIR}" 201 | echo "Blessing finished" 202 | 203 | if ! test -z "$VOLUME_ICON_FILE"; then 204 | # tell the volume that it has a special file attribute 205 | SetFile -a C "$MOUNT_DIR" 206 | fi 207 | 208 | # unmount 209 | echo "Unmounting disk image..." 210 | hdiutil detach "${DEV_NAME}" 211 | 212 | # compress image 213 | echo "Compressing disk image..." 214 | hdiutil convert "${DMG_TEMP_NAME}" -format UDZO -imagekey zlib-level=9 -o "${DMG_DIR}/${DMG_NAME}" 215 | rm -f "${DMG_TEMP_NAME}" 216 | 217 | # adding EULA resources 218 | if [ ! -z "${EULA_RSRC}" -a "${EULA_RSRC}" != "-null-" ]; then 219 | echo "adding EULA resources" 220 | "${AUX_PATH}/dmg-license.py" "${DMG_DIR}/${DMG_NAME}" "${EULA_RSRC}" 221 | fi 222 | 223 | if [ ! -z "${NOINTERNET}" -a "${NOINTERNET}" == 1 ]; then 224 | echo "not setting 'internet-enable' on the dmg" 225 | else 226 | hdiutil internet-enable -yes "${DMG_DIR}/${DMG_NAME}" 227 | fi 228 | 229 | echo "Disk image done" 230 | exit 0 231 | -------------------------------------------------------------------------------- /include/tev/ImageViewer.h: -------------------------------------------------------------------------------- 1 | // This file was developed by Thomas Müller . 2 | // It is published under the BSD 3-Clause License within the LICENSE file. 3 | 4 | #pragma once 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | 14 | #include 15 | #include 16 | #include 17 | #include 18 | 19 | #include 20 | #include 21 | #include 22 | #include 23 | 24 | TEV_NAMESPACE_BEGIN 25 | 26 | class ImageViewer : public nanogui::Screen { 27 | public: 28 | ImageViewer(const std::shared_ptr& imagesLoader, bool fullscreen, bool floatBuffer, bool supportsHdr); 29 | virtual ~ImageViewer(); 30 | 31 | bool mouse_button_event(const nanogui::Vector2i &p, int button, bool down, int modifiers) override; 32 | bool mouse_motion_event(const nanogui::Vector2i& p, const nanogui::Vector2i& rel, int button, int modifiers) override; 33 | 34 | bool drop_event(const std::vector& filenames) override; 35 | 36 | bool keyboard_event(int key, int scancode, int action, int modifiers) override; 37 | 38 | void focusWindow(); 39 | 40 | void draw_contents() override; 41 | 42 | void insertImage(std::shared_ptr image, size_t index, bool shallSelect = false); 43 | void moveImageInList(size_t oldIndex, size_t newIndex); 44 | 45 | bool hasImageWithName(const std::string& imageName) { 46 | return !!imageByName(imageName); 47 | } 48 | 49 | void addImage(std::shared_ptr image, bool shallSelect = false) { 50 | insertImage(image, mImages.size(), shallSelect); 51 | } 52 | 53 | void removeImage(std::shared_ptr image); 54 | void removeImage(const std::string& imageName) { 55 | removeImage(imageByName(imageName)); 56 | } 57 | void removeAllImages(); 58 | 59 | void replaceImage(std::shared_ptr image, std::shared_ptr replacement, bool shallSelect); 60 | void replaceImage(const std::string& imageName, std::shared_ptr replacement, bool shallSelect) { 61 | replaceImage(imageByName(imageName), replacement, shallSelect); 62 | } 63 | 64 | void reloadImage(std::shared_ptr image, bool shallSelect = false); 65 | void reloadImage(const std::string& imageName, bool shallSelect = false) { 66 | reloadImage(imageByName(imageName), shallSelect); 67 | } 68 | void reloadAllImages(); 69 | void reloadImagesWhoseFileChanged(); 70 | 71 | void updateImage( 72 | const std::string& imageName, 73 | bool shallSelect, 74 | const std::string& channel, 75 | int x, int y, 76 | int width, int height, 77 | const std::vector& imageData 78 | ); 79 | 80 | void selectImage(const std::shared_ptr& image, bool stopPlayback = true); 81 | 82 | void selectGroup(std::string name); 83 | 84 | void selectReference(const std::shared_ptr& image); 85 | 86 | float exposure() const { 87 | return mExposureSlider->value(); 88 | } 89 | 90 | void setExposure(float value); 91 | 92 | float offset() const { 93 | return mOffsetSlider->value(); 94 | } 95 | 96 | void setOffset(float value); 97 | 98 | float gamma() const { 99 | return mGammaSlider->value(); 100 | } 101 | 102 | void setGamma(float value); 103 | 104 | void normalizeExposureAndOffset(); 105 | void resetImage(); 106 | 107 | ETonemap tonemap() const { 108 | return mImageCanvas->tonemap(); 109 | } 110 | 111 | void setTonemap(ETonemap tonemap); 112 | 113 | EMetric metric() const { 114 | return mImageCanvas->metric(); 115 | } 116 | 117 | void setMetric(EMetric metric); 118 | 119 | nanogui::Vector2i sizeToFitImage(const std::shared_ptr& image); 120 | nanogui::Vector2i sizeToFitAllImages(); 121 | bool setFilter(const std::string& filter); 122 | 123 | bool useRegex() const; 124 | void setUseRegex(bool value); 125 | 126 | bool watchFilesForChanges() const; 127 | void setWatchFilesForChanges(bool value); 128 | 129 | void maximize(); 130 | bool isMaximized(); 131 | void toggleMaximized(); 132 | 133 | bool isUiVisible() { 134 | return mSidebar->visible(); 135 | } 136 | void setUiVisible(bool shouldBeVisible); 137 | 138 | void toggleHelpWindow(); 139 | 140 | void openImageDialog(); 141 | void saveImageDialog(); 142 | 143 | void requestLayoutUpdate() { 144 | mRequiresLayoutUpdate = true; 145 | } 146 | 147 | template 148 | void scheduleToUiThread(const T& fun) { 149 | mTaskQueue.push(fun); 150 | } 151 | 152 | BackgroundImagesLoader& imagesLoader() const { 153 | return *mImagesLoader; 154 | } 155 | 156 | private: 157 | void updateFilter(); 158 | void updateLayout(); 159 | void updateTitle(); 160 | std::string groupName(size_t index); 161 | 162 | int groupId(const std::string& groupName) const; 163 | int imageId(const std::shared_ptr& image) const; 164 | int imageId(const std::string& imageName) const; 165 | 166 | std::string nextGroup(const std::string& groupName, EDirection direction); 167 | std::string nthVisibleGroup(size_t n); 168 | 169 | std::shared_ptr nextImage(const std::shared_ptr& image, EDirection direction); 170 | std::shared_ptr nthVisibleImage(size_t n); 171 | std::shared_ptr imageByName(const std::string& imageName); 172 | 173 | bool canDragSidebarFrom(const nanogui::Vector2i& p) { 174 | return mSidebar->visible() && p.x() - mSidebar->fixed_width() < 10 && p.x() - mSidebar->fixed_width() > -5; 175 | } 176 | 177 | int visibleSidebarWidth() { 178 | return mSidebar->visible() ? mSidebar->fixed_width() : 0; 179 | } 180 | 181 | int visibleFooterHeight() { 182 | return mFooter->visible() ? mFooter->fixed_height() : 0; 183 | } 184 | 185 | SharedQueue> mTaskQueue; 186 | 187 | bool mRequiresFilterUpdate = true; 188 | bool mRequiresLayoutUpdate = true; 189 | 190 | nanogui::Widget* mVerticalScreenSplit; 191 | 192 | nanogui::Widget* mSidebar; 193 | nanogui::Button* mHelpButton; 194 | nanogui::Widget* mSidebarLayout; 195 | 196 | nanogui::Widget* mFooter; 197 | 198 | nanogui::Label* mExposureLabel; 199 | nanogui::Slider* mExposureSlider; 200 | 201 | nanogui::Label* mOffsetLabel; 202 | nanogui::Slider* mOffsetSlider; 203 | 204 | nanogui::Label* mGammaLabel; 205 | nanogui::Slider* mGammaSlider; 206 | 207 | nanogui::Widget* mTonemapButtonContainer; 208 | nanogui::Widget* mMetricButtonContainer; 209 | 210 | std::shared_ptr mImagesLoader; 211 | 212 | std::shared_ptr mCurrentImage; 213 | std::shared_ptr mCurrentReference; 214 | 215 | std::vector> mImages; 216 | 217 | MultiGraph* mHistogram; 218 | std::set> mToBump; 219 | 220 | nanogui::TextBox* mFilter; 221 | nanogui::Button* mRegexButton; 222 | 223 | nanogui::Button* mWatchFilesForChangesButton; 224 | std::chrono::steady_clock::time_point mLastFileChangesCheck = {}; 225 | 226 | // Buttons which require a current image to be meaningful. 227 | std::vector mCurrentImageButtons; 228 | 229 | // Buttons which require at least one image to be meaningful 230 | std::vector mAnyImageButtons; 231 | 232 | nanogui::Button* mPlayButton; 233 | nanogui::IntBox* mFpsTextBox; 234 | std::thread mPlaybackThread; 235 | bool mShallRunPlaybackThread = true; 236 | 237 | nanogui::Widget* mImageButtonContainer; 238 | nanogui::Widget* mScrollContent; 239 | nanogui::VScrollPanel* mImageScrollContainer; 240 | 241 | ImageCanvas* mImageCanvas; 242 | 243 | nanogui::Widget* mGroupButtonContainer; 244 | std::string mCurrentGroup; 245 | 246 | HelpWindow* mHelpWindow = nullptr; 247 | 248 | bool mIsDraggingSidebar = false; 249 | bool mIsDraggingImage = false; 250 | bool mIsDraggingImageButton = false; 251 | size_t mDraggedImageButtonId; 252 | 253 | nanogui::Vector2f mDraggingStartPosition; 254 | 255 | size_t mClipboardIndex = 0; 256 | 257 | bool mSupportsHdr = false; 258 | nanogui::Button* mClipToLdrButton; 259 | 260 | int mDidFitToImage = 0; 261 | }; 262 | 263 | TEV_NAMESPACE_END 264 | -------------------------------------------------------------------------------- /include/tev/Ipc.h: -------------------------------------------------------------------------------- 1 | // This file was developed by Thomas Müller . 2 | // It is published under the BSD 3-Clause License within the LICENSE file. 3 | 4 | #pragma once 5 | 6 | #include 7 | 8 | #include 9 | #include 10 | 11 | TEV_NAMESPACE_BEGIN 12 | 13 | struct IpcPacketOpenImage { 14 | std::string imagePath; 15 | std::string channelSelector; 16 | bool grabFocus; 17 | }; 18 | 19 | struct IpcPacketReloadImage { 20 | std::string imageName; 21 | bool grabFocus; 22 | }; 23 | 24 | struct IpcPacketUpdateImage { 25 | std::string imageName; 26 | bool grabFocus; 27 | int32_t nChannels; 28 | std::vector channelNames; 29 | std::vector channelOffsets; 30 | std::vector channelStrides; 31 | int32_t x, y, width, height; 32 | std::vector> imageData; // One set of data per channel 33 | }; 34 | 35 | struct IpcPacketCloseImage { 36 | std::string imageName; 37 | }; 38 | 39 | struct IpcPacketCreateImage { 40 | std::string imageName; 41 | bool grabFocus; 42 | int32_t width, height; 43 | int32_t nChannels; 44 | std::vector channelNames; 45 | }; 46 | 47 | class IpcPacket { 48 | public: 49 | enum Type : char { 50 | OpenImage = 0, 51 | ReloadImage = 1, 52 | CloseImage = 2, 53 | UpdateImage = 3, 54 | CreateImage = 4, 55 | UpdateImageV2 = 5, // Adds multi-channel support 56 | UpdateImageV3 = 6, // Adds custom striding/offset support 57 | OpenImageV2 = 7, // Explicit separation of image name and channel selector 58 | }; 59 | 60 | IpcPacket() = default; 61 | IpcPacket(const char* data, size_t length); 62 | 63 | const char* data() const { 64 | return mPayload.data(); 65 | } 66 | 67 | size_t size() const { 68 | return mPayload.size(); 69 | } 70 | 71 | Type type() const { 72 | // The first 4 bytes encode the message size. 73 | return (Type)mPayload[4]; 74 | } 75 | 76 | struct ChannelDesc { 77 | std::string name; 78 | int64_t offset; 79 | int64_t stride; 80 | }; 81 | 82 | void setOpenImage(const std::string& imagePath, const std::string& channelSelector, bool grabFocus); 83 | void setReloadImage(const std::string& imageName, bool grabFocus); 84 | void setCloseImage(const std::string& imageName); 85 | void setUpdateImage(const std::string& imageName, bool grabFocus, const std::vector& channelDescs, int32_t x, int32_t y, int32_t width, int32_t height, const std::vector& stridedImageData); 86 | void setCreateImage(const std::string& imageName, bool grabFocus, int32_t width, int32_t height, int32_t nChannels, const std::vector& channelNames); 87 | 88 | IpcPacketOpenImage interpretAsOpenImage() const; 89 | IpcPacketReloadImage interpretAsReloadImage() const; 90 | IpcPacketCloseImage interpretAsCloseImage() const; 91 | IpcPacketUpdateImage interpretAsUpdateImage() const; 92 | IpcPacketCreateImage interpretAsCreateImage() const; 93 | 94 | private: 95 | std::vector mPayload; 96 | 97 | class IStream { 98 | public: 99 | IStream(const std::vector& data) : mData{data} { 100 | uint32_t size; 101 | *this >> size; 102 | if ((size_t)size != data.size()) { 103 | throw std::runtime_error{"Trying to read IPC packet with incorrect size."}; 104 | } 105 | } 106 | 107 | IStream& operator>>(bool& var) { 108 | if (mData.size() < mIdx + 1) { 109 | throw std::runtime_error{"Trying to read bool beyond the bounds of the IPC packet payload."}; 110 | } 111 | 112 | var = mData[mIdx] == 1; 113 | ++mIdx; 114 | return *this; 115 | } 116 | 117 | IStream& operator>>(std::string& var) { 118 | std::vector buffer; 119 | do { 120 | if (mData.size() < mIdx + 1) { 121 | throw std::runtime_error{"Trying to read string beyond the bounds of the IPC packet payload."}; 122 | } 123 | 124 | buffer.push_back(mData[mIdx]); 125 | } while (mData[mIdx++] != '\0'); 126 | var = buffer.data(); 127 | return *this; 128 | } 129 | 130 | template 131 | IStream& operator>>(std::vector& var) { 132 | for (auto& elem : var) { 133 | *this >> elem; 134 | } 135 | return *this; 136 | } 137 | 138 | template 139 | IStream& operator>>(T& var) { 140 | if (mData.size() < mIdx + sizeof(T)) { 141 | throw std::runtime_error{"Trying to read generic type beyond the bounds of the IPC packet payload."}; 142 | } 143 | 144 | var = *(T*)&mData[mIdx]; 145 | mIdx += sizeof(T); 146 | return *this; 147 | } 148 | 149 | size_t remainingBytes() const { 150 | return mData.size() - mIdx; 151 | } 152 | 153 | const char* get() const { 154 | return &mData[mIdx]; 155 | } 156 | private: 157 | const std::vector& mData; 158 | size_t mIdx = 0; 159 | }; 160 | 161 | class OStream { 162 | public: 163 | OStream(std::vector& data) : mData{data} { 164 | // Reserve space for an integer denoting the size 165 | // of the packet. 166 | *this << (uint32_t)0; 167 | } 168 | 169 | template 170 | OStream& operator<<(const std::vector& var) { 171 | for (auto&& elem : var) { 172 | *this << elem; 173 | } 174 | return *this; 175 | } 176 | 177 | OStream& operator<<(const std::string& var) { 178 | for (auto&& character : var) { 179 | *this << character; 180 | } 181 | *this << '\0'; 182 | return *this; 183 | } 184 | 185 | OStream& operator<<(bool var) { 186 | if (mData.size() < mIdx + 1) { 187 | mData.resize(mIdx + 1); 188 | } 189 | 190 | mData[mIdx] = var ? 1 : 0; 191 | ++mIdx; 192 | updateSize(); 193 | return *this; 194 | } 195 | 196 | template 197 | OStream& operator<<(T var) { 198 | if (mData.size() < mIdx + sizeof(T)) { 199 | mData.resize(mIdx + sizeof(T)); 200 | } 201 | 202 | *(T*)&mData[mIdx] = var; 203 | mIdx += sizeof(T); 204 | updateSize(); 205 | return *this; 206 | } 207 | private: 208 | void updateSize() { 209 | *((uint32_t*)mData.data()) = (uint32_t)mIdx; 210 | } 211 | 212 | std::vector& mData; 213 | size_t mIdx = 0; 214 | }; 215 | }; 216 | 217 | class Ipc { 218 | public: 219 | #ifdef _WIN32 220 | using socket_t = SOCKET; 221 | #else 222 | using socket_t = int; 223 | #endif 224 | 225 | Ipc(const std::string& hostname); 226 | virtual ~Ipc(); 227 | 228 | bool isPrimaryInstance() { 229 | return mIsPrimaryInstance; 230 | } 231 | 232 | bool attemptToBecomePrimaryInstance(); 233 | 234 | void sendToPrimaryInstance(const IpcPacket& message); 235 | void receiveFromSecondaryInstance(std::function callback); 236 | 237 | private: 238 | bool mIsPrimaryInstance; 239 | socket_t mSocketFd; 240 | 241 | #ifdef _WIN32 242 | HANDLE mInstanceMutex; 243 | #else 244 | int mLockFileDescriptor; 245 | fs::path mLockFile; 246 | #endif 247 | 248 | class SocketConnection { 249 | public: 250 | SocketConnection(Ipc::socket_t fd, const std::string& name); 251 | 252 | void service(std::function callback); 253 | 254 | void close(); 255 | 256 | bool isClosed() const; 257 | 258 | private: 259 | Ipc::socket_t mSocketFd; 260 | std::string mName; 261 | 262 | // Because TCP socket recv() calls return as much data as is available 263 | // (which may have the partial contents of a client-side send() call, 264 | // we need to buffer it up in SocketConnection. 265 | std::vector mBuffer; 266 | // Offset into buffer where next recv() call should start writing. 267 | size_t mRecvOffset = 0; 268 | }; 269 | 270 | std::list mSocketConnections; 271 | 272 | std::string mIp; 273 | std::string mPort; 274 | std::string mLockName; 275 | }; 276 | 277 | TEV_NAMESPACE_END 278 | -------------------------------------------------------------------------------- /include/tev/Image.h: -------------------------------------------------------------------------------- 1 | // This file was developed by Thomas Müller . 2 | // It is published under the BSD 3-Clause License within the LICENSE file. 3 | 4 | #pragma once 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | #include 12 | 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | 21 | TEV_NAMESPACE_BEGIN 22 | 23 | class ImageLoader; 24 | 25 | struct ImageData { 26 | ImageData() = default; 27 | ImageData(const ImageData&) = delete; 28 | ImageData(ImageData&&) = default; 29 | 30 | std::vector channels; 31 | std::vector layers; 32 | nanogui::Matrix4f toRec709 = nanogui::Matrix4f{1.0f}; // Identity by default 33 | bool hasPremultipliedAlpha; 34 | 35 | Box2i dataWindow; 36 | Box2i displayWindow; 37 | 38 | std::string partName; 39 | 40 | nanogui::Vector2i size() const { 41 | return dataWindow.size(); 42 | } 43 | 44 | nanogui::Vector2i displaySize() const { 45 | return displayWindow.size(); 46 | } 47 | 48 | size_t numPixels() const { 49 | return channels.front().numPixels(); 50 | } 51 | 52 | std::vector channelsInLayer(std::string layerName) const; 53 | 54 | Task convertToRec709(int priority); 55 | 56 | void alphaOperation(const std::function& func); 57 | 58 | Task multiplyAlpha(int priority); 59 | Task unmultiplyAlpha(int priority); 60 | 61 | Task ensureValid(const std::string& channelSelector, int taskPriority); 62 | 63 | bool hasChannel(const std::string& channelName) const { 64 | return channel(channelName) != nullptr; 65 | } 66 | 67 | const Channel* channel(const std::string& channelName) const { 68 | auto it = std::find_if( 69 | std::begin(channels), 70 | std::end(channels), 71 | [&channelName](const Channel& c) { return c.name() == channelName; } 72 | ); 73 | 74 | if (it != std::end(channels)) { 75 | return &(*it); 76 | } else { 77 | return nullptr; 78 | } 79 | } 80 | 81 | Channel* mutableChannel(const std::string& channelName) { 82 | auto it = std::find_if( 83 | std::begin(channels), 84 | std::end(channels), 85 | [&channelName](const Channel& c) { return c.name() == channelName; } 86 | ); 87 | 88 | if (it != std::end(channels)) { 89 | return &(*it); 90 | } else { 91 | return nullptr; 92 | } 93 | } 94 | }; 95 | 96 | struct ChannelGroup { 97 | std::string name; 98 | std::vector channels; 99 | }; 100 | 101 | struct ImageTexture { 102 | nanogui::ref nanoguiTexture; 103 | std::vector channels; 104 | bool mipmapDirty; 105 | }; 106 | 107 | class Image { 108 | public: 109 | Image(const fs::path& path, fs::file_time_type fileLastModified, ImageData&& data, const std::string& channelSelector); 110 | virtual ~Image(); 111 | 112 | const fs::path& path() const { 113 | return mPath; 114 | } 115 | 116 | fs::file_time_type fileLastModified() const { 117 | return mFileLastModified; 118 | } 119 | 120 | void setFileLastModified(fs::file_time_type value) { 121 | mFileLastModified = value; 122 | } 123 | 124 | const std::string& channelSelector() const { 125 | return mChannelSelector; 126 | } 127 | 128 | const std::string& name() const { 129 | return mName; 130 | } 131 | 132 | std::string shortName() const; 133 | 134 | bool hasChannel(const std::string& channelName) const { 135 | return mData.hasChannel(channelName); 136 | } 137 | 138 | const Channel* channel(const std::string& channelName) const { 139 | return mData.channel(channelName); 140 | } 141 | 142 | nanogui::Texture* texture(const std::string& channelGroupName); 143 | nanogui::Texture* texture(const std::vector& channelNames); 144 | 145 | std::vector channelsInGroup(const std::string& groupName) const; 146 | std::vector getSortedChannels(const std::string& layerName) const; 147 | 148 | nanogui::Vector2i size() const { 149 | return mData.size(); 150 | } 151 | 152 | const Box2i& dataWindow() const { 153 | return mData.dataWindow; 154 | } 155 | 156 | const Box2i& displayWindow() const { 157 | return mData.displayWindow; 158 | } 159 | 160 | nanogui::Vector2f centerDisplayOffset(const Box2i& displayWindow) const { 161 | return Box2f{dataWindow()}.middle() - Box2f{displayWindow}.middle(); 162 | } 163 | 164 | size_t numPixels() const { 165 | return mData.numPixels(); 166 | } 167 | 168 | const std::vector& channelGroups() const { 169 | return mChannelGroups; 170 | } 171 | 172 | int id() const { 173 | return mId; 174 | } 175 | 176 | void bumpId() { 177 | int oldId = mId; 178 | mId = sId++; 179 | 180 | if (mStaleIdCallback) { 181 | mStaleIdCallback(oldId); 182 | } 183 | } 184 | 185 | static int drawId() { 186 | return sId++; 187 | } 188 | 189 | void updateChannel(const std::string& channelName, int x, int y, int width, int height, const std::vector& data); 190 | 191 | void setStaleIdCallback(const std::function& callback) { 192 | mStaleIdCallback = callback; 193 | } 194 | 195 | std::string toString() const; 196 | 197 | private: 198 | static std::atomic sId; 199 | 200 | Channel* mutableChannel(const std::string& channelName) { 201 | return mData.mutableChannel(channelName); 202 | } 203 | 204 | std::vector getGroupedChannels(const std::string& layerName) const; 205 | 206 | fs::path mPath; 207 | fs::file_time_type mFileLastModified; 208 | 209 | std::string mChannelSelector; 210 | 211 | std::string mName; 212 | 213 | std::map mTextures; 214 | 215 | ImageData mData; 216 | 217 | std::vector mChannelGroups; 218 | 219 | std::function mStaleIdCallback; 220 | 221 | int mId; 222 | }; 223 | 224 | Task>> tryLoadImage(int imageId, fs::path path, std::istream& iStream, std::string channelSelector); 225 | Task>> tryLoadImage(fs::path path, std::istream& iStream, std::string channelSelector); 226 | Task>> tryLoadImage(int imageId, fs::path path, std::string channelSelector); 227 | Task>> tryLoadImage(fs::path path, std::string channelSelector); 228 | 229 | struct ImageAddition { 230 | int loadId; 231 | bool shallSelect; 232 | std::vector> images; 233 | std::shared_ptr toReplace; 234 | 235 | struct Comparator { 236 | bool operator()(const ImageAddition& a, const ImageAddition& b) { 237 | return a.loadId > b.loadId; 238 | } 239 | }; 240 | }; 241 | 242 | struct PathAndChannelSelector { 243 | fs::path path; 244 | std::string channelSelector; 245 | 246 | bool operator<(const PathAndChannelSelector& other) const { 247 | return path == other.path ? (channelSelector < other.channelSelector) : (path < other.path); 248 | } 249 | }; 250 | 251 | class BackgroundImagesLoader { 252 | public: 253 | void enqueue(const fs::path& path, const std::string& channelSelector, bool shallSelect, const std::shared_ptr& toReplace = nullptr); 254 | void checkDirectoriesForNewFilesAndLoadThose(); 255 | 256 | std::optional tryPop() { return mLoadedImages.tryPop(); } 257 | 258 | bool publishSortedLoads(); 259 | bool hasPendingLoads() const { 260 | return mLoadCounter != mUnsortedLoadCounter; 261 | } 262 | 263 | bool recursiveDirectories() const { 264 | return mRecursiveDirectories; 265 | } 266 | 267 | void setRecursiveDirectories(bool value) { 268 | mRecursiveDirectories = value; 269 | } 270 | 271 | private: 272 | SharedQueue mLoadedImages; 273 | 274 | std::priority_queue< 275 | ImageAddition, 276 | std::vector, 277 | ImageAddition::Comparator 278 | > mPendingLoadedImages; 279 | std::mutex mPendingLoadedImagesMutex; 280 | 281 | std::atomic mLoadCounter{0}; 282 | std::atomic mUnsortedLoadCounter{0}; 283 | 284 | bool mRecursiveDirectories = false; 285 | std::map> mDirectories; 286 | std::set mFilesFoundInDirectories; 287 | }; 288 | 289 | TEV_NAMESPACE_END 290 | -------------------------------------------------------------------------------- /src/HelpWindow.cpp: -------------------------------------------------------------------------------- 1 | // This file was developed by Thomas Müller . 2 | // It is published under the BSD 3-Clause License within the LICENSE file. 3 | 4 | #include 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | 14 | using namespace nanogui; 15 | using namespace std; 16 | 17 | TEV_NAMESPACE_BEGIN 18 | 19 | #ifdef __APPLE__ 20 | string HelpWindow::COMMAND = "Cmd"; 21 | #else 22 | string HelpWindow::COMMAND = "Ctrl"; 23 | #endif 24 | 25 | #ifdef __APPLE__ 26 | string HelpWindow::ALT = "Opt"; 27 | #else 28 | string HelpWindow::ALT = "Alt"; 29 | #endif 30 | 31 | HelpWindow::HelpWindow(Widget *parent, bool supportsHdr, function closeCallback) 32 | : Window{parent, "Help"}, mCloseCallback{closeCallback} { 33 | 34 | auto closeButton = new Button{button_panel(), "", FA_TIMES}; 35 | closeButton->set_callback(mCloseCallback); 36 | 37 | set_layout(new GroupLayout{}); 38 | set_fixed_width(600); 39 | 40 | TabWidget* tabWidget = new TabWidget{this}; 41 | 42 | // Keybindings tab 43 | { 44 | Widget* shortcuts = new Widget(tabWidget); 45 | shortcuts->set_layout(new GroupLayout{}); 46 | tabWidget->append_tab("Keybindings", shortcuts); 47 | 48 | auto addRow = [](Widget* current, string keys, string desc) { 49 | auto row = new Widget{current}; 50 | row->set_layout(new BoxLayout{Orientation::Horizontal, Alignment::Fill, 0, 10}); 51 | auto descWidget = new Label{row, desc, "sans"}; 52 | descWidget->set_fixed_width(250); 53 | new Label{row, keys, "sans-bold"}; 54 | }; 55 | 56 | new Label{shortcuts, "Image Loading", "sans-bold", 18}; 57 | auto imageLoading = new Widget{shortcuts}; 58 | imageLoading->set_layout(new BoxLayout{Orientation::Vertical, Alignment::Fill, 0, 0}); 59 | 60 | addRow(imageLoading, COMMAND + "+O", "Open Image"); 61 | addRow(imageLoading, COMMAND + "+S", "Save View as Image"); 62 | addRow(imageLoading, COMMAND + "+R or F5", "Reload Image"); 63 | addRow(imageLoading, COMMAND + "+Shift+R or " + COMMAND + "+F5", "Reload All Images"); 64 | addRow(imageLoading, COMMAND + "+W", "Close Image"); 65 | addRow(imageLoading, COMMAND + "+Shift+W", "Close All Images"); 66 | addRow(imageLoading, COMMAND + "+C", "Copy Image or Path to Clipboard"); 67 | addRow(imageLoading, COMMAND + "+V", "Paste Image from Clipboard"); 68 | 69 | new Label{shortcuts, "Image Options", "sans-bold", 18}; 70 | auto imageSelection = new Widget{shortcuts}; 71 | imageSelection->set_layout(new BoxLayout{Orientation::Vertical, Alignment::Fill, 0, 0}); 72 | 73 | addRow(imageSelection, "Left Click", "Select Hovered Image"); 74 | addRow(imageSelection, "1…9", "Select N-th Image"); 75 | addRow(imageSelection, "Down or S / Up or W", "Select Next / Previous Image"); 76 | 77 | addRow(imageSelection, "Click & Drag (+Shift/" + COMMAND + ")", "Translate Image"); 78 | addRow(imageSelection, "Plus / Minus / Scroll (+Shift/" + COMMAND + ")", "Zoom In / Out of Image"); 79 | 80 | addRow(imageSelection, "F", "Fit Image to Screen"); 81 | addRow(imageSelection, "N", "Normalize Image to [0, 1]"); 82 | addRow(imageSelection, "R", "Reset Image Parameters"); 83 | if (supportsHdr) { 84 | addRow(imageSelection, "L", "Display the image as if on an LDR screen"); 85 | } 86 | 87 | addRow(imageSelection, "Shift+Right or Shift+D / Shift+Left or Shift+A", "Select Next / Previous Tonemap"); 88 | 89 | addRow(imageSelection, "E / Shift+E", "Increase / Decrease Exposure by 0.5"); 90 | addRow(imageSelection, "O / Shift+O", "Increase / Decrease Offset by 0.1"); 91 | 92 | addRow(imageSelection, "Shift+Ctrl (hold)", "Display raw bytes on pixels when zoomed-in"); 93 | 94 | new Label{shortcuts, "Reference Options", "sans-bold", 18}; 95 | auto referenceSelection = new Widget{shortcuts}; 96 | referenceSelection->set_layout(new BoxLayout{Orientation::Vertical, Alignment::Fill, 0, 0}); 97 | 98 | addRow(referenceSelection, ALT + " (hold)", "View currently selected Reference"); 99 | addRow(referenceSelection, "Shift+Left Click or Right Click", "Select Hovered Image as Reference"); 100 | addRow(referenceSelection, "Shift+1…9", "Select N-th Image as Reference"); 101 | addRow(referenceSelection, "Shift+Down or Shift+S / Shift+Up or Shift+W", "Select Next / Previous Image as Reference"); 102 | 103 | addRow(referenceSelection, "Ctrl (hold)", "View selected Image if Reference is selected"); 104 | addRow(referenceSelection, "Ctrl+Right or Ctrl+D / Ctrl+Left or Ctrl+A", "Select Next / Previous Error Metric"); 105 | 106 | new Label{shortcuts, "Channel Group Options", "sans-bold", 18}; 107 | auto groupSelection = new Widget{shortcuts}; 108 | groupSelection->set_layout(new BoxLayout{Orientation::Vertical, Alignment::Fill, 0, 0}); 109 | 110 | addRow(groupSelection, "Left Click", "Select Hovered Channel Group"); 111 | addRow(groupSelection, "Ctrl+1…9", "Select N-th Channel Group"); 112 | addRow(groupSelection, "Right or D / Left or A", "Select Next / Previous Channel Group"); 113 | 114 | new Label{shortcuts, "Interface", "sans-bold", 18}; 115 | auto ui = new Widget{shortcuts}; 116 | ui->set_layout(new BoxLayout{Orientation::Vertical, Alignment::Fill, 0, 0}); 117 | 118 | addRow(ui, ALT + "+Enter", "Maximize"); 119 | addRow(ui, COMMAND + "+B", "Toggle GUI"); 120 | addRow(ui, "H", "Show Help (this Window)"); 121 | addRow(ui, COMMAND + "+P", "Find Image or Channel Group"); 122 | addRow(ui, COMMAND + "+Q", "Quit"); 123 | } 124 | 125 | // About tab 126 | { 127 | Widget* about = new Widget(tabWidget); 128 | about->set_layout(new GroupLayout{}); 129 | tabWidget->append_tab("About", about); 130 | 131 | auto addText = [](Widget* current, string text, string font = "sans", int fontSize = 18) { 132 | auto row = new Widget{current}; 133 | row->set_layout(new BoxLayout{Orientation::Vertical, Alignment::Middle, 0, 10}); 134 | new Label{row, text, font, fontSize }; 135 | }; 136 | 137 | auto addLibrary = [](Widget* current, string name, string license, string desc) { 138 | auto row = new Widget{current}; 139 | row->set_layout(new BoxLayout{Orientation::Horizontal, Alignment::Fill, 3, 30}); 140 | auto leftColumn = new Widget{row}; 141 | leftColumn->set_layout(new BoxLayout{Orientation::Vertical, Alignment::Maximum}); 142 | leftColumn->set_fixed_width(135); 143 | 144 | new Label{leftColumn, name, "sans-bold", 18}; 145 | new Label{row, desc, "sans", 18}; 146 | }; 147 | 148 | auto addSpacer = [](Widget* current, int space) { 149 | auto row = new Widget{current}; 150 | row->set_height(space); 151 | }; 152 | 153 | addSpacer(about, 15); 154 | 155 | addText(about, "tev — The EXR Viewer", "sans-bold", 46); 156 | addText(about, "version " TEV_VERSION, "sans", 26); 157 | 158 | addSpacer(about, 50); 159 | 160 | addText(about, "tev was developed by Thomas Müller and is released under the BSD 3-Clause License."); 161 | addText(about, "It was built directly or indirectly upon the following amazing third-party libraries."); 162 | 163 | addSpacer(about, 30); 164 | 165 | addLibrary(about, "args", "", "Single-Header Argument Parsing Library"); 166 | addLibrary(about, "clip", "", "Cross-Platform Clipboard Library"); 167 | addLibrary(about, "Glad", "", "Multi-Language GL Loader-Generator"); 168 | addLibrary(about, "GLFW", "", "OpenGL Desktop Development Library"); 169 | addLibrary(about, "NanoGUI", "", "Small GUI Library"); 170 | addLibrary(about, "NanoVG", "", "Small Vector Graphics Library"); 171 | addLibrary(about, "OpenEXR", "", "High Dynamic-Range (HDR) Image File Format"); 172 | addLibrary(about, "qoi", "", "File Format for Fast, Lossless Image Compression"); 173 | addLibrary(about, "stb_image(_write)", "", "Single-Header Library for Loading and Writing Images"); 174 | addLibrary(about, "tinyformat", "", "Minimal Type-Safe printf() Replacement"); 175 | addLibrary(about, "tinylogger", "", "Minimal Pretty-Logging Library"); 176 | addLibrary(about, "UTF8-CPP", "", "Lightweight UTF-8 String Manipulation Library"); 177 | } 178 | 179 | tabWidget->set_selected_id(0); 180 | tabWidget->set_callback([tabWidget] (int id) mutable { 181 | tabWidget->set_selected_id(id); 182 | }); 183 | } 184 | 185 | bool HelpWindow::keyboard_event(int key, int scancode, int action, int modifiers) { 186 | if (Window::keyboard_event(key, scancode, action, modifiers)) { 187 | return true; 188 | } 189 | 190 | if (key == GLFW_KEY_ESCAPE) { 191 | mCloseCallback(); 192 | return true; 193 | } 194 | 195 | return false; 196 | } 197 | 198 | TEV_NAMESPACE_END 199 | -------------------------------------------------------------------------------- /include/tev/Common.h: -------------------------------------------------------------------------------- 1 | // This file was developed by Thomas Müller . 2 | // It is published under the BSD 3-Clause License within the LICENSE file. 3 | 4 | #pragma once 5 | 6 | #include 7 | #include 8 | 9 | #include 10 | 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | 19 | #ifdef _WIN32 20 | # define NOMINMAX 21 | # include 22 | # undef NOMINMAX 23 | # pragma warning(disable : 4127) // warning C4127: conditional expression is constant 24 | # pragma warning(disable : 4244) // warning C4244: conversion from X to Y, possible loss of data 25 | #endif 26 | 27 | // Define command key for windows/mac/linux 28 | #ifdef __APPLE__ 29 | #define SYSTEM_COMMAND_LEFT GLFW_KEY_LEFT_SUPER 30 | #define SYSTEM_COMMAND_RIGHT GLFW_KEY_RIGHT_SUPER 31 | #else 32 | #define SYSTEM_COMMAND_LEFT GLFW_KEY_LEFT_CONTROL 33 | #define SYSTEM_COMMAND_RIGHT GLFW_KEY_RIGHT_CONTROL 34 | #endif 35 | 36 | // A macro is used such that external tools won't end up indenting entire files, 37 | // resulting in wasted horizontal space. 38 | #define TEV_NAMESPACE_BEGIN namespace tev { 39 | #define TEV_NAMESPACE_END } 40 | 41 | #ifdef __GNUC__ 42 | # define LIKELY(condition) __builtin_expect(static_cast(condition), 1) 43 | # define UNLIKELY(condition) __builtin_expect(static_cast(condition), 0) 44 | #else 45 | # define LIKELY(condition) condition 46 | # define UNLIKELY(condition) condition 47 | #endif 48 | 49 | #define TEV_ASSERT(cond, description, ...) \ 50 | if (UNLIKELY(!(cond))) \ 51 | throw std::runtime_error{tfm::format(description, ##__VA_ARGS__)}; 52 | 53 | #ifndef TEV_VERSION 54 | # define TEV_VERSION "undefined" 55 | #endif 56 | 57 | struct NVGcontext; 58 | 59 | namespace nanogui { 60 | template 61 | Array inverse(const Array &a) { 62 | Array result; 63 | for (size_t i = 0; i < Size; ++i) 64 | result.v[i] = 1.0f / a.v[i]; 65 | return result; 66 | } 67 | 68 | template 69 | Value mean(const Array &a) { 70 | Value result = 0; 71 | for (size_t i = 0; i < Size; ++i) 72 | result += a.v[i]; 73 | return result / (Value)Size; 74 | } 75 | 76 | inline Matrix3f inverse(Matrix3f mat) { 77 | float d11 = mat.m[1][1] * mat.m[2][2] + mat.m[1][2] * -mat.m[2][1]; 78 | float d12 = mat.m[1][0] * mat.m[2][2] + mat.m[1][2] * -mat.m[2][0]; 79 | float d13 = mat.m[1][0] * mat.m[2][1] + mat.m[1][1] * -mat.m[2][0]; 80 | 81 | float det = mat.m[0][0] * d11 - mat.m[0][1] * d12 + mat.m[0][2] * d13; 82 | 83 | if (std::abs(det) == 0.0f) { 84 | return Matrix3f{0.0f}; 85 | } 86 | 87 | det = 1.0f / det; 88 | 89 | float d21 = mat.m[0][1] * mat.m[2][2] + mat.m[0][2] * -mat.m[2][1]; 90 | float d22 = mat.m[0][0] * mat.m[2][2] + mat.m[0][2] * -mat.m[2][0]; 91 | float d23 = mat.m[0][0] * mat.m[2][1] + mat.m[0][1] * -mat.m[2][0]; 92 | 93 | float d31 = mat.m[0][1] * mat.m[1][2] - mat.m[0][2] * mat.m[1][1]; 94 | float d32 = mat.m[0][0] * mat.m[1][2] - mat.m[0][2] * mat.m[1][0]; 95 | float d33 = mat.m[0][0] * mat.m[1][1] - mat.m[0][1] * mat.m[1][0]; 96 | 97 | mat.m[0][0] = +d11 * det; 98 | mat.m[0][1] = -d21 * det; 99 | mat.m[0][2] = +d31 * det; 100 | mat.m[1][0] = -d12 * det; 101 | mat.m[1][1] = +d22 * det; 102 | mat.m[1][2] = -d32 * det; 103 | mat.m[2][0] = +d13 * det; 104 | mat.m[2][1] = -d23 * det; 105 | mat.m[2][2] = +d33 * det; 106 | return mat; 107 | } 108 | 109 | template 110 | Array operator*(const Matrix& m, const Array& v) { 111 | Array result; 112 | Value w = 0; 113 | for (size_t i = 0; i < Size; ++i) { 114 | Value accum = 0; 115 | for (size_t k = 0; k < Size; ++k) 116 | accum += m.m[k][i] * (k == Size - 1 ? 1 : v.v[k]); 117 | 118 | if (i == Size-1) { 119 | w = accum; 120 | } else { 121 | result.v[i] = accum; 122 | } 123 | } 124 | return result / w; 125 | } 126 | 127 | template 128 | bool operator==(const Matrix& a, const Matrix& b) { 129 | for (size_t m = 0; m < Size; ++m) { 130 | for (size_t n = 0; n < Size; ++n) { 131 | if (a.m[m][n] != b.m[m][n]) { 132 | return false; 133 | } 134 | } 135 | } 136 | return true; 137 | } 138 | 139 | template 140 | bool operator!=(const Matrix& a, const Matrix& b) { 141 | return !(a == b); 142 | } 143 | } 144 | 145 | TEV_NAMESPACE_BEGIN 146 | 147 | namespace fs = std::filesystem; 148 | 149 | inline uint32_t swapBytes(uint32_t value) { 150 | #ifdef _WIN32 151 | return _byteswap_ulong(value); 152 | #else 153 | return __builtin_bswap32(value); 154 | #endif 155 | } 156 | 157 | inline float swapBytes(float value) { 158 | float result; 159 | auto valueChars = reinterpret_cast(&value); 160 | auto resultChars = reinterpret_cast(&result); 161 | 162 | resultChars[0] = valueChars[3]; 163 | resultChars[1] = valueChars[2]; 164 | resultChars[2] = valueChars[1]; 165 | resultChars[3] = valueChars[0]; 166 | 167 | return result; 168 | } 169 | 170 | inline int codePointLength(char first) { 171 | if ((first & 0xf8) == 0xf0) { 172 | return 4; 173 | } else if ((first & 0xf0) == 0xe0) { 174 | return 3; 175 | } else if ((first & 0xe0) == 0xc0) { 176 | return 2; 177 | } else { 178 | return 1; 179 | } 180 | } 181 | 182 | std::string ensureUtf8(const std::string& str); 183 | std::string utf16to8(const std::wstring& utf16); 184 | fs::path toPath(const std::string& utf8); 185 | std::string toString(const fs::path& path); 186 | 187 | template 188 | void forEachFileInDir(bool recursive, const fs::path& path, F&& callback) { 189 | // Ignore errors in case a directory no longer exists. 190 | // Simply don't invoke the loop body in that case. 191 | std::error_code ec; 192 | if (recursive) { 193 | for (auto const& entry : fs::recursive_directory_iterator{path, ec}) { 194 | callback(entry); 195 | } 196 | } else { 197 | for (auto const& entry : fs::directory_iterator{path, ec}) { 198 | callback(entry); 199 | } 200 | } 201 | } 202 | 203 | template 204 | class ScopeGuard { 205 | public: 206 | ScopeGuard(const T& callback) : mCallback{callback} {} 207 | ScopeGuard(T&& callback) : mCallback{std::move(callback)} {} 208 | ScopeGuard(const ScopeGuard& other) = delete; 209 | ScopeGuard(ScopeGuard&& other) { mCallback = std::move(other.mCallback); other.mCallback = {}; } 210 | ~ScopeGuard() { mCallback(); } 211 | private: 212 | T mCallback; 213 | }; 214 | 215 | template 216 | T round(T value, T decimals) { 217 | auto precision = std::pow(static_cast(10), decimals); 218 | return std::round(value * precision) / precision; 219 | } 220 | 221 | template 222 | std::string join(const T& components, const std::string& delim) { 223 | std::ostringstream s; 224 | for (const auto& component : components) { 225 | if (&components[0] != &component) { 226 | s << delim; 227 | } 228 | s << component; 229 | } 230 | 231 | return s.str(); 232 | } 233 | 234 | std::vector split(std::string text, const std::string& delim); 235 | 236 | std::string toLower(std::string str); 237 | std::string toUpper(std::string str); 238 | 239 | bool matchesFuzzy(std::string text, std::string filter, size_t* matchedPartId = nullptr); 240 | bool matchesRegex(std::string text, std::string filter); 241 | inline bool matchesFuzzyOrRegex(const std::string& text, const std::string& filter, bool isRegex) { 242 | return isRegex ? matchesRegex(text, filter) : matchesFuzzy(text, filter); 243 | } 244 | 245 | void drawTextWithShadow(NVGcontext* ctx, float x, float y, std::string text, float shadowAlpha = 1.0f); 246 | 247 | inline float toSRGB(float linear, float gamma = 2.4f) { 248 | static const float a = 0.055f; 249 | if (linear <= 0.0031308f) { 250 | return 12.92f * linear; 251 | } else { 252 | return (1 + a) * pow(linear, 1 / gamma) - a; 253 | } 254 | } 255 | 256 | inline float toLinear(float sRGB, float gamma = 2.4f) { 257 | static const float a = 0.055f; 258 | if (sRGB <= 0.04045f) { 259 | return sRGB / 12.92f; 260 | } else { 261 | return pow((sRGB + a) / (1 + a), gamma); 262 | } 263 | } 264 | 265 | int lastError(); 266 | int lastSocketError(); 267 | std::string errorString(int errorId); 268 | 269 | fs::path homeDirectory(); 270 | 271 | void toggleConsole(); 272 | 273 | bool shuttingDown(); 274 | void setShuttingDown(); 275 | 276 | enum ETonemap : int { 277 | SRGB = 0, 278 | Gamma, 279 | FalseColor, 280 | PositiveNegative, 281 | 282 | // This enum value should never be used directly. 283 | // It facilitates looping over all members of this enum. 284 | NumTonemaps, 285 | }; 286 | 287 | ETonemap toTonemap(std::string name); 288 | 289 | enum EMetric : int { 290 | Error = 0, 291 | AbsoluteError, 292 | SquaredError, 293 | RelativeAbsoluteError, 294 | RelativeSquaredError, 295 | 296 | // This enum value should never be used directly. 297 | // It facilitates looping over all members of this enum. 298 | NumMetrics, 299 | }; 300 | 301 | EMetric toMetric(std::string name); 302 | 303 | enum EDirection { 304 | Forward, 305 | Backward, 306 | }; 307 | 308 | // Implemented in main.cpp 309 | void scheduleToMainThread(const std::function& fun); 310 | void redrawWindow(); 311 | 312 | TEV_NAMESPACE_END 313 | --------------------------------------------------------------------------------