Compare commits

...

3 Commits

Author SHA1 Message Date
25c91f5a77
Add a note about default applications 2023-03-15 05:52:32 +01:00
796b05c9a5
Integrate online reverse image search
This makes use of our image processing capabilities in order to
turn arbitrary image files into normalized thumbnails,
upload them to a temporary host, and pass the resulting URI
to a search provider.

In future, fiv should ideally run the upload itself,
so that its status and any errors are obvious to the user,
as well as to get rid of the script's dependency on jq.
2023-03-15 05:52:32 +01:00
9286858573
Bump Wuffs 2023-03-07 20:26:04 +01:00
12 changed files with 233 additions and 22 deletions

View File

@ -43,7 +43,9 @@ Build-only dependencies:
Runtime dependencies:
gtk+-3.0, glib>=2.64, pixman-1, shared-mime-info, libturbojpeg, libwebp +
Optional dependencies: lcms2, LibRaw, librsvg-2.0, xcursor, libheif, libtiff,
ExifTool, resvg (unstable API, needs to be requested explicitly)
ExifTool, resvg (unstable API, needs to be requested explicitly) +
Runtime dependencies for reverse image search:
xdg-utils, cURL, jq
$ git clone --recursive https://git.janouch.name/p/fiv.git
$ meson builddir
@ -55,7 +57,8 @@ direct installations via `ninja install`. To test the program:
$ meson devenv fiv
The lossless JPEG cropper is intended to be invoked from a context menu.
The lossless JPEG cropper and reverse image search are intended to be invoked
from a context menu.
Windows
~~~~~~~

View File

@ -396,6 +396,15 @@ on_chooser_activate(GtkMenuItem *item, gpointer user_data)
GtkWidget *dialog = gtk_app_chooser_dialog_new_for_content_type(window,
GTK_DIALOG_DESTROY_WITH_PARENT | GTK_DIALOG_MODAL, ctx->content_type);
g_clear_object(&window);
#if 0
// This exists as a concept in mimeapps.list, but GNOME infuriatingly
// infers it from the last used application if missing.
gtk_app_chooser_widget_set_show_default(
GTK_APP_CHOOSER_WIDGET(gtk_app_chooser_dialog_get_widget(
GTK_APP_CHOOSER_DIALOG(dialog))), TRUE);
#endif
if (gtk_dialog_run(GTK_DIALOG(dialog)) == GTK_RESPONSE_OK) {
ctx->app_info = gtk_app_chooser_get_app_info(GTK_APP_CHOOSER(dialog));
open_context_launch(GTK_WIDGET(item), ctx);

View File

@ -1,7 +1,7 @@
//
// fiv-io.c: image operations
//
// Copyright (c) 2021 - 2022, Přemysl Eric Janouch <p@janouch.name>
// Copyright (c) 2021 - 2023, Přemysl Eric Janouch <p@janouch.name>
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted.
@ -633,9 +633,8 @@ load_wuffs_frame(struct load_wuffs_frame_context *ctx, GError **error)
if (!wuffs_base__status__is_ok(&status)) {
set_error(error, wuffs_base__status__message(&status));
// The PNG decoder, at minimum, will flush any pixel data, so use them.
if (status.repr != wuffs_base__suspension__short_read)
goto fail;
// The PNG decoder, at minimum, will flush any pixel data upon
// finding out that the input is truncated, so accept whatever we get.
}
if (ctx->target) {
@ -2959,6 +2958,88 @@ fiv_io_deserialize(GBytes *bytes, guint64 *user_data)
return surface;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static cairo_status_t
write_to_byte_array(
void *closure, const unsigned char *data, unsigned int length)
{
g_byte_array_append(closure, data, length);
return CAIRO_STATUS_SUCCESS;
}
GBytes *
fiv_io_serialize_for_search(cairo_surface_t *surface, GError **error)
{
g_return_val_if_fail(
surface && cairo_surface_get_type(surface) == CAIRO_SURFACE_TYPE_IMAGE,
NULL);
cairo_format_t format = cairo_image_surface_get_format(surface);
if (format == CAIRO_FORMAT_ARGB32) {
const uint32_t *data =
(const uint32_t *) cairo_image_surface_get_data(surface);
bool all_solid = true;
for (size_t len = cairo_image_surface_get_width(surface) *
cairo_image_surface_get_height(surface); len--; ) {
if ((data[len] >> 24) != 0xFF)
all_solid = false;
}
if (all_solid)
format = CAIRO_FORMAT_RGB24;
}
if (format != CAIRO_FORMAT_RGB24) {
#if CAIRO_HAS_PNG_FUNCTIONS
GByteArray *ba = g_byte_array_new();
cairo_status_t status =
cairo_surface_write_to_png_stream(surface, write_to_byte_array, ba);
if (status == CAIRO_STATUS_SUCCESS)
return g_byte_array_free_to_bytes(ba);
g_byte_array_unref(ba);
#endif
// Last resort: remove transparency by painting over black.
cairo_surface_t *converted =
cairo_image_surface_create(CAIRO_FORMAT_RGB24,
cairo_image_surface_get_width(surface),
cairo_image_surface_get_height(surface));
cairo_t *cr = cairo_create(converted);
cairo_set_source_surface(cr, surface, 0, 0);
cairo_set_operator(cr, CAIRO_OPERATOR_OVER);
cairo_paint(cr);
cairo_destroy(cr);
GBytes *result = fiv_io_serialize_for_search(converted, error);
cairo_surface_destroy(converted);
return result;
}
tjhandle enc = tjInitCompress();
if (!enc) {
set_error(error, tjGetErrorStr2(enc));
return NULL;
}
unsigned char *jpeg = NULL;
unsigned long length = 0;
if (tjCompress2(enc, cairo_image_surface_get_data(surface),
cairo_image_surface_get_width(surface),
cairo_image_surface_get_stride(surface),
cairo_image_surface_get_height(surface),
(G_BYTE_ORDER == G_LITTLE_ENDIAN ? TJPF_BGRX : TJPF_XRGB),
&jpeg, &length, TJSAMP_444, 90, 0)) {
set_error(error, tjGetErrorStr2(enc));
tjFree(jpeg);
tjDestroy(enc);
return NULL;
}
tjDestroy(enc);
return g_bytes_new_with_free_func(
jpeg, length, (GDestroyNotify) tjFree, jpeg);
}
// --- Filesystem --------------------------------------------------------------
#include "xdg.h"

View File

@ -1,7 +1,7 @@
//
// fiv-io.h: image operations
//
// Copyright (c) 2021 - 2022, Přemysl Eric Janouch <p@janouch.name>
// Copyright (c) 2021 - 2023, Přemysl Eric Janouch <p@janouch.name>
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted.
@ -107,6 +107,8 @@ enum { FIV_IO_SERIALIZE_LOW_QUALITY = 1 << 0 };
void fiv_io_serialize_to_stdout(cairo_surface_t *surface, guint64 user_data);
cairo_surface_t *fiv_io_deserialize(GBytes *bytes, guint64 *user_data);
GBytes *fiv_io_serialize_for_search(cairo_surface_t *surface, GError **error);
// --- Filesystem --------------------------------------------------------------
typedef enum _FivIoModelSort {

9
fiv-reverse-search Executable file
View File

@ -0,0 +1,9 @@
#!/bin/sh -e
if [ "$#" -ne 2 ]; then
echo "Usage: $0 SEARCH-ENGINE-URI-PREFIX {PATH | URI}" >&2
exit 1
fi
xdg-open "$1$(fiv --thumbnail-for-search large "$2" \
| curl --silent --show-error --upload-file - https://transfer.sh/image \
| jq --slurp --raw-input --raw-output @uri)"

View File

@ -0,0 +1,10 @@
[Desktop Entry]
Type=Application
Name=fiv @NAME@ Reverse Image Search
GenericName=@NAME@ Reverse Image Search
Icon=fiv
Exec=fiv-reverse-search "@URL@" %u
NoDisplay=true
Terminal=false
Categories=Graphics;2DGraphics;
MimeType=image/png;image/bmp;image/gif;image/x-tga;image/jpeg;image/webp;

View File

@ -1,7 +1,7 @@
//
// fiv-thumbnail.c: thumbnail management
//
// Copyright (c) 2021 - 2022, Přemysl Eric Janouch <p@janouch.name>
// Copyright (c) 2021 - 2023, Přemysl Eric Janouch <p@janouch.name>
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted.
@ -421,6 +421,29 @@ save_thumbnail(cairo_surface_t *thumbnail, const char *path, GString *thum)
WebPDataClear(&assembled);
}
cairo_surface_t *
fiv_thumbnail_produce_for_search(
GFile *target, FivThumbnailSize max_size, GError **error)
{
g_return_val_if_fail(max_size >= FIV_THUMBNAIL_SIZE_MIN &&
max_size <= FIV_THUMBNAIL_SIZE_MAX, NULL);
GBytes *data = g_file_load_bytes(target, NULL, NULL, error);
if (!data)
return NULL;
gboolean color_managed = FALSE;
cairo_surface_t *surface = render(target, data, &color_managed, error);
if (!surface)
return NULL;
// TODO(p): Might want to keep this a square.
cairo_surface_t *result =
adjust_thumbnail(surface, fiv_thumbnail_sizes[max_size].size);
cairo_surface_destroy(surface);
return result;
}
static cairo_surface_t *
produce_fallback(GFile *target, FivThumbnailSize size, GError **error)
{
@ -459,7 +482,7 @@ cairo_surface_t *
fiv_thumbnail_produce(GFile *target, FivThumbnailSize max_size, GError **error)
{
g_return_val_if_fail(max_size >= FIV_THUMBNAIL_SIZE_MIN &&
max_size <= FIV_THUMBNAIL_SIZE_MAX, FALSE);
max_size <= FIV_THUMBNAIL_SIZE_MAX, NULL);
// Don't save thumbnails for FUSE mounts, such as sftp://.
// Moreover, it doesn't make sense to save thumbnails of thumbnails.

View File

@ -1,7 +1,7 @@
//
// fiv-thumbnail.h: thumbnail management
//
// Copyright (c) 2021 - 2022, Přemysl Eric Janouch <p@janouch.name>
// Copyright (c) 2021 - 2023, Přemysl Eric Janouch <p@janouch.name>
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted.
@ -62,6 +62,10 @@ cairo_surface_t *fiv_thumbnail_extract(
cairo_surface_t *fiv_thumbnail_produce(
GFile *target, FivThumbnailSize max_size, GError **error);
/// Like fiv_thumbnail_produce(), but skips the cache.
cairo_surface_t *fiv_thumbnail_produce_for_search(
GFile *target, FivThumbnailSize max_size, GError **error);
/// Retrieves a thumbnail of the most appropriate quality and resolution
/// for the target file.
cairo_surface_t *fiv_thumbnail_lookup(

View File

@ -1,4 +1,8 @@
#!/bin/sh -e
sed -i "s|^MimeType=.*|MimeType=$(
"${DESTDIR:+$DESTDIR/}"'@EXE@' --list-supported-media-types | tr '\n' ';'
)|" "${DESTDIR:+$DESTDIR/}"'@DESKTOP@'
fiv=${DESTDIR:+$DESTDIR/}'@FIV@'
desktopdir=${DESTDIR:+$DESTDIR/}'@DESKTOPDIR@'
types=$("$fiv" --list-supported-media-types | tr '\n' ';')
for desktop in @DESKTOPS@
do sed -i "s|^MimeType=.*|MimeType=$types|" "$desktopdir"/"$desktop"
done

47
fiv.c
View File

@ -1,7 +1,7 @@
//
// fiv.c: fuck-if-I-know-how-to-name-it image browser and viewer
//
// Copyright (c) 2021 - 2022, Přemysl Eric Janouch <p@janouch.name>
// Copyright (c) 2021 - 2023, Přemysl Eric Janouch <p@janouch.name>
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted.
@ -1934,8 +1934,8 @@ static const char stylesheet[] = "@define-color fiv-tile @content_view_bg; \
} \
.fiv-information label { padding: 0 4px; }";
static void
output_thumbnail(gchar **uris, gboolean extract, const char *size_arg)
static FivThumbnailSize
output_thumbnail_prologue(gchar **uris, const char *size_arg)
{
if (!uris)
exit_fatal("No path given");
@ -1956,6 +1956,38 @@ output_thumbnail(gchar **uris, gboolean extract, const char *size_arg)
#ifdef G_OS_WIN32
_setmode(fileno(stdout), _O_BINARY);
#endif
return size;
}
static void
output_thumbnail_for_search(gchar **uris, const char *size_arg)
{
FivThumbnailSize size = output_thumbnail_prologue(uris, size_arg);
GError *error = NULL;
GFile *file = g_file_new_for_uri(uris[0]);
cairo_surface_t *surface = NULL;
GBytes *bytes = NULL;
if ((surface = fiv_thumbnail_produce(file, size, &error)) &&
(bytes = fiv_io_serialize_for_search(surface, &error))) {
fwrite(
g_bytes_get_data(bytes, NULL), 1, g_bytes_get_size(bytes), stdout);
g_bytes_unref(bytes);
} else {
g_assert(error != NULL);
}
g_object_unref(file);
if (error)
exit_fatal("%s", error->message);
cairo_surface_destroy(surface);
}
static void
output_thumbnail(gchar **uris, gboolean extract, const char *size_arg)
{
FivThumbnailSize size = output_thumbnail_prologue(uris, size_arg);
GError *error = NULL;
GFile *file = g_file_new_for_uri(uris[0]);
@ -1981,7 +2013,7 @@ main(int argc, char *argv[])
{
gboolean show_version = FALSE, show_supported_media_types = FALSE,
invalidate_cache = FALSE, browse = FALSE, extract_thumbnail = FALSE;
gchar **args = NULL, *thumbnail_size = NULL;
gchar **args = NULL, *thumbnail_size = NULL, *thumbnail_size_search = NULL;
const GOptionEntry options[] = {
{G_OPTION_REMAINING, 0, 0, G_OPTION_ARG_FILENAME_ARRAY, &args,
NULL, "[PATH | URI]..."},
@ -1991,6 +2023,9 @@ main(int argc, char *argv[])
{"browse", 0, G_OPTION_FLAG_IN_MAIN,
G_OPTION_ARG_NONE, &browse,
"Start in filesystem browsing mode", NULL},
{"thumbnail-for-search", 0, G_OPTION_FLAG_IN_MAIN,
G_OPTION_ARG_STRING, &thumbnail_size_search,
"Output an image file suitable for searching by content", "SIZE"},
{"extract-thumbnail", 0, G_OPTION_FLAG_IN_MAIN,
G_OPTION_ARG_NONE, &extract_thumbnail,
"Output any embedded thumbnail (superseding --thumbnail)", NULL},
@ -2032,6 +2067,10 @@ main(int argc, char *argv[])
args[i] = g_file_get_uri(resolved);
g_object_unref(resolved);
}
if (thumbnail_size_search) {
output_thumbnail_for_search(args, thumbnail_size_search);
return 0;
}
if (extract_thumbnail || thumbnail_size) {
output_thumbnail(args, extract_thumbnail, thumbnail_size);
return 0;

View File

@ -35,12 +35,12 @@ dependencies = [
dependency('gtk+-3.0'),
dependency('pixman-1'),
# Wuffs is included as a submodule.
dependency('libturbojpeg'),
dependency('libwebp'),
dependency('libwebpdemux'),
dependency('libwebpdecoder', required : false),
dependency('libwebpmux'),
# Wuffs is included as a submodule.
lcms2,
libjpegqs,
@ -251,6 +251,32 @@ if not win32
install_dir : get_option('datadir') / 'applications')
endforeach
# TODO(p): Consider moving this to /usr/share or /usr/lib.
install_data('fiv-reverse-search',
install_dir : get_option('bindir'))
# As usual, handling generated files in Meson is a fucking pain.
updatable_desktops = [application_ns + 'fiv.desktop']
foreach name, uri : {
'Google' : 'https://lens.google.com/uploadbyurl?url=',
'Bing' : 'https://www.bing.com/images/searchbyimage?cbir=sbi&imgurl=',
'Yandex' : 'https://yandex.com/images/search?rpt=imageview&url=',
'TinEye' : 'https://tineye.com/search?url=',
'SauceNAO' : 'https://saucenao.com/search.php?url=',
'IQDB' : 'https://iqdb.org/?url=',
}
desktop = 'fiv-reverse-search-' + name.to_lower() + '.desktop'
updatable_desktops += application_ns + desktop
test(desktop, dfv, args : configure_file(
input : 'fiv-reverse-search.desktop.in',
output : application_ns + desktop,
configuration : {'NAME' : name, 'URL' : uri},
install : true,
install_dir : get_option('datadir') / 'applications',
))
endforeach
# With gdk-pixbuf, fiv.desktop depends on currently installed modules;
# the package manager needs to be told to run this script as necessary.
dynamic_desktops = gdkpixbuf.found()
@ -259,9 +285,10 @@ if not win32
input : 'fiv-update-desktop-files.in',
output : 'fiv-update-desktop-files',
configuration : {
'EXE' : get_option('prefix') / get_option('bindir') / exe.name(),
'DESKTOP' : get_option('prefix') / get_option('datadir') \
/ 'applications' / application_ns + 'fiv.desktop',
'FIV' : get_option('prefix') / get_option('bindir') / exe.name(),
'DESKTOPDIR' : get_option('prefix') /
get_option('datadir') / 'applications',
'DESKTOPS' : ' \\\n\t'.join(updatable_desktops),
},
install : dynamic_desktops,
install_dir : get_option('bindir'))

@ -1 +1 @@
Subproject commit 0b70377a12c81cf88afe259f2b4085eb7a59eb59
Subproject commit 40ff91b31b3286aa92fd3cb4656975b275ef8b10