diff options
| author | pants <niklasjo2004@gmail.com> | 2026-09-21 14:18:06 -0700 |
|---|---|---|
| committer | pants <niklasjo2004@gmail.com> | 2026-09-21 14:18:06 -0700 |
| commit | 83d5ba42f8a98fc0df177efef4333f74a0481f61 (patch) | |
| tree | 6f1fcff64ec9adb2fc51a7cfcc451eb22a103fb3 /src/image | |
Diffstat (limited to 'src/image')
| -rw-r--r-- | src/image/libvips.cpp | 216 | ||||
| -rw-r--r-- | src/image/libvips.hpp | 71 | ||||
| -rw-r--r-- | src/image/opencv.cpp | 249 | ||||
| -rw-r--r-- | src/image/opencv.hpp | 68 |
4 files changed, 604 insertions, 0 deletions
diff --git a/src/image/libvips.cpp b/src/image/libvips.cpp new file mode 100644 index 0000000..c7ecffc --- /dev/null +++ b/src/image/libvips.cpp @@ -0,0 +1,216 @@ +// Display images inside a terminal +// Copyright (C) 2023 JustKidding +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. + +#include "libvips.hpp" +#include "dimensions.hpp" +#include "flags.hpp" +#include "terminal.hpp" +#include "util.hpp" + +#include <algorithm> +#include <unordered_set> + +#ifdef ENABLE_OPENCV +# include <opencv2/videoio.hpp> +#endif + +using vips::VError; +using vips::VImage; + +LibvipsImage::LibvipsImage(std::shared_ptr<Dimensions> new_dims, const std::string &filename, bool in_cache) + : path(filename), + dims(std::move(new_dims)), + max_width(dims->max_wpixels()), + max_height(dims->max_hpixels()), + in_cache(in_cache) +{ + image = VImage::new_from_file(path.c_str()).colourspace(VIPS_INTERPRETATION_sRGB); + flags = Flags::instance(); + logger = spdlog::get("vips"); + logger->info("loading file {}", filename); + + try { + // animated images should have both n-pages and delay + npages = image.get_int("n-pages"); + std::ignore = image.get_array_int("delay"); + is_anim = true; + logger->info("file is an animated image"); + auto *opts = VImage::option()->set("n", -1); + backup = VImage::new_from_file(filename.c_str(), opts).colourspace(VIPS_INTERPRETATION_sRGB); + orig_height = backup.height() / npages; + image = backup.crop(0, 0, backup.width(), orig_height); + } catch (const VError &err) { + logger->debug("Failed to process image animation"); + } + + if (!is_anim) { + image = image.autorot(); + } + process_image(); +} + +auto LibvipsImage::dimensions() const -> const Dimensions & +{ + return *dims; +} + +auto LibvipsImage::filename() const -> std::string +{ + return path.string(); +} + +auto LibvipsImage::width() const -> int +{ + return image.width(); +} + +auto LibvipsImage::height() const -> int +{ + return image.height(); +} + +auto LibvipsImage::size() const -> size_t +{ + return _size; +} + +auto LibvipsImage::data() const -> const unsigned char * +{ + return _data.get(); +} + +auto LibvipsImage::channels() const -> int +{ + return image.bands(); +} + +auto LibvipsImage::is_animated() const -> bool +{ + return is_anim; +} + +auto LibvipsImage::next_frame() -> void +{ + if (!is_anim) { + return; + } + top += orig_height; + if (top == backup.height()) { + top = 0; + } + image = backup.crop(0, top, backup.width(), orig_height); + process_image(); +} + +auto LibvipsImage::frame_delay() const -> int +{ + if (!is_anim) { + return -1; + } + try { + const auto delays = backup.get_array_int("delay"); + const int ms_per_sec = 1000; + if (delays.at(0) == 0) { +#ifdef ENABLE_OPENCV + const cv::VideoCapture video(path); + if (video.isOpened()) { + return static_cast<int>((1.0 / video.get(cv::CAP_PROP_FPS)) * ms_per_sec); + } +#endif + return static_cast<int>((1.0 / npages) * ms_per_sec); + } + return delays.at(0); + } catch (const VError &err) { + return -1; + } +} + +auto LibvipsImage::resize_image() -> void +{ + if (in_cache) { + return; + } + const auto [new_width, new_height] = get_new_sizes(max_width, max_height, dims->scaler, flags->scale_factor); + if (new_width <= 0 && new_height <= 0) { + // ensure width and height are pair + if (flags->needs_scaling) { + const auto curw = width(); + const auto curh = height(); + if ((curw % 2) != 0 || (curh % 2) != 0) { + auto *opts = VImage::option() + ->set("height", util::round_up(curh, flags->scale_factor)) + ->set("size", VIPS_SIZE_FORCE); + image = image.thumbnail_image(util::round_up(curw, flags->scale_factor), opts); + } + } + return; + } + + logger->debug("Resizing image"); + + auto *opts = VImage::option()->set("height", new_height)->set("size", VIPS_SIZE_FORCE); + image = image.thumbnail_image(new_width, opts); + + if (is_anim || flags->no_cache) { + return; + } + + const auto save_location = util::get_cache_file_save_location(path); + try { + image.write_to_file(save_location.c_str()); + logger->debug("Saved resized image"); + } catch (const VError &err) { + logger->debug("Could not save resized image"); + } +} + +auto LibvipsImage::process_image() -> void +{ + resize_image(); + if (flags->origin_center) { + const double img_width = static_cast<double>(width()) / dims->terminal->font_width; + const double img_height = static_cast<double>(height()) / dims->terminal->font_height; + dims->x -= std::floor(img_width / 2); + dims->y -= std::floor(img_height / 2); + } + + const std::unordered_set<std::string_view> bgra_trifecta = {"x11", "chafa", "wayland"}; + +#ifdef ENABLE_OPENGL + if (flags->use_opengl) { + image = image.flipver(); + } +#endif + + if (bgra_trifecta.contains(flags->output)) { + // alpha channel required + if (!image.has_alpha()) { + const int alpha_value = 255; + image = image.bandjoin(alpha_value); + } + // convert from RGB to BGR + auto bands = image.bandsplit(); + std::swap(bands[0], bands[2]); + image = VImage::bandjoin(bands); + } else if (flags->output == "sixel") { + // sixel expects RGB888 + if (image.has_alpha()) { + image = image.flatten(); + } + } + _size = VIPS_IMAGE_SIZEOF_IMAGE(image.get_image()); + _data.reset(static_cast<unsigned char *>(image.write_to_memory(&_size))); +} diff --git a/src/image/libvips.hpp b/src/image/libvips.hpp new file mode 100644 index 0000000..76cc731 --- /dev/null +++ b/src/image/libvips.hpp @@ -0,0 +1,71 @@ +// Display images inside a terminal +// Copyright (C) 2023 JustKidding +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. + +#ifndef LIBVIPS_IMAGE_H +#define LIBVIPS_IMAGE_H + +#include "image.hpp" +#include "util/ptr.hpp" + +#include <filesystem> +#include <spdlog/spdlog.h> +#include <string> +#include <vips/vips8> + +class LibvipsImage : public Image +{ + public: + LibvipsImage(std::shared_ptr<Dimensions> new_dims, const std::string &filename, bool in_cache); + + [[nodiscard]] auto dimensions() const -> const Dimensions & override; + [[nodiscard]] auto width() const -> int override; + [[nodiscard]] auto height() const -> int override; + [[nodiscard]] auto size() const -> size_t override; + [[nodiscard]] auto data() const -> const unsigned char * override; + [[nodiscard]] auto channels() const -> int override; + + void next_frame() override; + [[nodiscard]] auto frame_delay() const -> int override; + [[nodiscard]] auto is_animated() const -> bool override; + [[nodiscard]] auto filename() const -> std::string override; + + private: + vips::VImage image; + vips::VImage backup; + + c_unique_ptr<unsigned char, g_free> _data; + std::filesystem::path path; + std::shared_ptr<Dimensions> dims; + + std::shared_ptr<Flags> flags; + std::shared_ptr<spdlog::logger> logger; + + uint32_t max_width; + uint32_t max_height; + size_t _size = 0; + + // for animated pictures + int top = 0; + int orig_height; + int npages = 0; + bool is_anim = false; + bool in_cache; + + void process_image(); + void resize_image(); +}; + +#endif diff --git a/src/image/opencv.cpp b/src/image/opencv.cpp new file mode 100644 index 0000000..60c2425 --- /dev/null +++ b/src/image/opencv.cpp @@ -0,0 +1,249 @@ +// Display images inside a terminal +// Copyright (C) 2023 JustKidding +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. + +#include "opencv.hpp" +#include "dimensions.hpp" +#include "flags.hpp" +#include "terminal.hpp" +#include "util.hpp" + +#include <string_view> +#include <unordered_set> + +#include <opencv2/core/ocl.hpp> +#include <opencv2/imgcodecs.hpp> +#include <opencv2/imgproc.hpp> + +enum { + EXIF_ORIENTATION_2 = 2, + EXIF_ORIENTATION_3, + EXIF_ORIENTATION_4, + EXIF_ORIENTATION_5, + EXIF_ORIENTATION_6, + EXIF_ORIENTATION_7, + EXIF_ORIENTATION_8, +}; + +OpencvImage::OpencvImage(std::shared_ptr<Dimensions> new_dims, const std::string &filename, bool in_cache) + : path(filename), + dims(std::move(new_dims)), + max_width(dims->max_wpixels()), + max_height(dims->max_hpixels()), + in_cache(in_cache) +{ + logger = spdlog::get("opencv"); + image = cv::imread(filename, cv::IMREAD_UNCHANGED); + + if (image.empty()) { + logger->warn("unable to read image"); + throw std::runtime_error(""); + } + logger->info("loading file {}", filename); + flags = Flags::instance(); + + rotate_image(); + process_image(); +} + +auto OpencvImage::filename() const -> std::string +{ + return path.string(); +} + +auto OpencvImage::dimensions() const -> const Dimensions & +{ + return *dims; +} + +auto OpencvImage::width() const -> int +{ + return image.cols; +} + +auto OpencvImage::height() const -> int +{ + return image.rows; +} + +auto OpencvImage::size() const -> size_t +{ + return _size; +} + +auto OpencvImage::data() const -> const unsigned char * +{ + return image.data; +} + +auto OpencvImage::channels() const -> int +{ + return image.channels(); +} + +void OpencvImage::wayland_processing() +{ + if (flags->output != "wayland") { + return; + } +} + +void OpencvImage::rotate_image() +{ + const auto rotation = util::read_exif_rotation(path); + if (!rotation.has_value()) { + return; + } + const auto value = rotation.value(); + + // kudos https://jdhao.github.io/2019/07/31/image_rotation_exif_info/ + switch (value) { + case EXIF_ORIENTATION_2: + cv::flip(image, image, 1); + break; + case EXIF_ORIENTATION_3: + cv::flip(image, image, -1); + break; + case EXIF_ORIENTATION_4: + cv::flip(image, image, 0); + break; + case EXIF_ORIENTATION_5: + cv::rotate(image, image, cv::ROTATE_90_CLOCKWISE); + cv::flip(image, image, 1); + break; + case EXIF_ORIENTATION_6: + cv::rotate(image, image, cv::ROTATE_90_CLOCKWISE); + break; + case EXIF_ORIENTATION_7: + cv::rotate(image, image, cv::ROTATE_90_COUNTERCLOCKWISE); + cv::flip(image, image, 1); + break; + case EXIF_ORIENTATION_8: + cv::rotate(image, image, cv::ROTATE_90_COUNTERCLOCKWISE); + break; + default: + break; + } +} + +// only use opencl if required +auto OpencvImage::resize_image() -> void +{ + if (in_cache) { + return; + } + const auto [new_width, new_height] = get_new_sizes(max_width, max_height, dims->scaler, flags->scale_factor); + if (new_width <= 0 && new_height <= 0) { + // ensure width and height are pair + if (flags->needs_scaling) { + const auto curw = width(); + const auto curh = height(); + if ((curw % 2) != 0 || (curh % 2) != 0) { + resize_image_helper(image, util::round_up(curw, flags->scale_factor), + util::round_up(curh, flags->scale_factor)); + } + } + return; + } + + const auto opencl_ctx = cv::ocl::Context::getDefault(); + opencl_available = opencl_ctx.ptr() != nullptr; + + if (opencl_available) { + logger->debug("OpenCL is available"); + image.copyTo(uimage); + resize_image_helper(uimage, new_width, new_height); + uimage.copyTo(image); + } else { + resize_image_helper(image, new_width, new_height); + } +} + +void OpencvImage::resize_image_helper(cv::InputOutputArray &mat, int new_width, int new_height) +{ + logger->debug("Resizing image"); + cv::resize(mat, mat, cv::Size(new_width, new_height), 0, 0, cv::INTER_AREA); + + if (flags->no_cache) { + logger->debug("Caching is disabled"); + return; + } + + const auto save_location = util::get_cache_file_save_location(path); + try { + cv::imwrite(save_location, mat); + logger->debug("Saved resized image"); + } catch (const cv::Exception &ex) { + logger->error("Could not save image"); + } +} + +void OpencvImage::process_image() +{ + resize_image(); + if (flags->origin_center) { + const double img_width = static_cast<double>(width()) / dims->terminal->font_width; + const double img_height = static_cast<double>(height()) / dims->terminal->font_height; + dims->x -= std::floor(img_width / 2); + dims->y -= std::floor(img_height / 2); + } + + const std::unordered_set<std::string_view> bgra_trifecta = {"x11", "chafa", "wayland"}; + + if (image.depth() == CV_16U) { + const float alpha = 0.00390625; // 1 / 256 + image.convertTo(image, CV_8U, alpha); + } + + if (image.channels() == 4) { + // premultiply alpha + image.forEach<cv::Vec4b>([](cv::Vec4b &pix, const int *) { + const uint8_t alpha = pix[3]; + const uint8_t div = 255; + pix[0] = (pix[0] * alpha) / div; + pix[1] = (pix[1] * alpha) / div; + pix[2] = (pix[2] * alpha) / div; + }); + } + +#ifdef ENABLE_OPENGL + if (flags->use_opengl) { + cv::flip(image, image, 0); + } +#endif + + if (image.channels() == 1) { + cv::cvtColor(image, image, cv::COLOR_GRAY2BGRA); + } + + if (bgra_trifecta.contains(flags->output)) { + if (image.channels() == 3) { + cv::cvtColor(image, image, cv::COLOR_BGR2BGRA); + } + } else if (flags->output == "kitty") { + if (image.channels() == 4) { + cv::cvtColor(image, image, cv::COLOR_BGRA2RGBA); + } else { + cv::cvtColor(image, image, cv::COLOR_BGR2RGB); + } + } else if (flags->output == "sixel") { + if (image.channels() == 4) { + cv::cvtColor(image, image, cv::COLOR_BGRA2RGB); + } else { + cv::cvtColor(image, image, cv::COLOR_BGR2RGB); + } + } + _size = image.total() * image.elemSize(); +} diff --git a/src/image/opencv.hpp b/src/image/opencv.hpp new file mode 100644 index 0000000..6214abc --- /dev/null +++ b/src/image/opencv.hpp @@ -0,0 +1,68 @@ +// Display images inside a terminal +// Copyright (C) 2023 JustKidding +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. + +#ifndef OPENCV_IMAGE_H +#define OPENCV_IMAGE_H + +#include "image.hpp" + +#include <filesystem> +#include <opencv2/core.hpp> +#include <spdlog/spdlog.h> +#include <string> + +namespace fs = std::filesystem; + +class OpencvImage : public Image +{ + public: + OpencvImage(std::shared_ptr<Dimensions> new_dims, const std::string &filename, bool in_cache); + ~OpencvImage() override = default; + + [[nodiscard]] auto dimensions() const -> const Dimensions & override; + [[nodiscard]] auto width() const -> int override; + [[nodiscard]] auto height() const -> int override; + [[nodiscard]] auto size() const -> size_t override; + [[nodiscard]] auto data() const -> const unsigned char * override; + [[nodiscard]] auto channels() const -> int override; + + [[nodiscard]] auto filename() const -> std::string override; + + private: + cv::Mat image; + cv::UMat uimage; + + fs::path path; + std::shared_ptr<Dimensions> dims; + + uint64_t _size = 0; + uint32_t max_width; + uint32_t max_height; + bool in_cache; + bool opencl_available = false; + + std::shared_ptr<spdlog::logger> logger; + std::shared_ptr<Flags> flags; + + void process_image(); + void resize_image(); + void resize_image_helper(cv::InputOutputArray &mat, int new_width, int new_height); + + void rotate_image(); + void wayland_processing(); +}; + +#endif |
