// 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 .
#include "opencv.hpp"
#include "dimensions.hpp"
#include "flags.hpp"
#include "terminal.hpp"
#include "util.hpp"
#include
#include
#include
#include
#include
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 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(width()) / dims->terminal->font_width;
const double img_height = static_cast(height()) / dims->terminal->font_height;
dims->x -= std::floor(img_width / 2);
dims->y -= std::floor(img_height / 2);
}
const std::unordered_set 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 &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();
}