Compare commits
3 Commits
95bc91e020
...
25c91f5a77
Author | SHA1 | Date | |
---|---|---|---|
25c91f5a77 | |||
796b05c9a5 | |||
9286858573 |
@ -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
|
||||
~~~~~~~
|
||||
|
@ -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);
|
||||
|
89
fiv-io.c
89
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.
|
||||
@ -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"
|
||||
|
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 {
|
||||
|
9
fiv-reverse-search
Executable file
9
fiv-reverse-search
Executable 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)"
|
10
fiv-reverse-search.desktop.in
Normal file
10
fiv-reverse-search.desktop.in
Normal 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;
|
@ -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'))
|
||||
|
@ -1 +1 @@
|
||||
Subproject commit 0b70377a12c81cf88afe259f2b4085eb7a59eb59
|
||||
Subproject commit 40ff91b31b3286aa92fd3cb4656975b275ef8b10
|
Loading…
x
Reference in New Issue
Block a user