Commit 06cf4c78 authored by Bastien Nocera's avatar Bastien Nocera

thumbnailer: Add an external thumbnailer for images

So that broken images, or images that use too much RAM can get killed
without prejudice.

_gdk_pixbuf_new_from_uri_at_scale() and
gnome_desktop_thumbnail_scale_down_pixbuf () are directly from
gnome-desktop.

https://bugzilla.gnome.org/show_bug.cgi?id=768062
parent 85b4f6c2
SUBDIRS = gdk-pixbuf po docs tests contrib build SUBDIRS = gdk-pixbuf po docs thumbnailer tests contrib build
EXTRA_DIST = \ EXTRA_DIST = \
config.h.win32 \ config.h.win32 \
......
...@@ -1121,6 +1121,7 @@ docs/reference/gdk-pixbuf/Makefile ...@@ -1121,6 +1121,7 @@ docs/reference/gdk-pixbuf/Makefile
docs/reference/gdk-pixbuf/version.xml docs/reference/gdk-pixbuf/version.xml
po/Makefile.in po/Makefile.in
tests/Makefile tests/Makefile
thumbnailer/Makefile
contrib/Makefile contrib/Makefile
contrib/gdk-pixbuf-xlib/Makefile contrib/gdk-pixbuf-xlib/Makefile
contrib/gdk-pixbuf-xlib/gdk-pixbuf-xlib-2.0.pc contrib/gdk-pixbuf-xlib/gdk-pixbuf-xlib-2.0.pc
......
bin_PROGRAMS = gdk-pixbuf-thumbnailer
noinst_PROGRAMS = gdk-pixbuf-print-mime-types
gdk_pixbuf_thumbnailer_SOURCES = gdk-pixbuf-thumbnailer.c gnome-thumbnailer-skeleton.c gnome-thumbnailer-skeleton.h
gdk_pixbuf_thumbnailer_CPPFLAGS = \
-I$(top_srcdir) \
-I$(top_srcdir)/gdk-pixbuf \
$(GDK_PIXBUF_DEP_CFLAGS) \
-DTHUMBNAILER_RETURNS_PIXBUF \
-DTHUMBNAILER_USAGE="\"Thumbnail images\"" \
$(WARN_CFLAGS)
gdk_pixbuf_thumbnailer_LDADD = \
$(GDK_PIXBUF_DEP_LIBS) \
$(top_builddir)/gdk-pixbuf/libgdk_pixbuf-$(GDK_PIXBUF_API_VERSION).la
gdk_pixbuf_print_mime_types_SOURCES = gdk-pixbuf-print-mime-types.c
gdk_pixbuf_print_mime_types_CPPFLAGS = \
-I$(top_srcdir) \
-I$(top_srcdir)/gdk-pixbuf \
$(GDK_PIXBUF_DEP_CFLAGS) \
$(WARN_CFLAGS)
gdk_pixbuf_print_mime_types_LDADD = \
$(GDK_PIXBUF_DEP_LIBS) \
$(top_builddir)/gdk-pixbuf/libgdk_pixbuf-$(GDK_PIXBUF_API_VERSION).la
thumbnailerdir = $(datadir)/thumbnailers/
thumbnailer_DATA = gdk-pixbuf-thumbnailer.thumbnailer
gdk-pixbuf-thumbnailer.thumbnailer: gdk-pixbuf-thumbnailer.thumbnailer.in Makefile gdk-pixbuf-print-mime-types
$(AM_V_GEN) GDK_PIXBUF_MODULE_FILE=$(top_builddir)/gdk-pixbuf/loaders.cache \
GDK_PIXBUF_PIXDATA=$(top_builddir)/gdk-pixbuf/gdk-pixbuf-pixdata \
$(SED) -e "s|\@bindir\@|$(bindir)|" \
-e "s|\@mimetypes\@|`$(builddir)/gdk-pixbuf-print-mime-types`|" $< > $@
EXTRA_DIST = gdk-pixbuf-thumbnailer.thumbnailer.in
CLEANFILES = $(thumbnailer_DATA)
#include <gdk-pixbuf/gdk-pixbuf.h>
int main (int argc, char **argv)
{
GSList *formats, *l;
GString *s;
formats = gdk_pixbuf_get_formats ();
s = g_string_new (NULL);
for (l = formats; l != NULL; l = l->next) {
GdkPixbufFormat *format = l->data;
char **mime_types;
guint i;
mime_types = gdk_pixbuf_format_get_mime_types (format);
for (i = 0; mime_types[i] != NULL; i++) {
g_string_append (s, mime_types[i]);
g_string_append (s, ";");
}
g_strfreev (mime_types);
}
g_slist_free (formats);
g_print ("%s", s->str);
g_string_free (s, TRUE);
return 0;
}
/*
* Copyright (C) 2016 Bastien Nocera <hadess@hadess.net>
*
* Authors: Bastien Nocera <hadess@hadess.net>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*
*/
#include <string.h>
#include <glib.h>
#include "gnome-thumbnailer-skeleton.h"
typedef struct {
gint width;
gint height;
gint input_width;
gint input_height;
gboolean preserve_aspect_ratio;
} SizePrepareContext;
#define LOAD_BUFFER_SIZE 4096
static void
size_prepared_cb (GdkPixbufLoader *loader,
int width,
int height,
gpointer data)
{
SizePrepareContext *info = data;
g_return_if_fail (width > 0 && height > 0);
info->input_width = width;
info->input_height = height;
if (width < info->width && height < info->height) return;
if (info->preserve_aspect_ratio &&
(info->width > 0 || info->height > 0)) {
if (info->width < 0)
{
width = width * (double)info->height/(double)height;
height = info->height;
}
else if (info->height < 0)
{
height = height * (double)info->width/(double)width;
width = info->width;
}
else if ((double)height * (double)info->width >
(double)width * (double)info->height) {
width = 0.5 + (double)width * (double)info->height / (double)height;
height = info->height;
} else {
height = 0.5 + (double)height * (double)info->width / (double)width;
width = info->width;
}
} else {
if (info->width > 0)
width = info->width;
if (info->height > 0)
height = info->height;
}
gdk_pixbuf_loader_set_size (loader, width, height);
}
static GdkPixbufLoader *
create_loader (GFile *file,
const guchar *data,
gsize size)
{
GdkPixbufLoader *loader;
GError *error = NULL;
char *mime_type;
char *filename;
loader = NULL;
/* need to specify the type here because the gdk_pixbuf_loader_write
doesn't have access to the filename in order to correct detect
the image type. */
filename = g_file_get_basename (file);
mime_type = g_content_type_guess (filename, data, size, NULL);
g_free (filename);
if (mime_type != NULL) {
loader = gdk_pixbuf_loader_new_with_mime_type (mime_type, &error);
}
if (loader == NULL) {
g_debug ("Unable to create loader for mime type %s: %s", mime_type, error->message);
g_clear_error (&error);
loader = gdk_pixbuf_loader_new ();
}
g_free (mime_type);
return loader;
}
static GdkPixbuf *
_gdk_pixbuf_new_from_uri_at_scale (const char *uri,
gint width,
gint height,
gboolean preserve_aspect_ratio)
{
gboolean result;
guchar buffer[LOAD_BUFFER_SIZE];
gssize bytes_read;
GdkPixbufLoader *loader = NULL;
GdkPixbuf *pixbuf;
GdkPixbufAnimation *animation;
GdkPixbufAnimationIter *iter;
gboolean has_frame;
SizePrepareContext info;
GFile *file;
GFileInfo *file_info;
GInputStream *input_stream;
GError *error = NULL;
g_return_val_if_fail (uri != NULL, NULL);
input_stream = NULL;
file = g_file_new_for_uri (uri);
/* First see if we can get an input stream via preview::icon */
file_info = g_file_query_info (file,
G_FILE_ATTRIBUTE_PREVIEW_ICON,
G_FILE_QUERY_INFO_NONE,
NULL, /* GCancellable */
NULL); /* return location for GError */
if (file_info != NULL) {
GObject *object;
object = g_file_info_get_attribute_object (file_info,
G_FILE_ATTRIBUTE_PREVIEW_ICON);
if (object != NULL && G_IS_LOADABLE_ICON (object)) {
input_stream = g_loadable_icon_load (G_LOADABLE_ICON (object),
0, /* size */
NULL, /* return location for type */
NULL, /* GCancellable */
NULL); /* return location for GError */
}
g_object_unref (file_info);
}
if (input_stream == NULL) {
input_stream = G_INPUT_STREAM (g_file_read (file, NULL, &error));
if (input_stream == NULL) {
g_warning ("Unable to create an input stream for %s: %s", uri, error->message);
g_clear_error (&error);
g_object_unref (file);
return NULL;
}
}
has_frame = FALSE;
result = FALSE;
while (!has_frame) {
bytes_read = g_input_stream_read (input_stream,
buffer,
sizeof (buffer),
NULL,
&error);
if (bytes_read == -1) {
g_warning ("Error reading from %s: %s", uri, error->message);
g_clear_error (&error);
break;
}
result = TRUE;
if (bytes_read == 0) {
break;
}
if (loader == NULL) {
loader = create_loader (file, buffer, bytes_read);
if (1 <= width || 1 <= height) {
info.width = width;
info.height = height;
info.input_width = info.input_height = 0;
info.preserve_aspect_ratio = preserve_aspect_ratio;
g_signal_connect (loader, "size-prepared", G_CALLBACK (size_prepared_cb), &info);
}
g_assert (loader != NULL);
}
if (!gdk_pixbuf_loader_write (loader,
(unsigned char *)buffer,
bytes_read,
&error)) {
g_warning ("Error creating thumbnail for %s: %s", uri, error->message);
g_clear_error (&error);
result = FALSE;
break;
}
animation = gdk_pixbuf_loader_get_animation (loader);
if (animation) {
iter = gdk_pixbuf_animation_get_iter (animation, NULL);
if (!gdk_pixbuf_animation_iter_on_currently_loading_frame (iter)) {
has_frame = TRUE;
}
g_object_unref (iter);
}
}
if (loader == NULL) {
/* This can happen if the above loop was exited due to the
* g_input_stream_read() call failing. */
result = FALSE;
} else if (gdk_pixbuf_loader_close (loader, &error) == FALSE &&
!g_error_matches (error, GDK_PIXBUF_ERROR, GDK_PIXBUF_ERROR_INCOMPLETE_ANIMATION)) {
g_warning ("Error creating thumbnail for %s: %s", uri, error->message);
result = FALSE;
}
g_clear_error (&error);
if (!result) {
g_clear_object (&loader);
g_input_stream_close (input_stream, NULL, NULL);
g_object_unref (input_stream);
g_object_unref (file);
return NULL;
}
g_input_stream_close (input_stream, NULL, NULL);
g_object_unref (input_stream);
g_object_unref (file);
pixbuf = gdk_pixbuf_loader_get_pixbuf (loader);
if (pixbuf != NULL) {
g_object_ref (G_OBJECT (pixbuf));
g_object_set_data (G_OBJECT (pixbuf), "gnome-original-width",
GINT_TO_POINTER (info.input_width));
g_object_set_data (G_OBJECT (pixbuf), "gnome-original-height",
GINT_TO_POINTER (info.input_height));
}
g_object_unref (G_OBJECT (loader));
return pixbuf;
}
GdkPixbuf *
file_to_pixbuf (const char *path,
guint destination_size,
GError **error)
{
GdkPixbuf *pixbuf, *tmp_pixbuf;
GFile *file;
char *uri;
int original_width, original_height;
file = g_file_new_for_path (path);
uri = g_file_get_uri (file);
pixbuf = _gdk_pixbuf_new_from_uri_at_scale (uri, destination_size, destination_size, TRUE);
if (pixbuf == NULL) {
g_set_error_literal (error, G_IO_ERROR, G_IO_ERROR_FAILED,
"Generic error");
return pixbuf;
}
tmp_pixbuf = gdk_pixbuf_apply_embedded_orientation (pixbuf);
gdk_pixbuf_copy_options (pixbuf, tmp_pixbuf);
gdk_pixbuf_remove_option (tmp_pixbuf, "orientation");
g_object_unref (pixbuf);
pixbuf = tmp_pixbuf;
original_width = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (pixbuf),
"gnome-original-width"));
original_height = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (pixbuf),
"gnome-original-height"));
if (original_width > 0 && original_height > 0) {
char *tmp;
tmp = g_strdup_printf ("%d", original_width);
gdk_pixbuf_set_option (pixbuf, "tEXt::Thumb::Image::Width", tmp);
g_free (tmp);
tmp = g_strdup_printf ("%d", original_height);
gdk_pixbuf_set_option (pixbuf, "tEXt::Thumb::Image::Height", tmp);
g_free (tmp);
}
return pixbuf;
}
[Thumbnailer Entry]
TryExec=@bindir@/gdk-pixbuf-thumbnailer
Exec=@bindir@/gdk-pixbuf-thumbnailer -s %s %u %o
MimeType=@mimetypes@
/*
* Copyright (C) 2013 Bastien Nocera <hadess@hadess.net>
*
* Authors: Bastien Nocera <hadess@hadess.net>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*
*/
#include <string.h>
#include <glib.h>
#include <gio/gio.h>
#include <gdk-pixbuf/gdk-pixbuf.h>
#include <math.h>
#include <stdlib.h>
#include "gnome-thumbnailer-skeleton.h"
#ifndef THUMBNAILER_USAGE
#error "THUMBNAILER_USAGE must be set"
#endif
static int output_size = 256;
static gboolean g_fatal_warnings = FALSE;
static char **filenames = NULL;
/**
* gnome_desktop_thumbnail_scale_down_pixbuf:
* @pixbuf: a #GdkPixbuf
* @dest_width: the desired new width
* @dest_height: the desired new height
*
* Scales the pixbuf to the desired size. This function
* is a lot faster than gdk-pixbuf when scaling down by
* large amounts.
*
* Return value: (transfer full): a scaled pixbuf
*
* Since: 2.2
**/
GdkPixbuf *
gnome_desktop_thumbnail_scale_down_pixbuf (GdkPixbuf *pixbuf,
int dest_width,
int dest_height)
{
int source_width, source_height;
int s_x1, s_y1, s_x2, s_y2;
int s_xfrac, s_yfrac;
int dx, dx_frac, dy, dy_frac;
div_t ddx, ddy;
int x, y;
int r, g, b, a;
int n_pixels;
gboolean has_alpha;
guchar *dest, *src, *xsrc, *src_pixels;
GdkPixbuf *dest_pixbuf;
int pixel_stride;
int source_rowstride, dest_rowstride;
if (dest_width == 0 || dest_height == 0) {
return NULL;
}
source_width = gdk_pixbuf_get_width (pixbuf);
source_height = gdk_pixbuf_get_height (pixbuf);
g_assert (source_width >= dest_width);
g_assert (source_height >= dest_height);
ddx = div (source_width, dest_width);
dx = ddx.quot;
dx_frac = ddx.rem;
ddy = div (source_height, dest_height);
dy = ddy.quot;
dy_frac = ddy.rem;
has_alpha = gdk_pixbuf_get_has_alpha (pixbuf);
source_rowstride = gdk_pixbuf_get_rowstride (pixbuf);
src_pixels = gdk_pixbuf_get_pixels (pixbuf);
dest_pixbuf = gdk_pixbuf_new (GDK_COLORSPACE_RGB, has_alpha, 8,
dest_width, dest_height);
dest = gdk_pixbuf_get_pixels (dest_pixbuf);
dest_rowstride = gdk_pixbuf_get_rowstride (dest_pixbuf);
pixel_stride = (has_alpha)?4:3;
s_y1 = 0;
s_yfrac = -dest_height/2;
while (s_y1 < source_height) {
s_y2 = s_y1 + dy;
s_yfrac += dy_frac;
if (s_yfrac > 0) {
s_y2++;
s_yfrac -= dest_height;
}
s_x1 = 0;
s_xfrac = -dest_width/2;
while (s_x1 < source_width) {
s_x2 = s_x1 + dx;
s_xfrac += dx_frac;
if (s_xfrac > 0) {
s_x2++;
s_xfrac -= dest_width;
}
/* Average block of [x1,x2[ x [y1,y2[ and store in dest */
r = g = b = a = 0;
n_pixels = 0;
src = src_pixels + s_y1 * source_rowstride + s_x1 * pixel_stride;
for (y = s_y1; y < s_y2; y++) {
xsrc = src;
if (has_alpha) {
for (x = 0; x < s_x2-s_x1; x++) {
n_pixels++;
r += xsrc[3] * xsrc[0];
g += xsrc[3] * xsrc[1];
b += xsrc[3] * xsrc[2];
a += xsrc[3];
xsrc += 4;
}
} else {
for (x = 0; x < s_x2-s_x1; x++) {
n_pixels++;
r += *xsrc++;
g += *xsrc++;
b += *xsrc++;
}
}
src += source_rowstride;
}
if (has_alpha) {
if (a != 0) {
*dest++ = r / a;
*dest++ = g / a;
*dest++ = b / a;
*dest++ = a / n_pixels;
} else {
*dest++ = 0;
*dest++ = 0;
*dest++ = 0;
*dest++ = 0;
}
} else {
*dest++ = r / n_pixels;
*dest++ = g / n_pixels;
*dest++ = b / n_pixels;
}
s_x1 = s_x2;
}
s_y1 = s_y2;
dest += dest_rowstride - dest_width * pixel_stride;
}
return dest_pixbuf;
}
static char *
get_target_uri (GFile *file)
{
GFileInfo *info;
char *target;
info = g_file_query_info (file, G_FILE_ATTRIBUTE_STANDARD_TARGET_URI, G_FILE_QUERY_INFO_NONE, NULL, NULL);
if (info == NULL)
return NULL;
target = g_strdup (g_file_info_get_attribute_string (info, G_FILE_ATTRIBUTE_STANDARD_TARGET_URI));
g_object_unref (info);
return target;
}
static char *
get_target_path (GFile *input)
{
if (g_file_has_uri_scheme (input, "trash") != FALSE ||
g_file_has_uri_scheme (input, "recent") != FALSE) {
GFile *file;
char *input_uri;
char *input_path;
input_uri = get_target_uri (input);
file = g_file_new_for_uri (input_uri);
g_free (input_uri);
input_path = g_file_get_path (file);
g_object_unref (file);
return input_path;
}
return g_file_get_path (input);
}
static const GOptionEntry entries[] = {
{ "size", 's', 0, G_OPTION_ARG_INT, &output_size, "Size of the thumbnail in pixels", NULL },
{"g-fatal-warnings", 0, 0, G_OPTION_ARG_NONE, &g_fatal_warnings, "Make all warnings fatal", NULL},
{ G_OPTION_REMAINING, '\0', 0, G_OPTION_ARG_FILENAME_ARRAY, &filenames, NULL, "[INPUT FILE] [OUTPUT FILE]" },
{ NULL }
};
int main (int argc, char **argv)
{
char *input_filename;
GdkPixbuf *pixbuf;
GError *error = NULL;
GOptionContext *context;
GFile *input;
const char *output;
#ifdef THUMBNAILER_RETURNS_PIXBUF
int width, height;
#elif THUMBNAILER_RETURNS_DATA
char *data = NULL;
gsize length;
#endif
g_type_init ();
/* Options parsing */
context = g_option_context_new (THUMBNAILER_USAGE);
g_option_context_add_main_entries (context, entries, NULL);
if (g_option_context_parse (context, &argc, &argv, &error) == FALSE) {
g_warning ("Couldn't parse command-line options: %s", error->message);
g_error_free (error);
return 1;
}
/* Set fatal warnings if required */
if (g_fatal_warnings) {
GLogLevelFlags fatal_mask;
fatal_mask = g_log_set_always_fatal (G_LOG_FATAL_MASK);
fatal_mask |= G_LOG_LEVEL_WARNING | G_LOG_LEVEL_CRITICAL;
g_log_set_always_fatal (fatal_mask);
}
if (filenames == NULL || g_strv_length (filenames) != 2) {
g_print ("Expects an input and an output file\n");
return 1;
}
input = g_file_new_for_commandline_arg (filenames[0]);
input_filename = get_target_path (input);
g_object_unref (input);
if (input_filename == NULL) {
g_warning ("Could not get file path for %s", filenames[0]);
return 1;
}
output = filenames[1];
#ifdef THUMBNAILER_RETURNS_PIXBUF
pixbuf = file_to_pixbuf (input_filename, output_size, &error);
width = gdk_pixbuf_get_width (pixbuf);
height = gdk_pixbuf_get_height (pixbuf);
/* Handle naive thumbnailers that don't resize */
if (output_size != 0 &&
(height > output_size || width > output_size)) {
GdkPixbuf *scaled;
double scale;
scale = (double)output_size / MAX (width, height);
scaled = gnome_desktop_thumbnail_scale_down_pixbuf (pixbuf,
floor (width * scale + 0.5),
floor (height * scale + 0.5));
gdk_pixbuf_copy_options (pixbuf, scaled);
g_object_unref (pixbuf);
pixbuf = scaled;
}
#elif THUMBNAILER_RETURNS_DATA
data = file_to_data (input_filename, &length, &error);
if (data) {
GInputStream *mem_stream;
mem_stream = g_memory_input_stream_new_from_data (data, length, g_free);
pixbuf = gdk_pixbuf_new_from_stream_at_scale (mem_stream, output_size, -1, TRUE, NULL, &error);
g_object_unref (mem_stream);
} else {
pixbuf = NULL;
}
#else
#error "One of THUMBNAILER_RETURNS_PIXBUF or THUMBNAILER_RETURNS_DATA must be set"
#endif
g_free (input_filename);
if (!pixbuf) {
g_warning ("Could not thumbnail '%s': %s", filenames[0], error->message);
g_error_free (error);
g_strfreev (filenames);
return 1;
}
if (gdk_pixbuf_save (pixbuf, output, "png", &error, NULL) == FALSE) {
g_warning ("Couldn't save the thumbnail '%s' for file '%s': %s", output, filenames[0], error->message);
g_error_free (error);
return 1;
}
g_object_unref (pixbuf);
return 0;
}
/*
* Copyright (C) 2013 Bastien Nocera <hadess@hadess.net>
*
* Authors: Bastien Nocera <hadess@hadess.net>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by