aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--README.md4
-rw-r--r--include/ImageUtils.hpp8
-rw-r--r--include/ImageViewer.hpp16
-rw-r--r--include/QuickMedia.hpp15
-rw-r--r--src/ImageUtils.cpp114
-rw-r--r--src/ImageViewer.cpp69
-rw-r--r--src/QuickMedia.cpp119
7 files changed, 315 insertions, 30 deletions
diff --git a/README.md b/README.md
index 5d998c9..998886a 100644
--- a/README.md
+++ b/README.md
@@ -13,6 +13,7 @@ OPTIONS:
plugin The plugin to use. Should be either 4chan, manganelo, mangatown, mangadex, youtube or dmenu
--tor Use tor. Disabled by default
--use-system-mpv-config Use system mpv config instead of no config. Disabled by default
+ --upscale-images Upscale low-resolution manga pages using waifu2x-ncnn-vulkan. Disabled by default
-p Change the placeholder text for dmenu
EXAMPLES:
QuickMedia manganelo
@@ -60,7 +61,8 @@ See project.conf \[dependencies].
`youtube-dl` needs to be installed to play videos from youtube.\
`notify-send` needs to be installed to show notifications (on Linux and other systems that uses d-bus notification system).\
`torsocks` needs to be installed when using the `--tor` option.\
-[automedia](https://git.dec05eba.com/AutoMedia/) needs to be installed when tracking manga with `Ctrl + T`.
+[automedia](https://git.dec05eba.com/AutoMedia/) needs to be installed when tracking manga with `Ctrl + T`.\
+`waifu2x-ncnn-vulkan` needs to be installed when using the `--upscale-images` option.
# Screenshots
## Youtube search
![](https://www.dec05eba.com/images/youtube-search.png)
diff --git a/include/ImageUtils.hpp b/include/ImageUtils.hpp
new file mode 100644
index 0000000..f0670d6
--- /dev/null
+++ b/include/ImageUtils.hpp
@@ -0,0 +1,8 @@
+#pragma once
+
+#include "Path.hpp"
+
+namespace QuickMedia {
+ // Works with jpg, png and gif files
+ bool image_get_resolution(const Path &path, int *width, int *height);
+} \ No newline at end of file
diff --git a/include/ImageViewer.hpp b/include/ImageViewer.hpp
index 7fe4921..93e7d7c 100644
--- a/include/ImageViewer.hpp
+++ b/include/ImageViewer.hpp
@@ -9,14 +9,22 @@
#include <SFML/Graphics/Font.hpp>
#include <SFML/Graphics/Text.hpp>
#include <SFML/System/Clock.hpp>
+#include <thread>
namespace QuickMedia {
class Manga;
+ enum class ImageStatus {
+ WAITING,
+ LOADING,
+ FAILED_TO_LOAD,
+ LOADED
+ };
+
struct ImageData {
sf::Texture texture;
sf::Sprite sprite;
- bool failed_to_load_image;
+ ImageStatus image_status;
bool visible_on_screen;
};
@@ -39,6 +47,7 @@ namespace QuickMedia {
int get_focused_page() const;
int get_num_pages() const { return num_pages; }
private:
+ void load_image_async(const Path &path, std::shared_ptr<ImageData> image_data, int page);
bool render_page(sf::RenderWindow &window, int page, double offset_y);
sf::Vector2<double> get_page_size(int page);
private:
@@ -61,7 +70,7 @@ namespace QuickMedia {
sf::Clock frame_timer;
sf::Text page_text;
- std::vector<std::unique_ptr<ImageData>> image_data;
+ std::vector<std::shared_ptr<ImageData>> image_data;
std::vector<PageSize> page_size;
sf::Vector2<double> window_size;
@@ -77,5 +86,8 @@ namespace QuickMedia {
bool up_pressed = false;
bool down_pressed = false;
+
+ std::thread image_loader_thread;
+ bool loading_image = false;
};
} \ No newline at end of file
diff --git a/include/QuickMedia.hpp b/include/QuickMedia.hpp
index b2c499c..d3bff47 100644
--- a/include/QuickMedia.hpp
+++ b/include/QuickMedia.hpp
@@ -11,7 +11,11 @@
#include <json/value.h>
#include <unordered_set>
#include <future>
+#include <thread>
+#include <mutex>
+#include <condition_variable>
#include <stack>
+#include <deque>
#include <X11/Xlib.h>
#include <X11/Xatom.h>
@@ -23,6 +27,11 @@ namespace QuickMedia {
SINGLE,
SCROLL
};
+
+ struct CopyOp {
+ Path source;
+ Path destination;
+ };
class Program {
public:
@@ -94,12 +103,18 @@ namespace QuickMedia {
std::future<BodyItems> search_suggestion_future;
std::future<std::string> autocomplete_future;
std::future<void> image_download_future;
+ std::thread image_upscale_thead;
+ std::mutex image_upscale_mutex;
+ std::deque<CopyOp> images_to_upscale;
+ std::condition_variable image_upscale_cv;
std::string downloading_chapter_url;
bool image_download_cancel = false;
int exit_code = 0;
std::string resources_root;
bool use_tor = false;
bool use_system_mpv_config = false;
+ bool upscale_images = false;
+ bool running = false;
// TODO: Save this to config file when switching modes
ImageViewMode image_view_mode = ImageViewMode::SINGLE;
Body *related_media_body;
diff --git a/src/ImageUtils.cpp b/src/ImageUtils.cpp
new file mode 100644
index 0000000..d160450
--- /dev/null
+++ b/src/ImageUtils.cpp
@@ -0,0 +1,114 @@
+#include "../include/ImageUtils.hpp"
+#include <string.h>
+#include <arpa/inet.h>
+
+namespace QuickMedia {
+ static bool gif_get_size(unsigned char *data, size_t data_size, int *width, int *height) {
+ if(data_size >= 10 && memcmp(data, "GIF", 3) == 0) {
+ *width = ((int)data[7] << 8) | data[6];
+ *height = ((int)data[9] << 8) | data[8];
+ return true;
+ }
+ return false;
+ }
+
+ static bool png_get_size(unsigned char *data, size_t data_size, int *width, int *height) {
+ if(data_size >= 24 && memcmp(data, "\x89\x50\x4E\x47\x0D\x0A\x1A\x0A", 8) == 0) {
+ memcpy(width, data + 16, sizeof(int));
+ memcpy(height, data + 16 + sizeof(int), sizeof(int));
+ *width = htonl(*width);
+ *height = htonl(*height);
+ return true;
+ }
+ return false;
+ }
+
+ static bool is_cpu_little_endian() {
+ unsigned short i;
+ memcpy(&i, "LE", sizeof(i));
+ return (i & 0xF) == 'E';
+ }
+
+ static unsigned short read_uint16(unsigned char *data, bool cpu_little_endian, bool file_little_endian) {
+ unsigned short result;
+ memcpy(&result, data, sizeof(result));
+ if(cpu_little_endian != file_little_endian)
+ result = __builtin_bswap16(result);
+ return result;
+ }
+
+ static unsigned int read_uint32(unsigned char *data, bool cpu_little_endian, bool file_little_endian) {
+ unsigned int result;
+ memcpy(&result, data, sizeof(result));
+ if(cpu_little_endian != file_little_endian)
+ result = __builtin_bswap32(result);
+ return result;
+ }
+#if 0
+ static bool tiff_get_size(unsigned char *data, size_t data_size, int *width, int *height) {
+ if(data_size < 8)
+ return false;
+
+ bool cpu_little_endian = is_cpu_little_endian();
+ bool file_little_endian = true;
+ if(memcmp(data, "II", 2) == 0)
+ file_little_endian = true;
+ else if(memcmp(data, "MM", 2) == 0)
+ file_little_endian = false;
+ else
+ return false;
+
+ unsigned short id = read_uint16(data, cpu_little_endian, file_little_endian);
+ if(id != 42)
+ return false;
+
+ unsigned int offset_to_ifd = read_uint32(data, cpu_little_endian, file_little_endian);
+ if(offset_to_ifd )
+ }
+#endif
+
+ static bool jpeg_get_size(unsigned char *data, size_t data_size, int *width, int *height) {
+ if(data_size < 11 || memcmp(data, "\xFF\xD8\xFF\xE0", 4) != 0)
+ return false;
+
+ unsigned short block_length = ((int)data[4] << 8) | data[5];
+ if(memcmp(data + 6, "JFIF\0", 5) != 0)
+ return false;
+
+ size_t index = 4;
+ while(index < data_size) {
+ index += block_length;
+ if(index + 1 >= data_size) return false;
+ if(data[index] != 0xFF) return false; // Check if start of block
+ if((data[index + 1] == 0xC0 || data[index + 1] == 0xC2) && index + 8 < data_size) { // Start of frame marker
+ *height = ((int)data[index + 5] << 8) | data[index + 6];
+ *width = ((int)data[index + 7] << 8) | data[index + 8];
+ return true;
+ } else {
+ if(index + 2 >= data_size) return false;
+ index += 2;
+ block_length = ((int)data[index] << 8) | data[index + 1];
+ }
+ }
+
+ return false;
+ }
+
+ bool image_get_resolution(const Path &path, int *width, int *height) {
+ FILE *file = fopen(path.data.c_str(), "rb");
+ if(!file)
+ return false;
+
+ unsigned char data[512];
+ size_t bytes_read = fread(data, 1, sizeof(data), file);
+ fclose(file);
+
+ if(png_get_size(data, bytes_read, width, height))
+ return true;
+ else if(gif_get_size(data, bytes_read, width, height))
+ return true;
+ else if(jpeg_get_size(data, bytes_read, width, height))
+ return true;
+ return false;
+ }
+} \ No newline at end of file
diff --git a/src/ImageViewer.cpp b/src/ImageViewer.cpp
index fc31274..86e7138 100644
--- a/src/ImageViewer.cpp
+++ b/src/ImageViewer.cpp
@@ -34,12 +34,34 @@ namespace QuickMedia {
has_size_vertical_cursor = size_vertical_cursor.loadFromSystem(sf::Cursor::SizeVertical);
}
+ void ImageViewer::load_image_async(const Path &path, std::shared_ptr<ImageData> image_data, int page) {
+ image_data->image_status = ImageStatus::LOADING;
+ image_data->texture.setSmooth(true);
+ assert(!loading_image);
+ loading_image = true;
+ image_loader_thread = std::thread([this, image_data, path, page]() {
+ std::string image_data_str;
+ if(file_get_content(path, image_data_str) == 0) {
+ if(image_data->texture.loadFromMemory(image_data_str.data(), image_data_str.size())) {
+ image_data->sprite.setTexture(image_data->texture, true);
+ page_size[page].size = get_page_size(page);
+ page_size[page].loaded = true;
+ image_data->image_status = ImageStatus::LOADED;
+ } else {
+ image_data->image_status = ImageStatus::FAILED_TO_LOAD;
+ }
+ }
+ loading_image = false;
+ });
+ image_loader_thread.detach();
+ }
+
bool ImageViewer::render_page(sf::RenderWindow &window, int page, double offset_y) {
if(page < 0 || page >= (int)image_data.size())
return false;
const sf::Vector2<double> image_size = get_page_size(page);
- std::unique_ptr<ImageData> &page_image_data = image_data[page];
+ std::shared_ptr<ImageData> &page_image_data = image_data[page];
sf::Vector2<double> render_pos(std::floor(window_size.x * 0.5 - image_size.x * 0.5), - image_size.y * 0.5 + scroll + offset_y);
if(render_pos.y + image_size.y <= 0.0 || render_pos.y >= window_size.y) {
if(page_image_data)
@@ -58,8 +80,26 @@ namespace QuickMedia {
}
if(page_image_data) {
- if(page_image_data->failed_to_load_image) {
- sf::Text error_message("Failed to load image for page " + std::to_string(1 + page), *font, 30);
+ if(page_image_data->image_status == ImageStatus::LOADED) {
+ page_image_data->sprite.setPosition(render_pos.x, render_pos.y);
+ window.draw(page_image_data->sprite);
+ } else {
+ std::string page_str = std::to_string(1 + page);
+ std::string msg;
+ if(page_image_data->image_status == ImageStatus::WAITING) {
+ if(!loading_image) {
+ Path image_path = chapter_cache_dir;
+ image_path.join(page_str);
+ load_image_async(image_path, page_image_data, page);
+ }
+ msg = "Loading image for page " + page_str;
+ } else if(page_image_data->image_status == ImageStatus::LOADING) {
+ msg = "Loading image for page " + page_str;
+ } else if(page_image_data->image_status == ImageStatus::FAILED_TO_LOAD) {
+ msg = "Failed to load image for page " + page_str;
+ }
+
+ sf::Text error_message(std::move(msg), *font, 30);
auto text_bounds = error_message.getLocalBounds();
error_message.setFillColor(sf::Color::Black);
sf::Vector2<double> render_pos_text(std::floor(window_size.x * 0.5 - text_bounds.width * 0.5), - text_bounds.height * 0.5 + scroll + offset_y);
@@ -74,9 +114,6 @@ namespace QuickMedia {
error_message.setPosition(render_pos_text.x, render_pos_text.y);
window.draw(error_message);
- } else {
- page_image_data->sprite.setPosition(render_pos.x, render_pos.y);
- window.draw(page_image_data->sprite);
}
} else {
std::string page_str = std::to_string(1 + page);
@@ -103,23 +140,17 @@ namespace QuickMedia {
// TODO: Make image loading asynchronous
if(get_file_type(image_path) == FileType::REGULAR) {
fprintf(stderr, "ImageViewer: Loaded page %d\n", 1 + page);
- page_image_data = std::make_unique<ImageData>();
+
+ page_image_data = std::make_shared<ImageData>();
page_image_data->visible_on_screen = true;
+
std::string image_data;
if(file_get_content(image_path, image_data) == 0) {
- if(page_image_data->texture.loadFromMemory(image_data.data(), image_data.size())) {
- page_image_data->texture.setSmooth(true);
- page_image_data->sprite.setTexture(page_image_data->texture, true);
- //image_texture.generateMipmap();
- page_image_data->failed_to_load_image = false;
- page_size[page].size = get_page_size(page);
- page_size[page].loaded = true;
- } else {
- page_image_data->failed_to_load_image = true;
- }
+ page_image_data->image_status = ImageStatus::WAITING;
+ page_image_data->texture.setSmooth(true);
} else {
show_notification("Manga", "Failed to load image for page " + page_str + ". Image filepath: " + image_path.data, Urgency::CRITICAL);
- page_image_data->failed_to_load_image = true;
+ page_image_data->image_status = ImageStatus::FAILED_TO_LOAD;
}
}
@@ -289,7 +320,7 @@ namespace QuickMedia {
for(auto &page_data : image_data) {
if(page_data && !page_data->visible_on_screen) {
fprintf(stderr, "ImageViewer: Unloaded page %d\n", 1 + i);
- page_data.reset();
+ page_data = nullptr;
}
++i;
}
diff --git a/src/QuickMedia.cpp b/src/QuickMedia.cpp
index 75bc027..6db58c9 100644
--- a/src/QuickMedia.cpp
+++ b/src/QuickMedia.cpp
@@ -13,6 +13,7 @@
#include "../include/GoogleCaptcha.hpp"
#include "../include/Notification.hpp"
#include "../include/ImageViewer.hpp"
+#include "../include/ImageUtils.hpp"
#include <cppcodec/base64_rfc4648.hpp>
#include <SFML/Graphics/RectangleShape.hpp>
@@ -68,6 +69,11 @@ static int get_monitor_max_hz(Display *display) {
return 60;
}
+static void get_screen_resolution(Display *display, int *width, int *height) {
+ *width = DefaultScreenOfDisplay(display)->width;
+ *height = DefaultScreenOfDisplay(display)->height;
+}
+
static bool has_gl_ext(Display *disp, const char *ext) {
const char *extensions = glXQueryExtensionsString(disp, DefaultScreen(disp));
if(!extensions)
@@ -193,6 +199,14 @@ namespace QuickMedia {
}
Program::~Program() {
+ running = false;
+ if(upscale_images) {
+ {
+ std::unique_lock<std::mutex> lock(image_upscale_mutex);
+ image_upscale_cv.notify_one();
+ }
+ image_upscale_thead.join();
+ }
delete related_media_body;
delete body;
delete current_plugin;
@@ -223,6 +237,7 @@ namespace QuickMedia {
fprintf(stderr, " plugin The plugin to use. Should be either 4chan, manganelo, mangatown, mangadex, pornhub, youtube or dmenu\n");
fprintf(stderr, " --tor Use tor. Disabled by default\n");
fprintf(stderr, " --use-system-mpv-config Use system mpv config instead of no config. Disabled by default\n");
+ fprintf(stderr, " --upscale-images Upscale low-resolution manga pages using waifu2x-ncnn-vulkan. Disabled by default\n");
fprintf(stderr, " -p Change the placeholder text for dmenu\n");
fprintf(stderr, "EXAMPLES:\n");
fprintf(stderr, "QuickMedia manganelo\n");
@@ -292,6 +307,8 @@ namespace QuickMedia {
use_tor = true;
} else if(strcmp(argv[i], "--use-system-mpv-config") == 0) {
use_system_mpv_config = true;
+ } else if(strcmp(argv[i], "--upscale-images") == 0) {
+ upscale_images = true;
} else if(strcmp(argv[i], "-p") == 0) {
if(i < argc - 1) {
search_placeholder = argv[i + 1];
@@ -309,6 +326,51 @@ namespace QuickMedia {
return -2;
}
+ if(upscale_images) {
+ if(!current_plugin->is_manga()) {
+ fprintf(stderr, "Option --upscale-images is only valid for manganelo, mangatown and mangadex\n");
+ return -2;
+ }
+
+ if(!is_program_executable_by_name("waifu2x-ncnn-vulkan")) {
+ fprintf(stderr, "waifu2x-ncnn-vulkan needs to be installed (and accessible from PATH environment variable) when using the --upscale-images option\n");
+ return -2;
+ }
+
+ running = true;
+ image_upscale_thead = std::thread([this]{
+ CopyOp copy_op;
+ while(running) {
+ {
+ std::unique_lock<std::mutex> lock(image_upscale_mutex);
+ while(images_to_upscale.empty() && running) image_upscale_cv.wait(lock);
+ if(!running)
+ break;
+ copy_op = images_to_upscale.front();
+ images_to_upscale.pop_front();
+ }
+
+ Path tmp_file = copy_op.source;
+ tmp_file.append(".tmp.jpg");
+
+ fprintf(stderr, "Upscaling %s\n", copy_op.source.data.c_str());
+ const char *args[] = { "waifu2x-ncnn-vulkan", "-i", copy_op.source.data.c_str(), "-o", tmp_file.data.c_str(), nullptr };
+ if(exec_program(args, nullptr, nullptr) != 0) {
+ fprintf(stderr, "Warning: failed to upscale %s with waifu2x-ncnn-vulkan\n", copy_op.source.data.c_str());
+ // No conversion, but we need the file to have the destination name to see that the operation completed (and read it)
+ if(rename(copy_op.source.data.c_str(), copy_op.destination.data.c_str()) != 0)
+ perror(tmp_file.data.c_str());
+ continue;
+ }
+
+ if(rename(tmp_file.data.c_str(), copy_op.destination.data.c_str()) != 0)
+ perror(tmp_file.data.c_str());
+ }
+ });
+ } else {
+ running = true;
+ }
+
current_plugin->use_tor = use_tor;
window.setTitle("QuickMedia - " + current_plugin->name);
@@ -1828,6 +1890,7 @@ namespace QuickMedia {
}
}
+ // TODO: Cancel download when navigating to another non-manga page
void Program::download_chapter_images_if_needed(Manga *image_plugin) {
if(downloading_chapter_url == images_url)
return;
@@ -1910,10 +1973,35 @@ namespace QuickMedia {
return false;
}
- if(rename(image_filepath_tmp.data.c_str(), image_filepath.data.c_str()) != 0) {
- perror("rename");
- show_notification("Storage", "Failed to save image to file: " + image_filepath_tmp.data, Urgency::CRITICAL);
- return false;
+ bool rename_immediately = true;
+ if(upscale_images) {
+ int screen_width, screen_height;
+ get_screen_resolution(disp, &screen_width, &screen_height);
+
+ int image_width, image_height;
+ if(image_get_resolution(image_filepath_tmp, &image_width, &image_height)) {
+ if(image_height < screen_height * 0.75) {
+ rename_immediately = false;
+ CopyOp copy_op;
+ copy_op.source = image_filepath_tmp;
+ copy_op.destination = image_filepath;
+ std::unique_lock<std::mutex> lock(image_upscale_mutex);
+ images_to_upscale.push_back(std::move(copy_op));
+ image_upscale_cv.notify_one();
+ } else {
+ fprintf(stderr, "Info: not upscaling %s because the file is already large on your monitor (screen height: %d, image height: %d)\n", image_filepath_tmp.data.c_str(), screen_height, image_height);
+ }
+ } else {
+ fprintf(stderr, "Warning: failed to upscale %s because QuickMedia failed to recognize the resolution of the image\n", image_filepath_tmp.data.c_str());
+ }
+ }
+
+ if(rename_immediately) {
+ if(rename(image_filepath_tmp.data.c_str(), image_filepath.data.c_str()) != 0) {
+ perror(image_filepath_tmp.data.c_str());
+ show_notification("Storage", "Failed to save image to file: " + image_filepath.data, Urgency::CRITICAL);
+ return false;
+ }
}
return true;
@@ -1922,6 +2010,7 @@ namespace QuickMedia {
}
void Program::image_page() {
+ image_download_cancel = false;
search_bar->onTextUpdateCallback = nullptr;
search_bar->onTextSubmitCallback = nullptr;
@@ -2024,25 +2113,25 @@ namespace QuickMedia {
if(event.key.code == sf::Keyboard::Up) {
if(image_index > 0) {
--image_index;
- return;
+ goto end_of_images_page;
} else if(image_index == 0 && body->get_selected_item() < (int)body->items.size() - 1) {
// TODO: Make this work if the list is sorted differently than from newest to oldest.
body->filter_search_fuzzy("");
body->select_next_item();
select_episode(body->items[body->get_selected_item()].get(), true);
image_index = 99999; // Start at the page that shows we are at the end of the chapter
- return;
+ goto end_of_images_page;
}
} else if(event.key.code == sf::Keyboard::Down) {
if(image_index < num_images) {
++image_index;
- return;
+ goto end_of_images_page;
} else if(image_index == num_images && body->get_selected_item() > 0) {
// TODO: Make this work if the list is sorted differently than from newest to oldest.
body->filter_search_fuzzy("");
body->select_previous_item();
select_episode(body->items[body->get_selected_item()].get(), true);
- return;
+ goto end_of_images_page;
}
} else if(event.key.code == sf::Keyboard::Escape) {
current_page = Page::EPISODE_LIST;
@@ -2112,9 +2201,17 @@ namespace QuickMedia {
window.display();
}
+
+ end_of_images_page:
+ if(current_page != Page::IMAGES && current_page != Page::IMAGES_CONTINUOUS) {
+ image_download_cancel = true;
+ std::unique_lock<std::mutex> lock(image_upscale_mutex);
+ images_to_upscale.clear();
+ }
}
void Program::image_continuous_page() {
+ image_download_cancel = false;
search_bar->onTextUpdateCallback = nullptr;
search_bar->onTextSubmitCallback = nullptr;
@@ -2182,6 +2279,12 @@ namespace QuickMedia {
}
}
}
+
+ if(current_page != Page::IMAGES && current_page != Page::IMAGES_CONTINUOUS) {
+ image_download_cancel = true;
+ std::unique_lock<std::mutex> lock(image_upscale_mutex);
+ images_to_upscale.clear();
+ }
}
void Program::content_list_page() {