totem r5859 - in trunk: . src



Author: pwithnall
Date: Mon Dec 15 07:28:48 2008
New Revision: 5859
URL: http://svn.gnome.org/viewvc/totem?rev=5859&view=rev

Log:
2008-12-15  Philip Withnall  <philip tecnocode co uk>

	* src/totem-video-thumbnailer.c (scale_pixbuf), (save_pixbuf),
	(cairo_surface_to_pixbuf), (create_gallery), (main): Add an 
option
	to totem-video-thumbnailer to create galleries of screenshots 
from
	videos. (Helps: #561088)



Modified:
   trunk/ChangeLog
   trunk/src/totem-video-thumbnailer.c

Modified: trunk/src/totem-video-thumbnailer.c
==============================================================================
--- trunk/src/totem-video-thumbnailer.c	(original)
+++ trunk/src/totem-video-thumbnailer.c	Mon Dec 15 07:28:48 2008
@@ -30,13 +30,16 @@
 #include <glib/gstdio.h>
 #include <gtk/gtk.h>
 #include <glib/gi18n.h>
+#include <cairo/cairo.h>
 
 #include <unistd.h>
 #include <string.h>
+#include <math.h>
 #include <stdlib.h>
 #include <sys/types.h>
 #include <sys/stat.h>
 #include "bacon-video-widget.h"
+#include "video-utils.h"
 #include "totem-resources.h"
 
 /* #define THUMB_DEBUG */
@@ -48,12 +51,16 @@
 #endif
 
 #define BORING_IMAGE_VARIANCE 256.0		/* Tweak this if necessary */
+#define GALLERY_MIN 3				/* minimum number of screenshots in a gallery */
+#define GALLERY_MAX 30				/* maximum number of screenshots in a gallery */
+#define GALLERY_HEADER_HEIGHT 66		/* header height (in pixels) for the gallery */
 
 static gboolean jpeg_output = FALSE;
 static gboolean output_size = 128;
 static gboolean time_limit = TRUE;
 static gboolean verbose = FALSE;
 static gboolean g_fatal_warnings = FALSE;
+static gint gallery = -1;
 static gint64 second_index = -1;
 static char **filenames = NULL;
 
@@ -255,27 +262,20 @@
 	return (variance > BORING_IMAGE_VARIANCE);
 }
 
-static void
-save_pixbuf (GdkPixbuf *pixbuf, const char *path,
-	     const char *video_path, int size, gboolean is_still)
+static GdkPixbuf *
+scale_pixbuf (GdkPixbuf *pixbuf, int size, gboolean is_still)
 {
+	GdkPixbuf *result;
 	int width, height;
-	GdkPixbuf *small, *with_holes;
-	GError *err = NULL;
-	char *a_width, *a_height;
 
-	height = gdk_pixbuf_get_height (pixbuf);
-	width = gdk_pixbuf_get_width (pixbuf);
-
-	if (size <= 256)
-	{
+	if (size <= 256) {
 		int d_width, d_height;
+		GdkPixbuf *small;
 
 		height = gdk_pixbuf_get_height (pixbuf);
 		width = gdk_pixbuf_get_width (pixbuf);
 
-		if (width > height)
-		{
+		if (width > height) {
 			d_width = size;
 			d_height = size * height / width;
 		} else {
@@ -283,22 +283,41 @@
 			d_width = size * width / height;
 		}
 
-		small = gdk_pixbuf_scale_simple (pixbuf, d_width, d_height,
-				GDK_INTERP_TILES);
+		small = gdk_pixbuf_scale_simple (pixbuf, d_width, d_height, GDK_INTERP_TILES);
 
 		if (is_still == FALSE) {
-			with_holes = add_holes_to_pixbuf_small (small,
-					d_width, d_height);
-			g_return_if_fail (with_holes != NULL);
+			result = add_holes_to_pixbuf_small (small, d_width, d_height);
+			g_return_val_if_fail (result != NULL, NULL);
 			g_object_unref (small);
 		} else {
-			with_holes = small;
+			result = small;
 		}
 	} else {
-		with_holes = add_holes_to_pixbuf_large (pixbuf, size);
-		g_return_if_fail (with_holes != NULL);
+		result = add_holes_to_pixbuf_large (pixbuf, size);
+		g_return_val_if_fail (result != NULL, NULL);
 	}
 
+	return result;
+}
+
+static void
+save_pixbuf (GdkPixbuf *pixbuf, const char *path,
+	     const char *video_path, int size, gboolean is_still)
+{
+	int width, height;
+	GdkPixbuf *with_holes;
+	GError *err = NULL;
+	char *a_width, *a_height;
+
+	height = gdk_pixbuf_get_height (pixbuf);
+	width = gdk_pixbuf_get_width (pixbuf);
+
+	/* If we're outputting a gallery, don't scale the pixbuf or add borders */
+	if (gallery == -1)
+		with_holes = scale_pixbuf (pixbuf, size, is_still);
+	else
+		with_holes = g_object_ref (pixbuf);
+
 	a_width = g_strdup_printf ("%d", width);
 	a_height = g_strdup_printf ("%d", height);
 
@@ -311,8 +330,7 @@
 		g_free (a_width);
 		g_free (a_height);
 
-		if (err != NULL)
-		{
+		if (err != NULL) {
 			g_print ("totem-video-thumbnailer couldn't write the thumbnail '%s' for video '%s': %s\n", path, video_path, err->message);
 			g_error_free (err);
 		} else {
@@ -462,13 +480,250 @@
 	}
 }
 
+static GdkPixbuf *
+cairo_surface_to_pixbuf (cairo_surface_t *surface)
+{
+	gint stride, width, height, x, y;
+	guchar *data, *output, *output_pixel;
+
+	/* This doesn't deal with alpha --- it simply converts the 4-byte Cairo ARGB
+	 * format to the 3-byte GdkPixbuf packed RGB format. */
+	g_assert (cairo_image_surface_get_format (surface) == CAIRO_FORMAT_RGB24);
+
+	stride = cairo_image_surface_get_stride (surface);
+	width = cairo_image_surface_get_width (surface);
+	height = cairo_image_surface_get_height (surface);
+	data = cairo_image_surface_get_data (surface);
+
+	output = g_malloc (stride * height);
+	output_pixel = output;
+
+	for (y = 0; y < height; y++) {
+		guint32 *row = (guint32*) (data + y * stride);
+
+		for (x = 0; x < width; x++) {
+			output_pixel[0] = (row[x] & 0x00ff0000) >> 16;
+			output_pixel[1] = (row[x] & 0x0000ff00) >> 8;
+			output_pixel[2] = (row[x] & 0x000000ff);
+
+			output_pixel += 3;
+		}
+	}
+
+	return gdk_pixbuf_new_from_data (output, GDK_COLORSPACE_RGB, FALSE, 8,
+					 width, height, width * 3,
+					 (GdkPixbufDestroyNotify) g_free, NULL);
+}
+
+
+static GdkPixbuf *
+create_gallery (BaconVideoWidget *bvw, const char *input, const char *output)
+{
+	GdkPixbuf *screenshot, *pixbuf = NULL;
+	cairo_t *cr;
+	cairo_surface_t *surface;
+	PangoLayout *layout;
+	PangoFontDescription *font_desc;
+	gint64 stream_length, screenshot_interval, pos;
+	guint columns, rows, current_column, current_row, x, y;
+	gint screenshot_width, screenshot_height = 0, x_padding = 0, y_padding = 0;
+	gfloat scale = 1.0;
+	gchar *header_text, *duration_text, *filename;
+
+	/* Calculate how many screenshots we're going to take */
+	stream_length = bacon_video_widget_get_stream_length (bvw) / 1000;
+
+	/* As a default, we have one screenshot per minute of stream,
+	 * but adjusted so we don't have any gaps in the resulting gallery. */
+	if (gallery == 0) {
+		gallery = stream_length / 60;
+
+		while (gallery % 3 != 0 &&
+		       gallery % 4 != 0 &&
+		       gallery % 5 != 0) {
+			gallery++;
+		}
+	}
+
+	if (gallery < GALLERY_MIN)
+		gallery = GALLERY_MIN;
+	if (gallery > GALLERY_MAX)
+		gallery = GALLERY_MAX;
+	screenshot_interval = stream_length / gallery;
+
+	PROGRESS_DEBUG ("Producing gallery of %u screenshots, taken at %" G_GINT64_FORMAT " second intervals throughout a %" G_GINT64_FORMAT " second-long stream.",
+			gallery, screenshot_interval, stream_length);
+
+	/* Calculate how to arrange the screenshots so we don't get ones orphaned on the last row.
+	 * At this point, only deal with arrangements of 3, 4 or 5 columns. */
+	y = G_MAXUINT;
+	for (x = 3; x <= 5; x++) {
+		if (gallery % x == 0 || x - gallery % x < y) {
+			y = x - gallery % x;
+			columns = x;
+
+			/* Have we found an optimal solution already? */
+			if (y == x)
+				break;
+		}
+	}
+
+	rows = ceil ((gfloat) gallery / (gfloat) columns);
+
+	PROGRESS_DEBUG ("Outputting as %u rows and %u columns.", rows, columns);
+
+	/* Take the screenshots and composite them into a pixbuf */
+	current_column = current_row = x = y = 0;
+	for (pos = screenshot_interval; pos <= stream_length; pos += screenshot_interval) {
+		screenshot = capture_frame_at_time (bvw, input, output, pos);
+
+		if (pixbuf == NULL) {
+			screenshot_width = gdk_pixbuf_get_width (screenshot);
+			screenshot_height = gdk_pixbuf_get_height (screenshot);
+
+			/* Calculate a scaling factor so that screenshot_width -> output_size */
+			scale = (float) output_size / (float) screenshot_width;
+
+			x_padding = x = MAX (output_size * 0.05, 1);
+			y_padding = y = MAX (scale * screenshot_height * 0.05, 1);
+
+			PROGRESS_DEBUG ("Scaling each screenshot by %f.", scale);
+
+			/* Create our massive pixbuf */
+			pixbuf = gdk_pixbuf_new (GDK_COLORSPACE_RGB, FALSE, 8,
+						 columns * output_size + (columns + 1) * x_padding,
+						 (guint) (rows * scale * screenshot_height + (rows + 1) * y_padding));
+			gdk_pixbuf_fill (pixbuf, 0x000000ff);
+
+			PROGRESS_DEBUG ("Created output pixbuf (%ux%u).", gdk_pixbuf_get_width (pixbuf), gdk_pixbuf_get_height (pixbuf));
+		}
+
+		/* Composite the screenshot into our gallery */
+		gdk_pixbuf_composite (screenshot, pixbuf,
+				      x, y, output_size, scale * screenshot_height,
+				      (gdouble) x, (gdouble) y, scale, scale,
+				      GDK_INTERP_BILINEAR, 255);
+		g_object_unref (screenshot);
+
+		PROGRESS_DEBUG ("Composited screenshot from %" G_GINT64_FORMAT " seconds (address %u) at (%u,%u).",
+				pos, GPOINTER_TO_UINT (screenshot), x, y);
+
+		current_column = (current_column + 1) % columns;
+		x += output_size + x_padding;
+		if (current_column == 0) {
+			x = x_padding;
+			y += scale * screenshot_height + y_padding;
+			current_row++;
+		}
+	}
+
+	PROGRESS_DEBUG ("Converting pixbuf to a Cairo surface.");
+
+	/* Load the pixbuf into a Cairo surface and overlay the text. The height is the height of
+	 * the gallery plus the necessary height for 3 lines of header (at ~18px each), plus some
+	 * extra padding. */
+	surface = cairo_image_surface_create (CAIRO_FORMAT_RGB24, gdk_pixbuf_get_width (pixbuf),
+					      gdk_pixbuf_get_height (pixbuf) + GALLERY_HEADER_HEIGHT + y_padding);
+	cr = cairo_create (surface);
+	cairo_surface_destroy (surface);
+
+	/* First, copy across the gallery pixbuf */
+	gdk_cairo_set_source_pixbuf (cr, pixbuf, 0.0, GALLERY_HEADER_HEIGHT + y_padding);
+	cairo_rectangle (cr, 0.0, GALLERY_HEADER_HEIGHT + y_padding, gdk_pixbuf_get_width (pixbuf), gdk_pixbuf_get_height (pixbuf));
+	cairo_fill (cr);
+	g_object_unref (pixbuf);
+
+	/* Build the header information */
+	duration_text = totem_time_to_string (stream_length * 1000);
+	filename = g_path_get_basename (input);
+	header_text = g_strdup_printf (_("<b>Filename</b>: %s\n<b>Resolution</b>: %d\u00D7%d\n<b>Duration</b>: %s"),
+				       filename,
+				       screenshot_width,
+				       screenshot_height,
+				       duration_text);
+	g_free (duration_text);
+	g_free (filename);
+
+	PROGRESS_DEBUG ("Writing header text with Pango.");
+
+	/* Write out some header information */
+	layout = pango_cairo_create_layout (cr);
+	font_desc = pango_font_description_from_string ("Sans 18px");
+	pango_layout_set_font_description (layout, font_desc);
+	pango_font_description_free (font_desc);
+
+	pango_layout_set_markup (layout, header_text, -1);
+	g_free (header_text);
+
+	cairo_set_source_rgb (cr, 1.0, 1.0, 1.0); /* white */
+	cairo_move_to (cr, (gdouble) x_padding, (gdouble) y_padding);
+	pango_cairo_show_layout (cr, layout);
+
+	/* Go through each screenshot and write its timestamp */
+	current_column = current_row = 0;
+	x = x_padding + output_size;
+	y = y_padding * 2 + GALLERY_HEADER_HEIGHT + scale * screenshot_height;
+
+	font_desc = pango_font_description_from_string ("Sans 10px");
+	pango_layout_set_font_description (layout, font_desc);
+	pango_font_description_free (font_desc);
+
+	PROGRESS_DEBUG ("Writing screenshot timestamps with Pango.");
+
+	for (pos = screenshot_interval; pos <= stream_length; pos += screenshot_interval) {
+		gchar *timestamp_text;
+		gint layout_width, layout_height;
+
+		timestamp_text = totem_time_to_string (pos * 1000);
+
+		pango_layout_set_text (layout, timestamp_text, -1);
+		pango_layout_get_pixel_size (layout, &layout_width, &layout_height);
+
+		/* Display the timestamp in the bottom-right corner of the current screenshot */
+		cairo_move_to (cr, x - layout_width - 0.02 * output_size, y - layout_height - 0.02 * scale * screenshot_height);
+
+		/* We have to stroke the text so it's visible against screenshots of the same
+		 * foreground color. */
+		pango_cairo_layout_path (cr, layout);
+		cairo_set_source_rgb (cr, 0.0, 0.0, 0.0); /* black */
+		cairo_stroke_preserve (cr);
+		cairo_set_source_rgb (cr, 1.0, 1.0, 1.0); /* white */
+		cairo_fill (cr);
+
+		PROGRESS_DEBUG ("Writing timestamp \"%s\" at (%f,%f).", timestamp_text,
+				x - layout_width - 0.02 * output_size,
+				y - layout_height - 0.02 * scale * screenshot_height);
+
+		g_free (timestamp_text);
+
+		current_column = (current_column + 1) % columns;
+		x += output_size + x_padding;
+		if (current_column == 0) {
+			x = x_padding + output_size;
+			y += scale * screenshot_height + y_padding;
+			current_row++;
+		}
+	}
+
+	g_object_unref (layout);
+
+	PROGRESS_DEBUG ("Converting Cairo surface back to pixbuf.");
+
+	/* Create a new pixbuf from the Cairo context */
+	pixbuf = cairo_surface_to_pixbuf (cairo_get_target (cr));
+	cairo_destroy (cr);
+
+	return pixbuf;
+}
+
 static const GOptionEntry entries[] = {
 	{ "jpeg", 'j',  0, G_OPTION_ARG_NONE, &jpeg_output, "Output the thumbnail as a JPEG instead of PNG", NULL },
-	{ "size", 's', 0, G_OPTION_ARG_INT, &output_size, "Size of the thumbnail in pixels", NULL },
+	{ "size", 's', 0, G_OPTION_ARG_INT, &output_size, "Size of the thumbnail in pixels (with --gallery sets the size of individual screenshots)", NULL },
 	{ "no-limit", 'l', G_OPTION_FLAG_REVERSE, G_OPTION_ARG_NONE, &time_limit, "Don't limit the thumbnailing time to 30 seconds", NULL },
 	{ "verbose", 'v', 0, G_OPTION_ARG_NONE, &verbose, "Output debug information", NULL },
-	{ "time", 't', 0, G_OPTION_ARG_INT64, &second_index, "Choose this time (in seconds) as the thumbnail", NULL },
-	{"g-fatal-warnings", 0, 0, G_OPTION_ARG_NONE, &g_fatal_warnings, "Make all warnings fatal", NULL},
+	{ "time", 't', 0, G_OPTION_ARG_INT64, &second_index, "Choose this time (in seconds) as the thumbnail (can't be used with --gallery)", NULL },
+	{ "g-fatal-warnings", 0, 0, G_OPTION_ARG_NONE, &g_fatal_warnings, "Make all warnings fatal", NULL },
+	{ "gallery", 'g', 0, G_OPTION_ARG_INT, &gallery, "Output a gallery of the given number (0 is default) of screenshots (can't be used with --time)", NULL },
  	{ G_OPTION_REMAINING, '\0', 0, G_OPTION_ARG_FILENAME_ARRAY, &filenames, NULL, "[FILE...]" },
 	{ NULL }
 };
@@ -513,7 +768,8 @@
 		g_log_set_always_fatal (fatal_mask);
 	}
 
-	if (filenames == NULL || g_strv_length (filenames) != 2) {
+	if (filenames == NULL || g_strv_length (filenames) != 2 ||
+	    (second_index != -1 && gallery != -1)) {
 		char *help;
 		help = g_option_context_get_help (context, FALSE, NULL);
 		g_print ("%s", help);
@@ -565,13 +821,18 @@
 	}
 	PROGRESS_DEBUG("Started playing file");
 
-	/* If the user has told us to use a frame at a specific second 
-	 * into the video, just use that frame no matter how boring it
-	 * is */
-	if(second_index != -1)
-		pixbuf = capture_frame_at_time (bvw, input, output, second_index);
-	else
-		pixbuf = capture_interesting_frame (bvw, input, output);
+	if (gallery == -1) {
+		/* If the user has told us to use a frame at a specific second 
+		 * into the video, just use that frame no matter how boring it
+		 * is */
+		if (second_index != -1)
+			pixbuf = capture_frame_at_time (bvw, input, output, second_index);
+		else
+			pixbuf = capture_interesting_frame (bvw, input, output);
+	} else {
+		/* We're producing a gallery of screenshots from throughout the file */
+		pixbuf = create_gallery (bvw, input, output);
+	}
 
 	/* Cleanup */
 	bacon_video_widget_close (bvw);



[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]