diff options
| -rw-r--r-- | README.adoc | 7 | ||||
| -rw-r--r-- | fiv-io.c | 84 | ||||
| -rw-r--r-- | fiv-io.h | 4 | ||||
| -rwxr-xr-x | fiv-reverse-search | 9 | ||||
| -rw-r--r-- | fiv-reverse-search.desktop.in | 10 | ||||
| -rw-r--r-- | fiv-thumbnail.c | 27 | ||||
| -rw-r--r-- | fiv-thumbnail.h | 6 | ||||
| -rwxr-xr-x | fiv-update-desktop-files.in | 10 | ||||
| -rw-r--r-- | fiv.c | 47 | ||||
| -rw-r--r-- | meson.build | 35 | 
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  ~~~~~~~ @@ -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" @@ -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 @@ -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'))  | 
