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:
parent
9286858573
commit
796b05c9a5
|
@ -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
|
||||
~~~~~~~
|
||||
|
|
84
fiv-io.c
84
fiv-io.c
|
@ -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.
|
||||
|
@ -2958,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"
|
||||
|
|
4
fiv-io.h
4
fiv-io.h
|
@ -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 {
|
||||
|
|
|
@ -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)"
|
|
@ -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;
|
|
@ -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.
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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
47
fiv.c
|
@ -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;
|
||||
|
|
35
meson.build
35
meson.build
|
@ -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'))
|
||||
|
|
Loading…
Reference in New Issue