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.
This commit is contained in:
Přemysl Eric Janouch 2023-03-15 03:31:30 +01:00
parent 9286858573
commit 796b05c9a5
Signed by: p
GPG Key ID: A0420B94F92B9493
10 changed files with 221 additions and 18 deletions

View File

@ -43,7 +43,9 @@ Build-only dependencies:
Runtime dependencies: Runtime dependencies:
gtk+-3.0, glib>=2.64, pixman-1, shared-mime-info, libturbojpeg, libwebp + gtk+-3.0, glib>=2.64, pixman-1, shared-mime-info, libturbojpeg, libwebp +
Optional dependencies: lcms2, LibRaw, librsvg-2.0, xcursor, libheif, libtiff, 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 $ git clone --recursive https://git.janouch.name/p/fiv.git
$ meson builddir $ meson builddir
@ -55,7 +57,8 @@ direct installations via `ninja install`. To test the program:
$ meson devenv fiv $ 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 Windows
~~~~~~~ ~~~~~~~

View File

@ -1,7 +1,7 @@
// //
// fiv-io.c: image operations // 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 // Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted. // purpose with or without fee is hereby granted.
@ -2958,6 +2958,88 @@ fiv_io_deserialize(GBytes *bytes, guint64 *user_data)
return surface; 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 -------------------------------------------------------------- // --- Filesystem --------------------------------------------------------------
#include "xdg.h" #include "xdg.h"

View File

@ -1,7 +1,7 @@
// //
// fiv-io.h: image operations // 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 // Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted. // 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); void fiv_io_serialize_to_stdout(cairo_surface_t *surface, guint64 user_data);
cairo_surface_t *fiv_io_deserialize(GBytes *bytes, 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 -------------------------------------------------------------- // --- Filesystem --------------------------------------------------------------
typedef enum _FivIoModelSort { 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 // 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 // Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted. // 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); 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 * static cairo_surface_t *
produce_fallback(GFile *target, FivThumbnailSize size, GError **error) 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) fiv_thumbnail_produce(GFile *target, FivThumbnailSize max_size, GError **error)
{ {
g_return_val_if_fail(max_size >= FIV_THUMBNAIL_SIZE_MIN && 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://. // Don't save thumbnails for FUSE mounts, such as sftp://.
// Moreover, it doesn't make sense to save thumbnails of thumbnails. // Moreover, it doesn't make sense to save thumbnails of thumbnails.

View File

@ -1,7 +1,7 @@
// //
// fiv-thumbnail.h: thumbnail management // 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 // Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted. // purpose with or without fee is hereby granted.
@ -62,6 +62,10 @@ cairo_surface_t *fiv_thumbnail_extract(
cairo_surface_t *fiv_thumbnail_produce( cairo_surface_t *fiv_thumbnail_produce(
GFile *target, FivThumbnailSize max_size, GError **error); 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 /// Retrieves a thumbnail of the most appropriate quality and resolution
/// for the target file. /// for the target file.
cairo_surface_t *fiv_thumbnail_lookup( cairo_surface_t *fiv_thumbnail_lookup(

View File

@ -1,4 +1,8 @@
#!/bin/sh -e #!/bin/sh -e
sed -i "s|^MimeType=.*|MimeType=$( fiv=${DESTDIR:+$DESTDIR/}'@FIV@'
"${DESTDIR:+$DESTDIR/}"'@EXE@' --list-supported-media-types | tr '\n' ';' desktopdir=${DESTDIR:+$DESTDIR/}'@DESKTOPDIR@'
)|" "${DESTDIR:+$DESTDIR/}"'@DESKTOP@'
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 // 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 // Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted. // 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; }"; .fiv-information label { padding: 0 4px; }";
static void static FivThumbnailSize
output_thumbnail(gchar **uris, gboolean extract, const char *size_arg) output_thumbnail_prologue(gchar **uris, const char *size_arg)
{ {
if (!uris) if (!uris)
exit_fatal("No path given"); exit_fatal("No path given");
@ -1956,6 +1956,38 @@ output_thumbnail(gchar **uris, gboolean extract, const char *size_arg)
#ifdef G_OS_WIN32 #ifdef G_OS_WIN32
_setmode(fileno(stdout), _O_BINARY); _setmode(fileno(stdout), _O_BINARY);
#endif #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; GError *error = NULL;
GFile *file = g_file_new_for_uri(uris[0]); 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, gboolean show_version = FALSE, show_supported_media_types = FALSE,
invalidate_cache = FALSE, browse = FALSE, extract_thumbnail = 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[] = { const GOptionEntry options[] = {
{G_OPTION_REMAINING, 0, 0, G_OPTION_ARG_FILENAME_ARRAY, &args, {G_OPTION_REMAINING, 0, 0, G_OPTION_ARG_FILENAME_ARRAY, &args,
NULL, "[PATH | URI]..."}, NULL, "[PATH | URI]..."},
@ -1991,6 +2023,9 @@ main(int argc, char *argv[])
{"browse", 0, G_OPTION_FLAG_IN_MAIN, {"browse", 0, G_OPTION_FLAG_IN_MAIN,
G_OPTION_ARG_NONE, &browse, G_OPTION_ARG_NONE, &browse,
"Start in filesystem browsing mode", NULL}, "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, {"extract-thumbnail", 0, G_OPTION_FLAG_IN_MAIN,
G_OPTION_ARG_NONE, &extract_thumbnail, G_OPTION_ARG_NONE, &extract_thumbnail,
"Output any embedded thumbnail (superseding --thumbnail)", NULL}, "Output any embedded thumbnail (superseding --thumbnail)", NULL},
@ -2032,6 +2067,10 @@ main(int argc, char *argv[])
args[i] = g_file_get_uri(resolved); args[i] = g_file_get_uri(resolved);
g_object_unref(resolved); g_object_unref(resolved);
} }
if (thumbnail_size_search) {
output_thumbnail_for_search(args, thumbnail_size_search);
return 0;
}
if (extract_thumbnail || thumbnail_size) { if (extract_thumbnail || thumbnail_size) {
output_thumbnail(args, extract_thumbnail, thumbnail_size); output_thumbnail(args, extract_thumbnail, thumbnail_size);
return 0; return 0;

View File

@ -35,12 +35,12 @@ dependencies = [
dependency('gtk+-3.0'), dependency('gtk+-3.0'),
dependency('pixman-1'), dependency('pixman-1'),
# Wuffs is included as a submodule.
dependency('libturbojpeg'), dependency('libturbojpeg'),
dependency('libwebp'), dependency('libwebp'),
dependency('libwebpdemux'), dependency('libwebpdemux'),
dependency('libwebpdecoder', required : false), dependency('libwebpdecoder', required : false),
dependency('libwebpmux'), dependency('libwebpmux'),
# Wuffs is included as a submodule.
lcms2, lcms2,
libjpegqs, libjpegqs,
@ -251,6 +251,32 @@ if not win32
install_dir : get_option('datadir') / 'applications') install_dir : get_option('datadir') / 'applications')
endforeach 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; # With gdk-pixbuf, fiv.desktop depends on currently installed modules;
# the package manager needs to be told to run this script as necessary. # the package manager needs to be told to run this script as necessary.
dynamic_desktops = gdkpixbuf.found() dynamic_desktops = gdkpixbuf.found()
@ -259,9 +285,10 @@ if not win32
input : 'fiv-update-desktop-files.in', input : 'fiv-update-desktop-files.in',
output : 'fiv-update-desktop-files', output : 'fiv-update-desktop-files',
configuration : { configuration : {
'EXE' : get_option('prefix') / get_option('bindir') / exe.name(), 'FIV' : get_option('prefix') / get_option('bindir') / exe.name(),
'DESKTOP' : get_option('prefix') / get_option('datadir') \ 'DESKTOPDIR' : get_option('prefix') /
/ 'applications' / application_ns + 'fiv.desktop', get_option('datadir') / 'applications',
'DESKTOPS' : ' \\\n\t'.join(updatable_desktops),
}, },
install : dynamic_desktops, install : dynamic_desktops,
install_dir : get_option('bindir')) install_dir : get_option('bindir'))