mirror of
https://github.com/bsnes-emu/bsnes.git
synced 2025-08-31 22:21:51 +02:00
Scrap all of that, and use the undocumented spec that everyone actually uses
This commit is contained in:
10
Makefile
10
Makefile
@@ -222,8 +222,8 @@ ifneq ($(shell pkg-config --exists gio-2.0 || echo 0),)
|
||||
GIO_CFLAGS = $(error The Gio library could not be found)
|
||||
GIO_LDFLAGS = $(error The Gio library could not be found)
|
||||
else
|
||||
GIO_CFLAGS := $(shell $(PKG_CONFIG) --cflags gio-2.0) -DG_LOG_USE_STRUCTURED
|
||||
GIO_LDFLAGS := $(shell $(PKG_CONFIG) --libs gio-2.0)
|
||||
GIO_CFLAGS := $(shell $(PKG_CONFIG) --cflags gio-unix-2.0) -DG_LOG_USE_STRUCTURED
|
||||
GIO_LDFLAGS := $(shell $(PKG_CONFIG) --libs gio-unix-2.0)
|
||||
ifeq ($(CONF),debug)
|
||||
GIO_CFLAGS += -DG_ENABLE_DEBUG
|
||||
else
|
||||
@@ -437,8 +437,6 @@ $(OBJ)/SDL/%.c.o: SDL/%.c
|
||||
$(OBJ)/XdgThumbnailer/%.c.o: XdgThumbnailer/%.c
|
||||
-@$(MKDIR) -p $(dir $@)
|
||||
$(CC) $(CFLAGS) $(GIO_CFLAGS) $(GDK_PIXBUF_CFLAGS) -DG_LOG_DOMAIN='"sameboy-thumbnailer"' -c $< -o $@
|
||||
# Make sure not to attempt compiling this before generating the interface code.
|
||||
$(OBJ)/XdgThumbnailer/main.c.o: $(OBJ)/XdgThumbnailer/interface.h
|
||||
# Make sure not to attempt compiling this before generating the resource code.
|
||||
$(OBJ)/XdgThumbnailer/emulate.c.o: $(OBJ)/XdgThumbnailer/resources.h
|
||||
# Silence warnings for this. It is code generated not by us, so we do not want `-Werror` to break
|
||||
@@ -447,10 +445,6 @@ $(OBJ)/XdgThumbnailer/%.c.o: $(OBJ)/XdgThumbnailer/%.c
|
||||
-@$(MKDIR) -p $(dir $@)
|
||||
$(CC) $(CFLAGS) $(GIO_CFLAGS) $(GDK_PIXBUF_CFLAGS) -DG_LOG_DOMAIN='"sameboy-thumbnailer"' -w -c $< -o $@
|
||||
|
||||
$(OBJ)/XdgThumbnailer/interface.c $(OBJ)/XdgThumbnailer/interface.h: XdgThumbnailer/interface.xml
|
||||
-@$(MKDIR) -p $(dir $@)
|
||||
gdbus-codegen --c-generate-autocleanup none --c-namespace Thumbnailer --interface-prefix org.freedesktop.thumbnails. --generate-c-code $(OBJ)/XdgThumbnailer/interface $<
|
||||
|
||||
$(OBJ)/XdgThumbnailer/resources.c $(OBJ)/XdgThumbnailer/resources.h: %: XdgThumbnailer/resources.gresource.xml $(BIN)/BootROMs/cgb_boot_fast.bin
|
||||
-@$(MKDIR) -p $(dir $@)
|
||||
CC=$(CC) glib-compile-resources --dependency-file $@.mk --generate-phony-targets --generate --target $@ $<
|
||||
|
@@ -1,8 +1,10 @@
|
||||
#include "emulate.h"
|
||||
|
||||
#include <gio/gio.h>
|
||||
#include <glib.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "Core/gb.h"
|
||||
|
||||
@@ -15,15 +17,21 @@
|
||||
|
||||
/* --- */
|
||||
|
||||
static char *async_input_callback(GB_gameboy_t *gb) { return NULL; }
|
||||
static char *async_input_callback(GB_gameboy_t *gb)
|
||||
{
|
||||
(void)gb;
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static void log_callback(GB_gameboy_t *gb, const char *string, GB_log_attributes attributes)
|
||||
{
|
||||
// Swallow any logs.
|
||||
(void)gb, (void)string, (void)attributes; // Swallow any logs.
|
||||
}
|
||||
|
||||
static void vblank_callback(GB_gameboy_t *gb, GB_vblank_type_t type)
|
||||
{
|
||||
(void)type; // Ignore the type, we use VBlank counting as a kind of pacing (and to avoid tearing).
|
||||
|
||||
unsigned nb_frames_left = GPOINTER_TO_UINT(GB_get_user_data(gb));
|
||||
nb_frames_left--;
|
||||
GB_set_user_data(gb, GUINT_TO_POINTER(nb_frames_left));
|
||||
@@ -51,45 +59,43 @@ static uint32_t rgb_encode(GB_gameboy_t *gb, uint8_t r, uint8_t g, uint8_t b)
|
||||
return rgba;
|
||||
}
|
||||
|
||||
unsigned emulate(enum FileKind kind, unsigned char const *rom, size_t rom_size, uint32_t screen[static 160 * 144])
|
||||
uint8_t emulate(char const *path, uint32_t screen[static GB_SCREEN_WIDTH * GB_SCREEN_HEIGHT])
|
||||
{
|
||||
GB_gameboy_t gb;
|
||||
GB_init(&gb, GB_MODEL_CGB_E);
|
||||
|
||||
char const *last_dot = strrchr(path, '.');
|
||||
bool is_isx = last_dot && strcmp(last_dot + 1, "isx") == 0;
|
||||
if (is_isx ? GB_load_isx(&gb, path) : GB_load_rom(&gb, path)) {
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
|
||||
GError *error = NULL;
|
||||
GBytes *boot_rom = g_resource_lookup_data(resources_get_resource(), "/thumbnailer/cgb_boot_fast.bin",
|
||||
G_RESOURCE_LOOKUP_FLAGS_NONE, &error);
|
||||
g_assert_no_error(error);
|
||||
g_assert_no_error(error); // This shouldn't be able to fail.
|
||||
size_t boot_rom_size;
|
||||
unsigned char const *boot_rom_data = g_bytes_get_data(boot_rom, &boot_rom_size);
|
||||
g_assert_cmpuint(boot_rom_size, ==, BOOT_ROM_SIZE);
|
||||
GB_load_boot_rom_from_buffer(&gb, boot_rom_data, boot_rom_size);
|
||||
g_bytes_unref(boot_rom);
|
||||
|
||||
if (kind == KIND_ISX) {
|
||||
g_assert_not_reached(); // TODO: implement GB_load_isx_from_buffer
|
||||
}
|
||||
else {
|
||||
GB_load_rom_from_buffer(&gb, rom, rom_size);
|
||||
}
|
||||
|
||||
GB_set_user_data(&gb, GUINT_TO_POINTER(NB_FRAMES_TO_EMULATE));
|
||||
|
||||
GB_set_vblank_callback(&gb, vblank_callback);
|
||||
GB_set_pixels_output(&gb, screen);
|
||||
GB_set_rgb_encode_callback(&gb, rgb_encode);
|
||||
GB_set_async_input_callback(&gb, async_input_callback);
|
||||
GB_set_log_callback(&gb, log_callback);
|
||||
GB_set_log_callback(&gb, log_callback); // Anything bizarre the ROM does during emulation, we don't care about.
|
||||
GB_set_color_correction_mode(&gb, GB_COLOR_CORRECTION_MODERN_BALANCED);
|
||||
|
||||
GB_set_rendering_disabled(&gb, true);
|
||||
GB_set_turbo_mode(&gb, true, true);
|
||||
while (GPOINTER_TO_UINT(GB_get_user_data(&gb))) {
|
||||
// TODO: handle cancellation
|
||||
GB_run(&gb);
|
||||
}
|
||||
|
||||
unsigned cgb_flag = GB_read_memory(&gb, 0x143) & 0xC0;
|
||||
int cgb_flag = GB_read_memory(&gb, 0x143) & 0xC0;
|
||||
GB_free(&gb);
|
||||
return cgb_flag;
|
||||
}
|
||||
|
@@ -1,12 +1,8 @@
|
||||
#pragma once
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
enum FileKind {
|
||||
KIND_GB,
|
||||
KIND_GBC,
|
||||
KIND_ISX,
|
||||
};
|
||||
#define GB_SCREEN_WIDTH 160
|
||||
#define GB_SCREEN_HEIGHT 144
|
||||
|
||||
unsigned emulate(enum FileKind kind, unsigned char const *rom, size_t rom_size, uint32_t screen[static 160 * 144]);
|
||||
uint8_t emulate(char const *path, uint32_t screen[static GB_SCREEN_WIDTH * GB_SCREEN_HEIGHT]);
|
||||
|
@@ -1,32 +0,0 @@
|
||||
<!DOCTYPE node PUBLIC "-//freedesktop//DTD D-BUS Object Introspection 1.0//EN"
|
||||
"http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd">
|
||||
<node name="/com/github/liji32/sameboy/XdgThumbnailer">
|
||||
<interface name="org.freedesktop.thumbnails.SpecializedThumbnailer1">
|
||||
<method name="Queue">
|
||||
<arg type="s" name="uri" direction="in" />
|
||||
<arg type="s" name="mime_type" direction="in" />
|
||||
<arg type="s" name="flavor" direction="in" />
|
||||
<arg type="b" name="urgent" direction="in" />
|
||||
<arg type="u" name="handle" direction="out" />
|
||||
</method>
|
||||
<method name="Dequeue">
|
||||
<arg type="u" name="handle" direction="in" />
|
||||
</method>
|
||||
<signal name="Ready">
|
||||
<arg type="u" name="handle" />
|
||||
<arg type="s" name="uri" />
|
||||
</signal>
|
||||
<signal name="Started">
|
||||
<arg type="u" name="handle" />
|
||||
</signal>
|
||||
<signal name="Finished">
|
||||
<arg type="u" name="handle" />
|
||||
</signal>
|
||||
<signal name="Error">
|
||||
<arg type="u" name="handle" />
|
||||
<arg type="s" name="failed_uri" />
|
||||
<arg type="i" name="error_code" />
|
||||
<arg type="s" name="message" />
|
||||
</signal>
|
||||
</interface>
|
||||
</node>
|
@@ -1,147 +1,128 @@
|
||||
#include "main.h"
|
||||
|
||||
#include <gio/gio.h>
|
||||
#include <glib-object.h>
|
||||
#include <glib-unix.h>
|
||||
#include <errno.h>
|
||||
#include <gdk-pixbuf/gdk-pixbuf.h>
|
||||
#include <glib.h>
|
||||
#include <signal.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include "tasks.h"
|
||||
#include "thumbnail.h"
|
||||
#include "emulate.h"
|
||||
|
||||
// Auto-generated via `gdbus-codegen` from `interface.xml`.
|
||||
#include "build/obj/XdgThumbnailer/interface.h"
|
||||
static char const dmg_only_resource_path[] = "/thumbnailer/CartridgeTemplate.png";
|
||||
static char const dual_resource_path[] = "/thumbnailer/UniversalCartridgeTemplate.png";
|
||||
static char const cgb_only_resource_path[] = "/thumbnailer/ColorCartridgeTemplate.png";
|
||||
|
||||
static char const name_on_bus[] = "com.github.liji32.sameboy.XdgThumbnailer";
|
||||
static char const object_path[] = "/com/github/liji32/sameboy/XdgThumbnailer";
|
||||
|
||||
ThumbnailerSpecializedThumbnailer1 *thumbnailer_interface = NULL;
|
||||
static unsigned max_nb_worker_threads;
|
||||
|
||||
pid_t pid;
|
||||
|
||||
static gboolean handle_queue(void *instance, GDBusMethodInvocation *invocation, char const *uri, char const *mime_type,
|
||||
char const *flavor, gboolean urgent, void *user_data)
|
||||
static GdkPixbuf *generate_thumbnail(char const *input_path)
|
||||
{
|
||||
ThumbnailerSpecializedThumbnailer1 *skeleton = instance;
|
||||
g_info("Received Queue(uri=\"%s\", mime_type=\"%s\", flavor=\"%s\", urgent=%s) request", uri, mime_type, flavor,
|
||||
urgent ? "true" : "false");
|
||||
g_assert(skeleton == thumbnailer_interface);
|
||||
uint32_t screen_raw[GB_SCREEN_WIDTH * GB_SCREEN_HEIGHT];
|
||||
uint8_t cgb_flag = emulate(input_path, screen_raw);
|
||||
|
||||
struct NewTaskInfo task_info = new_task(urgent);
|
||||
start_thumbnailing(task_info.handle, task_info.cancellable, urgent, uri, mime_type, flavor);
|
||||
// Generate the thumbnail from `screen_raw` and `cgb_flag`.
|
||||
|
||||
thumbnailer_specialized_thumbnailer1_complete_queue(skeleton, invocation, task_info.handle);
|
||||
return G_DBUS_METHOD_INVOCATION_HANDLED;
|
||||
}
|
||||
// `screen_raw` is properly formatted for this operation; see the comment in `rgb_encode` for a
|
||||
// discussion of why and how.
|
||||
GdkPixbuf *screen = gdk_pixbuf_new_from_data((uint8_t *)screen_raw, GDK_COLORSPACE_RGB,
|
||||
true, // Yes, we have alpha!
|
||||
8, // bpp
|
||||
GB_SCREEN_WIDTH, GB_SCREEN_HEIGHT, // Size.
|
||||
GB_SCREEN_WIDTH * sizeof(screen_raw[0]), // Row stride.
|
||||
NULL, NULL); // Do not free the buffer.
|
||||
// Scale the screen and position it in the appropriate place for compositing the cartridge templates.
|
||||
GdkPixbuf *scaled_screen = gdk_pixbuf_new(GDK_COLORSPACE_RGB, true, 8, 1024, 1024);
|
||||
gdk_pixbuf_scale(screen, // Source.
|
||||
scaled_screen, // Destination.
|
||||
192, 298, // Match the displacement below.
|
||||
GB_SCREEN_WIDTH * 4, GB_SCREEN_HEIGHT * 4, // How the scaled rectangle should be cropped.
|
||||
192, 298, // Displace the scaled screen so it lines up with the template.
|
||||
4, 4, // Scaling factors.
|
||||
GDK_INTERP_NEAREST);
|
||||
g_object_unref(screen);
|
||||
|
||||
static gboolean handle_dequeue(void *instance, GDBusMethodInvocation *invocation, unsigned handle, void *user_data)
|
||||
{
|
||||
ThumbnailerSpecializedThumbnailer1 *skeleton = instance;
|
||||
g_info("Received Dequeue(handle=%u) request", handle);
|
||||
g_assert(skeleton == thumbnailer_interface);
|
||||
|
||||
cancel_task(handle);
|
||||
|
||||
return G_DBUS_METHOD_INVOCATION_HANDLED;
|
||||
}
|
||||
|
||||
static GMainLoop *main_loop;
|
||||
|
||||
static void on_bus_acquired(GDBusConnection *connection, const char *name, void *user_data)
|
||||
{
|
||||
g_assert_cmpstr(name, ==, name_on_bus);
|
||||
(void)user_data;
|
||||
g_info("Acquired bus");
|
||||
|
||||
// Create the interface, and hook up callbacks for when its methods are called.
|
||||
thumbnailer_interface = thumbnailer_specialized_thumbnailer1_skeleton_new();
|
||||
g_signal_connect(thumbnailer_interface, "handle-queue", G_CALLBACK(handle_queue), NULL);
|
||||
g_signal_connect(thumbnailer_interface, "handle-dequeue", G_CALLBACK(handle_dequeue), NULL);
|
||||
|
||||
// Export the interface on the bus.
|
||||
GError *error = NULL;
|
||||
GDBusInterfaceSkeleton *interface = G_DBUS_INTERFACE_SKELETON(thumbnailer_interface);
|
||||
gboolean res = g_dbus_interface_skeleton_export(interface, connection, object_path, &error);
|
||||
g_assert(res);
|
||||
GdkPixbuf *template;
|
||||
switch (cgb_flag) {
|
||||
case 0xC0:
|
||||
template = gdk_pixbuf_new_from_resource(cgb_only_resource_path, &error);
|
||||
break;
|
||||
case 0x80:
|
||||
template = gdk_pixbuf_new_from_resource(dual_resource_path, &error);
|
||||
break;
|
||||
default:
|
||||
template = gdk_pixbuf_new_from_resource(dmg_only_resource_path, &error);
|
||||
break;
|
||||
}
|
||||
g_assert_no_error(error);
|
||||
g_assert_cmpint(gdk_pixbuf_get_width(template), ==, 1024);
|
||||
g_assert_cmpint(gdk_pixbuf_get_height(template), ==, 1024);
|
||||
gdk_pixbuf_composite(template, // Source.
|
||||
scaled_screen, // Destination.
|
||||
0, 0, // Match the displacement below.
|
||||
1024, 1024, // Crop of the scaled rectangle.
|
||||
0, 0, // Displacement of the scaled rectangle.
|
||||
1, 1, // Scaling factors.
|
||||
GDK_INTERP_NEAREST, // Doesn't really matter, but should be a little faster.
|
||||
255); // Blending factor of the source onto the destination.
|
||||
g_object_unref(template);
|
||||
|
||||
return scaled_screen;
|
||||
}
|
||||
|
||||
static GdkPixbuf *enforce_max_size(GdkPixbuf *thumbnail, unsigned long max_size)
|
||||
{
|
||||
g_assert_cmpuint(gdk_pixbuf_get_width(thumbnail), ==, gdk_pixbuf_get_height(thumbnail));
|
||||
g_assert_cmpuint(gdk_pixbuf_get_width(thumbnail), ==, 1024);
|
||||
// This is only a *max* size; don't bother scaling up.
|
||||
// (This also prevents any overflow errors—notice that the scale function takes `int` size parameters!)
|
||||
if (max_size > 1024) return thumbnail;
|
||||
GdkPixbuf *scaled = gdk_pixbuf_scale_simple(thumbnail, max_size, max_size, GDK_INTERP_BILINEAR);
|
||||
g_object_unref(thumbnail);
|
||||
return scaled;
|
||||
}
|
||||
|
||||
static void write_thumbnail(GdkPixbuf *thumbnail, char const *output_path)
|
||||
{
|
||||
GError *error = NULL;
|
||||
// Intentionally be "not a good citizen":
|
||||
// - Write directly to the provided path, instead of atomically replacing it with a fully-formed file;
|
||||
// this is necessary for at least Tumbler (XFCE's thumbnailer daemon), which creates the file **and** keeps the
|
||||
// returned FD—which keeps pointing to the deleted file... which is still empty!
|
||||
// - Do not save any metadata to the PNG, since the thumbnailer daemon (again, at least XFCE's, the only one I have
|
||||
// tested with) appears to read the PNG's pixels, and write a new one with the appropriate metadata.
|
||||
// (Thank you! Saves me all that work.)
|
||||
gdk_pixbuf_save(thumbnail, output_path, "png", &error, NULL);
|
||||
if (error) {
|
||||
g_error("Error exporting interface \"%s\" at \"%s\": %s", g_dbus_interface_skeleton_get_info(interface)->name,
|
||||
object_path, error->message);
|
||||
g_error("Failed to save thumbnail: %s", error->message);
|
||||
// NOTREACHED
|
||||
}
|
||||
}
|
||||
|
||||
static void on_name_acquired(GDBusConnection *connection, const char *name, void *user_data)
|
||||
int main(int argc, char *argv[])
|
||||
{
|
||||
g_assert_cmpstr(name, ==, name_on_bus);
|
||||
(void)user_data;
|
||||
|
||||
g_info("Acquired the name \"%s\" on the session bus", name);
|
||||
}
|
||||
|
||||
static void on_name_lost(GDBusConnection *connection, const char *name, void *user_data)
|
||||
{
|
||||
g_assert_cmpstr(name, ==, name_on_bus);
|
||||
(void)user_data;
|
||||
|
||||
if (connection != NULL) {
|
||||
g_info("Lost the name \"%s\" on the session bus", name);
|
||||
if (argc != 3 && argc != 4) {
|
||||
g_error("Usage: %s <input path> <output path> [<size>]", argv[0] ? argv[0] : "sameboy-thumbnailer");
|
||||
// NOTREACHED
|
||||
}
|
||||
else {
|
||||
g_error("Failed to connect to session bus");
|
||||
char const *input_path = argv[1];
|
||||
char *output_path = argv[2]; // Gets mutated in-place.
|
||||
char const *max_size = argv[3]; // May be NULL.
|
||||
|
||||
g_debug("%s -> %s [%s]", input_path, output_path, max_size ? max_size : "(none)");
|
||||
|
||||
GdkPixbuf *thumbnail = generate_thumbnail(input_path);
|
||||
if (max_size) {
|
||||
char *endptr;
|
||||
errno = 0;
|
||||
unsigned long size = strtoul(max_size, &endptr, 10);
|
||||
if (errno != 0 || *max_size == '\0' || *endptr != '\0') {
|
||||
g_error("Invalid size parameter \"%s\": %s", max_size, strerror(errno == 0 ? EINVAL : errno));
|
||||
// NOTREACHED
|
||||
}
|
||||
|
||||
thumbnail = enforce_max_size(thumbnail, size);
|
||||
}
|
||||
g_main_loop_quit(main_loop);
|
||||
}
|
||||
write_thumbnail(thumbnail, output_path);
|
||||
g_object_unref(thumbnail);
|
||||
|
||||
static gboolean handle_sigterm(void *user_data)
|
||||
{
|
||||
g_info("SIGTERM received! Quitting...");
|
||||
|
||||
g_main_loop_quit(main_loop);
|
||||
return G_SOURCE_CONTINUE; // Do not remove this source ourselves, let the post-main loop do so.
|
||||
}
|
||||
|
||||
/* --- */
|
||||
|
||||
int main(int argc, char const *argv[])
|
||||
{
|
||||
pid = getpid();
|
||||
|
||||
locate_and_create_thumbnail_dir();
|
||||
|
||||
max_nb_worker_threads = g_get_num_processors();
|
||||
// unsigned active_worker_threads = 0;
|
||||
|
||||
// Create the task queue *before* starting to accept tasks from D-Bus.
|
||||
init_tasks();
|
||||
// Likewise, create the main loop before then, so it can be aborted even before entering it.
|
||||
main_loop = g_main_loop_new(NULL, false);
|
||||
|
||||
// Refuse to replace the name or be replaced; there should only be one instance of the
|
||||
// thumbnailer on the bus at all times. To replace this program, kill it.
|
||||
unsigned owner_id = g_bus_own_name(G_BUS_TYPE_SESSION, name_on_bus, G_BUS_NAME_OWNER_FLAGS_DO_NOT_QUEUE,
|
||||
on_bus_acquired, on_name_acquired, on_name_lost, NULL, NULL);
|
||||
|
||||
unsigned sigterm_source_id = g_unix_signal_add(SIGTERM, handle_sigterm, NULL);
|
||||
g_main_loop_run(main_loop);
|
||||
// This must be done before destroying the main loop.
|
||||
gboolean removed = g_source_remove(sigterm_source_id);
|
||||
g_assert(removed);
|
||||
|
||||
g_info("Waiting for outstanding tasks...");
|
||||
cleanup_tasks(); // Also waits for any remaining tasks.
|
||||
// "Pedantic" cleanup for Valgrind et al.
|
||||
g_main_loop_unref(main_loop);
|
||||
g_bus_unown_name(owner_id);
|
||||
if (thumbnailer_interface) {
|
||||
g_dbus_interface_skeleton_unexport(G_DBUS_INTERFACE_SKELETON(thumbnailer_interface));
|
||||
g_object_unref(thumbnailer_interface);
|
||||
}
|
||||
free_thumbnail_dir_path();
|
||||
return 0;
|
||||
}
|
||||
|
@@ -1,18 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <unistd.h>
|
||||
|
||||
// As defined in the thumbnailer spec.
|
||||
enum ErrorCode {
|
||||
ERROR_UNKNOWN_SCHEME_OR_MIME,
|
||||
ERROR_SPECIALIZED_THUMBNAILER_CONNECTION_FAILED, // Not applicable.
|
||||
ERROR_INVALID_DATA, // Any file can be decoded as a GB ROM, apparently!
|
||||
ERROR_THUMBNAIILING_THUMBNAIL, // We defer checking this to the generic thumbnailer.
|
||||
ERROR_COULD_NOT_WRITE,
|
||||
ERROR_UNSUPPORTED_FLAVOR,
|
||||
};
|
||||
|
||||
struct _ThumbnailerSpecializedThumbnailer1;
|
||||
extern struct _ThumbnailerSpecializedThumbnailer1 *thumbnailer_interface;
|
||||
|
||||
extern pid_t pid;
|
4
XdgThumbnailer/sameboy.thumbnailer
Normal file
4
XdgThumbnailer/sameboy.thumbnailer
Normal file
@@ -0,0 +1,4 @@
|
||||
[Thumbnailer Entry]
|
||||
TryExec=/home/issotm/SameBoy/build/bin/XdgThumbnailer/sameboy-thumbnailer
|
||||
Exec=/home/issotm/SameBoy/build/bin/XdgThumbnailer/sameboy-thumbnailer %i %o %s
|
||||
MimeType=application/x-gameboy-rom;application/x-gameboy-color-rom;application/x-gameboy-isx
|
@@ -1,103 +0,0 @@
|
||||
#include "tasks.h"
|
||||
|
||||
#include <gio/gio.h>
|
||||
#include <glib.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
#define URGENT_FLAG (1u << (sizeof(unsigned) * CHAR_BIT - 1)) // The compiler should warn if this shift is out of range.
|
||||
|
||||
struct Tasks {
|
||||
// Note that the lock only applies to the whole array; individual elements may be mutated
|
||||
// in-place just fine by the readers.
|
||||
GRWLock lock;
|
||||
GArray /* of GCancellable* */ *tasks;
|
||||
};
|
||||
static struct Tasks urgent_tasks, tasks;
|
||||
|
||||
static void init_task_list(struct Tasks *task_list)
|
||||
{
|
||||
g_rw_lock_init(&task_list->lock);
|
||||
task_list->tasks = g_array_new(false, false, sizeof(GCancellable *));
|
||||
}
|
||||
void init_tasks(void)
|
||||
{
|
||||
init_task_list(&urgent_tasks);
|
||||
init_task_list(&tasks);
|
||||
}
|
||||
|
||||
static void cleanup_task_list(struct Tasks *task_list)
|
||||
{
|
||||
// TODO: wait for the remaining tasks to end?
|
||||
g_rw_lock_clear(&task_list->lock);
|
||||
g_array_unref(task_list->tasks);
|
||||
}
|
||||
void cleanup_tasks(void)
|
||||
{
|
||||
cleanup_task_list(&urgent_tasks);
|
||||
cleanup_task_list(&tasks);
|
||||
}
|
||||
|
||||
struct NewTaskInfo new_task(gboolean is_urgent)
|
||||
{
|
||||
struct Tasks *task_list = is_urgent ? &urgent_tasks : &tasks;
|
||||
GCancellable **array = (void *)task_list->tasks->data;
|
||||
|
||||
GCancellable *cancellable = g_cancellable_new();
|
||||
|
||||
// We may reallocate the array, so we need a writer lock.
|
||||
g_rw_lock_writer_lock(&task_list->lock);
|
||||
// First, look for a free slot in the array.
|
||||
unsigned index = 0;
|
||||
for (unsigned i = 0; i < task_list->tasks->len; ++i) {
|
||||
if (array[i] == NULL) {
|
||||
array[i] = cancellable;
|
||||
index = i + 1;
|
||||
goto got_slot;
|
||||
}
|
||||
}
|
||||
// We need to allocate a new slot.
|
||||
|
||||
// Each task list cannot contain 0x7FFFFFFF handles, as otherwise bit 7 cannot differentiate
|
||||
// between regular and urgent tasks.
|
||||
// Note that index 0 is invalid, since it's reserved for "no handle", so that's 1 less.
|
||||
if (task_list->tasks->len == URGENT_FLAG - 2) {
|
||||
g_object_unref(cancellable);
|
||||
return (struct NewTaskInfo){.handle = 0};
|
||||
}
|
||||
g_array_append_val(task_list->tasks, cancellable);
|
||||
index = task_list->tasks->len; // We want the new index *plus one*.
|
||||
got_slot:
|
||||
g_rw_lock_writer_unlock(&task_list->lock);
|
||||
|
||||
g_assert_cmpuint(index, !=, 0);
|
||||
g_assert_cmpuint(index, <, URGENT_FLAG);
|
||||
|
||||
return (struct NewTaskInfo){.handle = is_urgent ? (index | URGENT_FLAG) : index, .cancellable = cancellable};
|
||||
}
|
||||
|
||||
void cancel_task(unsigned handle)
|
||||
{
|
||||
struct Tasks *task_list = (handle & URGENT_FLAG) ? &urgent_tasks : &tasks;
|
||||
|
||||
g_rw_lock_reader_lock(&task_list->lock);
|
||||
GCancellable **slot = &((GCancellable **)task_list->tasks->data)[(handle & ~URGENT_FLAG) - 1];
|
||||
GCancellable *cancellable = *slot;
|
||||
*slot = NULL;
|
||||
g_rw_lock_reader_unlock(&task_list->lock);
|
||||
|
||||
g_cancellable_cancel(cancellable);
|
||||
g_object_unref(cancellable);
|
||||
}
|
||||
|
||||
void finished_task(unsigned handle)
|
||||
{
|
||||
struct Tasks *task_list = (handle & URGENT_FLAG) ? &urgent_tasks : &tasks;
|
||||
|
||||
g_rw_lock_reader_lock(&task_list->lock);
|
||||
GCancellable **slot = &((GCancellable **)task_list->tasks->data)[(handle & ~URGENT_FLAG) - 1];
|
||||
GCancellable *cancellable = *slot;
|
||||
*slot = NULL;
|
||||
g_rw_lock_reader_unlock(&task_list->lock);
|
||||
|
||||
g_object_unref(cancellable);
|
||||
}
|
@@ -1,15 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <gio/gio.h>
|
||||
#include <glib.h>
|
||||
|
||||
void init_tasks(void);
|
||||
void cleanup_tasks(void);
|
||||
|
||||
struct NewTaskInfo {
|
||||
unsigned handle;
|
||||
GCancellable *cancellable;
|
||||
};
|
||||
struct NewTaskInfo new_task(gboolean is_urgent);
|
||||
void cancel_task(unsigned handle);
|
||||
void finished_task(unsigned handle);
|
@@ -1,355 +0,0 @@
|
||||
#include "thumbnail.h"
|
||||
|
||||
#include <fcntl.h>
|
||||
#include <gdk-pixbuf/gdk-pixbuf.h>
|
||||
#include <gio/gio.h>
|
||||
#include <glib-unix.h>
|
||||
#include <glib.h>
|
||||
#include <pwd.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include "emulate.h"
|
||||
#include "glibconfig.h"
|
||||
#include "main.h"
|
||||
#include "tasks.h"
|
||||
|
||||
#define THUMBNAILING_ERROR_DOMAIN (g_quark_from_static_string("thumbnailing"))
|
||||
|
||||
static char const dmg_only_resource_path[] = "/thumbnailer/CartridgeTemplate.png";
|
||||
static char const dual_resource_path[] = "/thumbnailer/UniversalCartridgeTemplate.png";
|
||||
static char const cgb_only_resource_path[] = "/thumbnailer/ColorCartridgeTemplate.png";
|
||||
|
||||
static char const gb_mime_type[] = "application/x-gameboy-rom";
|
||||
static char const gbc_mime_type[] = "application/x-gameboy-color-rom";
|
||||
static char const isx_mime_type[] = "application/x-gameboy-isx";
|
||||
|
||||
/* --- */
|
||||
|
||||
enum ThumbnailFlavor {
|
||||
FLAVOR_NORMAL, // 128×128.
|
||||
FLAVOR_LARGE, // 256×256.
|
||||
FLAVOR_X_LARGE, // 512×512.
|
||||
FLAVOR_XX_LARGE, // 1024×1024.
|
||||
};
|
||||
|
||||
static char const flavor_dir_names[][9] = {
|
||||
[FLAVOR_NORMAL] = "normal",
|
||||
[FLAVOR_LARGE] = "large",
|
||||
[FLAVOR_X_LARGE] = "x-large",
|
||||
[FLAVOR_XX_LARGE] = "xx-large",
|
||||
};
|
||||
|
||||
static GPathBuf thumbnail_dir_path;
|
||||
|
||||
static __attribute__((returns_nonnull)) char const *get_home_path(void)
|
||||
{
|
||||
char const *home = g_getenv("HOME"); // Statically allocated, so no need to free.
|
||||
if (home != NULL) return home;
|
||||
|
||||
// This is pushing it, but let's give it one last shot.
|
||||
uid_t uid = geteuid();
|
||||
struct passwd *passwd_entry;
|
||||
do {
|
||||
errno = 0; // It may be left alone if no such entry can be found.
|
||||
passwd_entry = getpwuid(uid); // Statically allocated, so no need to free.
|
||||
} while (!passwd_entry && errno == EINTR);
|
||||
if (passwd_entry == NULL) {
|
||||
g_error("`XDG_CACHE_HOME` is unset, and failed to get the path to user %ju's home: %s", (uintmax_t)uid,
|
||||
strerror(errno));
|
||||
// NOTREACHED
|
||||
}
|
||||
return passwd_entry->pw_dir;
|
||||
}
|
||||
|
||||
void locate_and_create_thumbnail_dir(void)
|
||||
{
|
||||
// Compute the path to the thumbnail directory.
|
||||
char const *cache_home = g_getenv("XDG_CACHE_HOME"); // Statically allocated, so no need to free.
|
||||
if (cache_home != NULL) {
|
||||
g_path_buf_init_from_path(&thumbnail_dir_path, cache_home);
|
||||
}
|
||||
else {
|
||||
char const *home = get_home_path();
|
||||
g_path_buf_init_from_path(&thumbnail_dir_path, home);
|
||||
g_path_buf_push(&thumbnail_dir_path, ".cache");
|
||||
}
|
||||
g_path_buf_push(&thumbnail_dir_path, "thumbnails");
|
||||
|
||||
// Create the thumbnail directories if they don't already exist.
|
||||
for (size_t i = 0; i < G_N_ELEMENTS(flavor_dir_names); ++i) {
|
||||
g_path_buf_push(&thumbnail_dir_path, flavor_dir_names[i]);
|
||||
char *path = g_path_buf_to_path(&thumbnail_dir_path);
|
||||
|
||||
if (g_mkdir_with_parents(path, S_IRWXU) != 0) { // The permissions are mandated by the thumbnail spec.
|
||||
g_error("Failed to create thumbnail cache directory at \"%s\": %s", path, strerror(errno));
|
||||
// NOTREACHED
|
||||
}
|
||||
|
||||
g_free(path);
|
||||
// Restore the GPathBuf to its original state (pointing at the thumbnail cache's root dir).
|
||||
g_path_buf_pop(&thumbnail_dir_path);
|
||||
}
|
||||
// ...thus, we end with the path buf pointing to the root dir, which is what the rest of this module expects.
|
||||
}
|
||||
|
||||
void free_thumbnail_dir_path(void) { g_path_buf_clear(&thumbnail_dir_path); }
|
||||
|
||||
/* --- */
|
||||
|
||||
struct TaskData {
|
||||
char *contents;
|
||||
size_t length;
|
||||
enum FileKind kind;
|
||||
enum ThumbnailFlavor flavor;
|
||||
};
|
||||
|
||||
static void destroy_task_data(void *data)
|
||||
{
|
||||
struct TaskData *task_data = data;
|
||||
|
||||
g_free(task_data->contents);
|
||||
g_slice_free(struct TaskData, task_data);
|
||||
}
|
||||
|
||||
__attribute__((returns_nonnull)) char const *mime_type(enum FileKind kind)
|
||||
{
|
||||
switch (kind) {
|
||||
case KIND_GB:
|
||||
return gb_mime_type;
|
||||
case KIND_GBC:
|
||||
return gbc_mime_type;
|
||||
case KIND_ISX:
|
||||
return isx_mime_type;
|
||||
}
|
||||
__builtin_unreachable();
|
||||
}
|
||||
|
||||
/* --- */
|
||||
|
||||
#define GB_SCREEN_WIDTH 160
|
||||
#define GB_SCREEN_HEIGHT 144
|
||||
|
||||
static void generate_thumbnail(GTask *task, void *source_object, void *data, GCancellable *cancellable)
|
||||
{
|
||||
struct TaskData *task_data = data;
|
||||
char const *uri = g_task_get_name(task);
|
||||
char *md5 = g_compute_checksum_for_string(G_CHECKSUM_MD5, uri, -1);
|
||||
|
||||
uint32_t screen_raw[GB_SCREEN_WIDTH * GB_SCREEN_HEIGHT];
|
||||
unsigned cgb_flag =
|
||||
emulate(task_data->kind, (unsigned char const *)task_data->contents, task_data->length, screen_raw);
|
||||
|
||||
// Generate the thumbnail from `screen_raw` and `cgb_flag`.
|
||||
|
||||
// `screen_raw` is properly formatted for this operation; see the comment in `rgb_encode` for a
|
||||
// discussion of why and how.
|
||||
GdkPixbuf *screen = gdk_pixbuf_new_from_data((uint8_t *)screen_raw, GDK_COLORSPACE_RGB,
|
||||
true, // Yes, we have alpha!
|
||||
8, // bpp
|
||||
GB_SCREEN_WIDTH, GB_SCREEN_HEIGHT, // Size.
|
||||
GB_SCREEN_WIDTH * sizeof(screen_raw[0]), // Row stride.
|
||||
NULL, NULL); // Do not free the buffer.
|
||||
// Scale the screen and position it in the appropriate place for compositing the cartridge templates.
|
||||
GdkPixbuf *scaled_screen = gdk_pixbuf_new(GDK_COLORSPACE_RGB, true, 8, 1024, 1024);
|
||||
gdk_pixbuf_scale(screen, // Source.
|
||||
scaled_screen, // Destination.
|
||||
192, 298, // Match the displacement below.
|
||||
GB_SCREEN_WIDTH * 4, GB_SCREEN_HEIGHT * 4, // How the scaled rectangle should be cropped.
|
||||
192, 298, // Displace the scaled screen so it lines up with the template.
|
||||
4, 4, // Scaling factors.
|
||||
GDK_INTERP_NEAREST);
|
||||
g_object_unref(screen);
|
||||
|
||||
GError *error = NULL;
|
||||
GdkPixbuf *template;
|
||||
switch (cgb_flag) {
|
||||
case 0xC0:
|
||||
template = gdk_pixbuf_new_from_resource(cgb_only_resource_path, &error);
|
||||
break;
|
||||
case 0x80:
|
||||
template = gdk_pixbuf_new_from_resource(dual_resource_path, &error);
|
||||
break;
|
||||
default:
|
||||
template = gdk_pixbuf_new_from_resource(dmg_only_resource_path, &error);
|
||||
break;
|
||||
}
|
||||
g_assert_no_error(error);
|
||||
g_assert_cmpint(gdk_pixbuf_get_width(template), ==, 1024);
|
||||
g_assert_cmpint(gdk_pixbuf_get_height(template), ==, 1024);
|
||||
gdk_pixbuf_composite(template, // Source.
|
||||
scaled_screen, // Destination.
|
||||
0, 0, // Match the displacement below.
|
||||
1024, 1024, // Crop of the scaled rectangle.
|
||||
0, 0, // Displacement of the scaled rectangle.
|
||||
1, 1, // Scaling factors.
|
||||
GDK_INTERP_NEAREST, // Doesn't really matter, but should be a little faster.
|
||||
255); // Blending factor of the source onto the destination.
|
||||
g_object_unref(template);
|
||||
|
||||
GPathBuf *output_path_buf = g_path_buf_copy(&thumbnail_dir_path);
|
||||
g_path_buf_push(output_path_buf, flavor_dir_names[task_data->flavor]);
|
||||
g_assert_cmpuint(strlen(md5), ==, 32);
|
||||
// The buffer's size is checked by the compiler (`-Wformat-overflow=2`).
|
||||
char temp_file_name[1 + 32 + 1 + 20 + 1 + 10 + 4 + 1]; // (".%s_%lu_%u.png", md5, pid, handle).
|
||||
temp_file_name[0] = '.';
|
||||
memcpy(&temp_file_name[1], md5, 32);
|
||||
sprintf(&temp_file_name[1 + 32], "_%lu_%u.png",
|
||||
pid + 0lu, // Promote the `pid_t` to `long int` if possible; `-Wformat` will warn otherwise.
|
||||
GPOINTER_TO_UINT(g_task_get_task_data(task)));
|
||||
g_path_buf_push(output_path_buf, temp_file_name);
|
||||
char *temp_file_path = g_path_buf_to_path(output_path_buf);
|
||||
|
||||
g_debug("Saving pixel buf for \"%s\" to \"%s\"", g_task_get_name(task), temp_file_path);
|
||||
char file_size[sizeof(G_STRINGIFY(SIZE_MAX))];
|
||||
sprintf(file_size, "%zu", task_data->length);
|
||||
gdk_pixbuf_save(scaled_screen, temp_file_path, "png", &error, // "Base" parameters.
|
||||
"tEXt::Thumb::URI", uri, // URI of the file being thumbnailed.
|
||||
// "tEXt::Thumb::MTime", "", // TODO
|
||||
"tEXt::Thumb::Size", file_size, // Size (in bytes) of the file being thumbnailed.
|
||||
"tEXt::Thumb::Mimetype", mime_type(task_data->kind), // MIME type of the file being thumbnailed.
|
||||
NULL);
|
||||
g_object_unref(scaled_screen);
|
||||
if (error) {
|
||||
g_path_buf_free(output_path_buf);
|
||||
g_free(temp_file_path);
|
||||
|
||||
g_task_return_new_error(task, THUMBNAILING_ERROR_DOMAIN, ERROR_COULD_NOT_WRITE, "Failed to write image: %s", error->message);
|
||||
g_error_free(error);
|
||||
g_object_unref(task);
|
||||
return;
|
||||
}
|
||||
|
||||
// Trim off all of the "temporary" parts.
|
||||
memcpy(&temp_file_name[1 + 32], ".png", sizeof(".png"));
|
||||
g_path_buf_set_filename(output_path_buf, &temp_file_name[1]);
|
||||
char *output_path = g_path_buf_free_to_path(output_path_buf);
|
||||
g_debug("Moving thumbnail for \"%s\" from \"%s\" to \"%s\"", g_task_get_name(task), temp_file_path, output_path);
|
||||
int rename_ret = rename(temp_file_path, output_path);
|
||||
int rename_errno = errno;
|
||||
g_free(temp_file_path);
|
||||
g_free(output_path);
|
||||
if (rename_ret != 0) {
|
||||
g_task_return_new_error(task, THUMBNAILING_ERROR_DOMAIN, ERROR_COULD_NOT_WRITE, "Failed to rename image from temp path: %s", strerror(rename_errno));
|
||||
g_object_unref(task);
|
||||
return;
|
||||
}
|
||||
|
||||
g_task_return_boolean(task, true);
|
||||
g_object_unref(task);
|
||||
}
|
||||
|
||||
// Callback when an async file operation completes.
|
||||
static void on_file_ready(GObject *source_object, GAsyncResult *res, void *user_data)
|
||||
{
|
||||
GFile *file = G_FILE(source_object);
|
||||
GTask *task = user_data;
|
||||
char const *uri = g_task_get_name(task);
|
||||
g_debug("File \"%s\" is done being read", uri);
|
||||
struct TaskData *task_data = g_task_get_task_data(task);
|
||||
|
||||
GError *error = NULL;
|
||||
g_file_load_contents_finish(file, res, &task_data->contents, &task_data->length, NULL, &error);
|
||||
g_object_unref(file);
|
||||
|
||||
if (error) {
|
||||
g_task_return_new_error(task, THUMBNAILING_ERROR_DOMAIN, ERROR_UNKNOWN_SCHEME_OR_MIME,
|
||||
"Failed to load URI \"%s\": %s", uri, error->message);
|
||||
g_error_free(error);
|
||||
g_object_unref(task);
|
||||
return;
|
||||
}
|
||||
|
||||
if (g_task_return_error_if_cancelled(task)) {
|
||||
g_object_unref(task);
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: cap the max number of active threads.
|
||||
g_task_run_in_thread(task, generate_thumbnail);
|
||||
}
|
||||
|
||||
/* --- */
|
||||
|
||||
static void on_thumbnailing_end(GObject *source_object, GAsyncResult *res, void *user_data)
|
||||
{
|
||||
// TODO: start a new thread if some task is pending.
|
||||
|
||||
g_assert_null(source_object); // The object that was passed to `g_task_new`.
|
||||
GTask *task = G_TASK(res);
|
||||
g_info("Ending thumbnailing for \"%s\"", g_task_get_name(task));
|
||||
unsigned handle = GPOINTER_TO_UINT(user_data);
|
||||
char const *uri = g_task_get_name(task);
|
||||
|
||||
GError *error = NULL;
|
||||
if (g_task_propagate_boolean(task, &error)) {
|
||||
g_signal_emit_by_name(thumbnailer_interface, "ready", handle, uri);
|
||||
}
|
||||
else if (!g_cancellable_is_cancelled(g_task_get_cancellable(task))) {
|
||||
// If the task was cancelled, do not emit an error response.
|
||||
g_signal_emit_by_name(thumbnailer_interface, "error", handle, uri, error->code, error->message);
|
||||
}
|
||||
g_signal_emit_by_name(thumbnailer_interface, "finished", handle);
|
||||
|
||||
finished_task(handle);
|
||||
}
|
||||
|
||||
void start_thumbnailing(unsigned handle, GCancellable *cancellable, gboolean is_urgent, char const *uri,
|
||||
char const *mime_type, char const *flavor_name)
|
||||
{
|
||||
g_signal_emit_by_name(thumbnailer_interface, "started", handle);
|
||||
|
||||
GTask *task = g_task_new(NULL, cancellable, on_thumbnailing_end, GUINT_TO_POINTER(handle));
|
||||
g_task_set_priority(task, is_urgent ? G_PRIORITY_HIGH : G_PRIORITY_DEFAULT);
|
||||
g_task_set_name(task, uri);
|
||||
|
||||
enum FileKind kind;
|
||||
if (g_strcmp0(mime_type, gbc_mime_type) == 0) {
|
||||
kind = KIND_GBC;
|
||||
}
|
||||
else if (g_strcmp0(mime_type, gb_mime_type) == 0) {
|
||||
kind = KIND_GB;
|
||||
}
|
||||
else if (g_strcmp0(mime_type, isx_mime_type) == 0) {
|
||||
kind = KIND_ISX;
|
||||
}
|
||||
else {
|
||||
g_task_return_new_error(task, THUMBNAILING_ERROR_DOMAIN, ERROR_UNKNOWN_SCHEME_OR_MIME,
|
||||
"Unsupported MIME type \"%s\"", mime_type);
|
||||
g_object_unref(task);
|
||||
return;
|
||||
}
|
||||
|
||||
enum ThumbnailFlavor flavor;
|
||||
if (g_strcmp0(flavor_name, "normal") == 0) {
|
||||
flavor = FLAVOR_NORMAL;
|
||||
}
|
||||
else if (g_strcmp0(flavor_name, "large") == 0) {
|
||||
flavor = FLAVOR_LARGE;
|
||||
}
|
||||
else if (g_strcmp0(flavor_name, "x-large") == 0) {
|
||||
flavor = FLAVOR_X_LARGE;
|
||||
}
|
||||
else if (g_strcmp0(flavor_name, "xx-large") == 0) {
|
||||
flavor = FLAVOR_XX_LARGE;
|
||||
}
|
||||
else {
|
||||
g_task_return_new_error(task, THUMBNAILING_ERROR_DOMAIN, ERROR_UNSUPPORTED_FLAVOR,
|
||||
"Unsupported thumbnail size/flavor \"%s\"", flavor_name);
|
||||
g_object_unref(task);
|
||||
return;
|
||||
}
|
||||
|
||||
struct TaskData *task_data = g_slice_new(struct TaskData);
|
||||
task_data->contents = NULL;
|
||||
task_data->kind = kind;
|
||||
task_data->flavor = flavor;
|
||||
g_task_set_task_data(task, task_data, destroy_task_data);
|
||||
|
||||
GFile *file = g_file_new_for_uri(uri);
|
||||
g_file_load_contents_async(file, cancellable, on_file_ready, task);
|
||||
}
|
@@ -1,10 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <gio/gio.h>
|
||||
#include <glib.h>
|
||||
|
||||
void locate_and_create_thumbnail_dir(void);
|
||||
void free_thumbnail_dir_path(void);
|
||||
|
||||
void start_thumbnailing(unsigned handle, GCancellable *cancellable, gboolean is_urgent, char const *uri,
|
||||
char const *mime_type, char const *flavor_name);
|
Reference in New Issue
Block a user