#include "../include/AsyncImageLoader.hpp" #include "../include/DownloadUtils.hpp" #include "../include/ImageUtils.hpp" #include "../include/Scale.hpp" #include "../include/SfmlFixes.hpp" #include "../external/hash-library/sha256.h" #include namespace QuickMedia { // Linear interpolation // TODO: Is this implementation ok? it always uses +1 offset for interpolation but if we were to resize an image with near 1x1 scaling // then it would be slightly blurry static void copy_resize(const sf::Image &source, sf::Image &destination, sf::Vector2u destination_size) { const sf::Vector2u source_size = source.getSize(); if(source_size.x == 0 || source_size.y == 0 || destination_size.x == 0 || destination_size.y == 0) return; //float width_ratio = (float)source_size.x / (float)destination_size.x; //float height_ratio = (float)source_size.y / (float)destination_size.y; const sf::Uint8 *source_pixels = source.getPixelsPtr(); // TODO: Remove this somehow. Right now we need to allocate this and also allocate the same array in the destination image sf::Uint32 *destination_pixels = new sf::Uint32[destination_size.x * destination_size.y]; sf::Uint32 *destination_pixel = destination_pixels; for(unsigned int y = 0; y < destination_size.y; ++y) { for(unsigned int x = 0; x < destination_size.x; ++x) { int scaled_x_start = ((float)x / (float)destination_size.x) * source_size.x; int scaled_y_start = ((float)y / (float)destination_size.y) * source_size.y; int scaled_x_end = ((float)(x + 1) / (float)destination_size.x) * source_size.x; int scaled_y_end = ((float)(y + 1) / (float)destination_size.y) * source_size.y; if(scaled_x_end > (int)source_size.x - 1) scaled_x_end = source_size.x - 1; if(scaled_y_end > (int)source_size.y - 1) scaled_y_end = source_size.y - 1; //float scaled_x = x * width_ratio; //float scaled_y = y * height_ratio; //sf::Uint32 *source_pixel = (sf::Uint32*)(source_pixels + (int)(scaled_x + scaled_y * source_size.x) * 4); sf::Uint32 sum_red = 0; sf::Uint32 sum_green = 0; sf::Uint32 sum_blue = 0; sf::Uint32 sum_alpha = 0; sf::Uint32 num_colors = (scaled_x_end - scaled_x_start) * (scaled_y_end - scaled_y_start); for(int yy = scaled_y_start; yy < scaled_y_end; ++yy) { for(int xx = scaled_x_start; xx < scaled_x_end; ++xx) { sf::Uint32 *source_pixel = (sf::Uint32*)(source_pixels + (xx + yy * source_size.x) * 4); sum_red += (*source_pixel >> 24); sum_green += ((*source_pixel >> 16) & 0xFF); sum_blue += ((*source_pixel >> 8) & 0xFF); sum_alpha += (*source_pixel & 0xFF); } } sum_red /= num_colors; sum_green /= num_colors; sum_blue /= num_colors; sum_alpha /= num_colors; *destination_pixel = (sum_red << 24) | (sum_green << 16) | (sum_blue << 8) | sum_alpha; ++destination_pixel; } } destination.create(destination_size.x, destination_size.y, (sf::Uint8*)destination_pixels); delete []destination_pixels; } static bool save_image_as_thumbnail_atomic(const sf::Image &image, const Path &thumbnail_path, const char *ext) { Path tmp_path = thumbnail_path; tmp_path.append(".tmp"); const char *thumbnail_path_ext = thumbnail_path.ext(); if(is_image_ext(ext)) tmp_path.append(ext); else if(is_image_ext(thumbnail_path_ext)) tmp_path.append(thumbnail_path_ext); else tmp_path.append(".png"); return image.saveToFile(tmp_path.data) && (rename(tmp_path.data.c_str(), thumbnail_path.data.c_str()) == 0); } // Returns empty string if no extension static const char* get_ext(const std::string &path) { size_t slash_index = path.rfind('/'); size_t index = path.rfind('.'); if(index != std::string::npos && (slash_index == std::string::npos || index > slash_index)) return path.c_str() + index; return ""; } static void create_thumbnail_if_thumbnail_smaller_than_image(const std::string &original_url, const Path &thumbnail_path, ThumbnailData *thumbnail_data, sf::Vector2i resize_target_size) { sf::Vector2u new_image_size = clamp_to_size(thumbnail_data->image->getSize(), sf::Vector2u(resize_target_size.x, resize_target_size.y)); if(new_image_size.x < thumbnail_data->image->getSize().x || new_image_size.y < thumbnail_data->image->getSize().y) { auto destination_image = std::make_unique(); copy_resize(*thumbnail_data->image, *destination_image, new_image_size); thumbnail_data->image = std::move(destination_image); save_image_as_thumbnail_atomic(*thumbnail_data->image, thumbnail_path, get_ext(original_url)); thumbnail_data->loading_state = LoadingState::FINISHED_LOADING; } } AsyncImageLoader::AsyncImageLoader() { for(int i = 0; i < NUM_IMAGE_LOAD_THREADS; ++i) { loading_image[i] = false; } load_image_thread = std::thread([this]{ std::optional thumbnail_load_data_opt; while(true) { thumbnail_load_data_opt = image_load_queue.pop_wait(); if(!thumbnail_load_data_opt) break; ThumbnailLoadData &thumbnail_load_data = thumbnail_load_data_opt.value(); thumbnail_load_data.thumbnail_data->image = std::make_unique(); if(load_image_from_file(*thumbnail_load_data.thumbnail_data->image, thumbnail_load_data.thumbnail_path.data)) { fprintf(stderr, "Loaded %s from thumbnail cache\n", thumbnail_load_data.path.data.c_str()); thumbnail_load_data.thumbnail_data->loading_state = LoadingState::FINISHED_LOADING; continue; } if(thumbnail_load_data.local) { if(load_image_from_file(*thumbnail_load_data.thumbnail_data->image, thumbnail_load_data.path.data) && thumbnail_load_data.resize_target_size.x != 0 && thumbnail_load_data.resize_target_size.y != 0) { create_thumbnail_if_thumbnail_smaller_than_image(thumbnail_load_data.path.data, thumbnail_load_data.thumbnail_path, thumbnail_load_data.thumbnail_data.get(), thumbnail_load_data.resize_target_size); } thumbnail_load_data.thumbnail_data->loading_state = LoadingState::FINISHED_LOADING; } else { thumbnail_load_data.thumbnail_data->loading_state = LoadingState::FINISHED_LOADING; } } }); } AsyncImageLoader::~AsyncImageLoader() { image_load_queue.close(); if(load_image_thread.joinable()) load_image_thread.join(); // TODO: Find a way to kill the threads instead. We need to do this right now because creating a new thread before the last one has died causes a crash for(size_t i = 0; i < NUM_IMAGE_LOAD_THREADS; ++i) { if(download_image_thread[i].joinable()) download_image_thread[i].join(); } } void AsyncImageLoader::load_thumbnail(const std::string &url, bool local, sf::Vector2i resize_target_size, bool use_tor, std::shared_ptr thumbnail_data) { if(thumbnail_data->loading_state != LoadingState::NOT_LOADED) return; if(url.empty()) { thumbnail_data->image = std::make_unique(); thumbnail_data->loading_state = LoadingState::FINISHED_LOADING; return; } SHA256 sha256; sha256.add(url.data(), url.size()); Path thumbnail_path = get_cache_dir().join("thumbnails").join(sha256.getHash()); if(get_file_type(thumbnail_path) == FileType::REGULAR) { thumbnail_data->loading_state = LoadingState::LOADING; image_load_queue.push({ url, thumbnail_path, local, thumbnail_data, resize_target_size }); return; } else if(local && get_file_type(url) == FileType::REGULAR) { thumbnail_data->loading_state = LoadingState::LOADING; image_load_queue.push({ url, thumbnail_path, true, thumbnail_data, resize_target_size }); return; } int free_index = get_free_load_index(); if(free_index == -1) return; loading_image[free_index] = true; thumbnail_data->loading_state = LoadingState::LOADING; if(download_image_thread[free_index].joinable()) download_image_thread[free_index].join(); // TODO: Keep the thread running and use conditional variable instead to sleep until a new image should be loaded. Same in ImageViewer. download_image_thread[free_index] = std::thread([this, free_index, thumbnail_path, url, local, resize_target_size, thumbnail_data, use_tor]() mutable { thumbnail_data->image = std::make_unique(); if(load_image_from_file(*thumbnail_data->image, thumbnail_path.data)) { fprintf(stderr, "Loaded %s from thumbnail cache\n", url.c_str()); thumbnail_data->loading_state = LoadingState::FINISHED_LOADING; loading_image[free_index] = false; return; } else { if(local) { if(!load_image_from_file(*thumbnail_data->image, url)) { thumbnail_data->loading_state = LoadingState::FINISHED_LOADING; loading_image[free_index] = false; return; } } else { // Use the same path as the thumbnail path, since it wont be overwritten if the image is smaller than the thumbnail target size if(download_to_file(url, thumbnail_path.data, {}, use_tor, true) != DownloadResult::OK || !load_image_from_file(*thumbnail_data->image, thumbnail_path.data)) { thumbnail_data->loading_state = LoadingState::FINISHED_LOADING; loading_image[free_index] = false; return; } } } if(resize_target_size.x != 0 && resize_target_size.y != 0) create_thumbnail_if_thumbnail_smaller_than_image(url, thumbnail_path, thumbnail_data.get(), resize_target_size); thumbnail_data->loading_state = LoadingState::FINISHED_LOADING; loading_image[free_index] = false; return; }); } int AsyncImageLoader::get_free_load_index() const { for(int i = 0; i < NUM_IMAGE_LOAD_THREADS; ++i) { if(!loading_image[i]) return i; } return -1; } }