aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--README.adoc7
-rw-r--r--fiv-io.c84
-rw-r--r--fiv-io.h4
-rwxr-xr-xfiv-reverse-search9
-rw-r--r--fiv-reverse-search.desktop.in10
-rw-r--r--fiv-thumbnail.c27
-rw-r--r--fiv-thumbnail.h6
-rwxr-xr-xfiv-update-desktop-files.in10
-rw-r--r--fiv.c47
-rw-r--r--meson.build35
10 files changed, 221 insertions, 18 deletions
diff --git a/README.adoc b/README.adoc
index 79c3bf2..4404457 100644
--- a/README.adoc
+++ b/README.adoc
@@ -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
~~~~~~~
diff --git a/fiv-io.c b/fiv-io.c
index 74dd56e..a995940 100644
--- a/fiv-io.c
+++ b/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"
diff --git a/fiv-io.h b/fiv-io.h
index 9cbe5d8..484e43b 100644
--- a/fiv-io.h
+++ b/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 {
diff --git a/fiv-reverse-search b/fiv-reverse-search
new file mode 100755
index 0000000..5210703
--- /dev/null
+++ b/fiv-reverse-search
@@ -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)"
diff --git a/fiv-reverse-search.desktop.in b/fiv-reverse-search.desktop.in
new file mode 100644
index 0000000..49d5de3
--- /dev/null
+++ b/fiv-reverse-search.desktop.in
@@ -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;
diff --git a/fiv-thumbnail.c b/fiv-thumbnail.c
index d0ec91a..1f6897f 100644
--- a/fiv-thumbnail.c
+++ b/fiv-thumbnail.c
@@ -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.
diff --git a/fiv-thumbnail.h b/fiv-thumbnail.h
index 7f3360a..de5be51 100644
--- a/fiv-thumbnail.h
+++ b/fiv-thumbnail.h
@@ -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(
diff --git a/fiv-update-desktop-files.in b/fiv-update-desktop-files.in
index bbbe9a9..1c8568f 100755
--- a/fiv-update-desktop-files.in
+++ b/fiv-update-desktop-files.in
@@ -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
diff --git a/fiv.c b/fiv.c
index d58a5a5..3dfe058 100644
--- a/fiv.c
+++ b/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;
diff --git a/meson.build b/meson.build
index 1be61ff..8571101 100644
--- a/meson.build
+++ b/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'))