283
290
d_width = size * width / height;
286
small = gdk_pixbuf_scale_simple (pixbuf, d_width, d_height,
293
small = gdk_pixbuf_scale_simple (pixbuf, d_width, d_height, GDK_INTERP_TILES);
289
295
if (is_still == FALSE) {
290
with_holes = add_holes_to_pixbuf_small (small,
292
g_return_if_fail (with_holes != NULL);
296
result = add_holes_to_pixbuf_small (small, d_width, d_height);
297
g_return_val_if_fail (result != NULL, NULL);
293
298
g_object_unref (small);
298
with_holes = add_holes_to_pixbuf_large (pixbuf, size);
299
g_return_if_fail (with_holes != NULL);
302
a_width = g_strdup_printf ("%d", width);
303
a_height = g_strdup_printf ("%d", height);
305
if (gdk_pixbuf_save (with_holes, path,
306
jpeg_output ? "jpeg" : "png", &err,
307
"tEXt::Thumb::Image::Width", a_width,
308
"tEXt::Thumb::Image::Height", a_height,
303
result = add_holes_to_pixbuf_large (pixbuf, size);
304
g_return_val_if_fail (result != NULL, NULL);
311
save_pixbuf (GdkPixbuf *pixbuf, const char *path,
312
const char *video_path, int size, gboolean is_still)
315
GdkPixbuf *with_holes;
319
height = gdk_pixbuf_get_height (pixbuf);
320
width = gdk_pixbuf_get_width (pixbuf);
322
/* If we're outputting a gallery, don't scale the pixbuf or add borders */
324
with_holes = scale_pixbuf (pixbuf, size, is_still);
326
with_holes = g_object_ref (pixbuf);
329
if (jpeg_output == FALSE) {
330
char *a_width, *a_height;
332
a_width = g_strdup_printf ("%d", width);
333
a_height = g_strdup_printf ("%d", height);
335
ret = gdk_pixbuf_save (with_holes, path, "png", &err,
336
"tEXt::Thumb::Image::Width", a_width,
337
"tEXt::Thumb::Image::Height", a_height,
340
ret = gdk_pixbuf_save (with_holes, path, "jpeg", &err, NULL);
316
345
g_print ("totem-video-thumbnailer couldn't write the thumbnail '%s' for video '%s': %s\n", path, video_path, err->message);
317
346
g_error_free (err);
495
cairo_surface_to_pixbuf (cairo_surface_t *surface)
497
gint stride, width, height, x, y;
498
guchar *data, *output, *output_pixel;
500
/* This doesn't deal with alpha --- it simply converts the 4-byte Cairo ARGB
501
* format to the 3-byte GdkPixbuf packed RGB format. */
502
g_assert (cairo_image_surface_get_format (surface) == CAIRO_FORMAT_RGB24);
504
stride = cairo_image_surface_get_stride (surface);
505
width = cairo_image_surface_get_width (surface);
506
height = cairo_image_surface_get_height (surface);
507
data = cairo_image_surface_get_data (surface);
509
output = g_malloc (stride * height);
510
output_pixel = output;
512
for (y = 0; y < height; y++) {
513
guint32 *row = (guint32*) (data + y * stride);
515
for (x = 0; x < width; x++) {
516
output_pixel[0] = (row[x] & 0x00ff0000) >> 16;
517
output_pixel[1] = (row[x] & 0x0000ff00) >> 8;
518
output_pixel[2] = (row[x] & 0x000000ff);
524
return gdk_pixbuf_new_from_data (output, GDK_COLORSPACE_RGB, FALSE, 8,
525
width, height, width * 3,
526
(GdkPixbufDestroyNotify) g_free, NULL);
531
create_gallery (BaconVideoWidget *bvw, const char *input, const char *output)
533
GdkPixbuf *screenshot, *pixbuf = NULL;
535
cairo_surface_t *surface;
537
PangoFontDescription *font_desc;
538
gint64 stream_length, screenshot_interval, pos;
539
guint columns, rows, current_column, current_row, x, y;
540
gint screenshot_width, screenshot_height = 0, x_padding = 0, y_padding = 0;
542
gchar *header_text, *duration_text, *filename;
544
/* Calculate how many screenshots we're going to take */
545
stream_length = bacon_video_widget_get_stream_length (bvw) / 1000;
547
/* As a default, we have one screenshot per minute of stream,
548
* but adjusted so we don't have any gaps in the resulting gallery. */
550
gallery = stream_length / 60;
552
while (gallery % 3 != 0 &&
559
if (gallery < GALLERY_MIN)
560
gallery = GALLERY_MIN;
561
if (gallery > GALLERY_MAX)
562
gallery = GALLERY_MAX;
563
screenshot_interval = stream_length / gallery;
565
PROGRESS_DEBUG ("Producing gallery of %u screenshots, taken at %" G_GINT64_FORMAT " second intervals throughout a %" G_GINT64_FORMAT " second-long stream.",
566
gallery, screenshot_interval, stream_length);
568
/* Calculate how to arrange the screenshots so we don't get ones orphaned on the last row.
569
* At this point, only deal with arrangements of 3, 4 or 5 columns. */
571
for (x = 3; x <= 5; x++) {
572
if (gallery % x == 0 || x - gallery % x < y) {
576
/* Have we found an optimal solution already? */
582
rows = ceil ((gfloat) gallery / (gfloat) columns);
584
PROGRESS_DEBUG ("Outputting as %u rows and %u columns.", rows, columns);
586
/* Take the screenshots and composite them into a pixbuf */
587
current_column = current_row = x = y = 0;
588
for (pos = screenshot_interval; pos <= stream_length; pos += screenshot_interval) {
589
screenshot = capture_frame_at_time (bvw, input, output, pos);
591
if (pixbuf == NULL) {
592
screenshot_width = gdk_pixbuf_get_width (screenshot);
593
screenshot_height = gdk_pixbuf_get_height (screenshot);
595
/* Calculate a scaling factor so that screenshot_width -> output_size */
596
scale = (float) output_size / (float) screenshot_width;
598
x_padding = x = MAX (output_size * 0.05, 1);
599
y_padding = y = MAX (scale * screenshot_height * 0.05, 1);
601
PROGRESS_DEBUG ("Scaling each screenshot by %f.", scale);
603
/* Create our massive pixbuf */
604
pixbuf = gdk_pixbuf_new (GDK_COLORSPACE_RGB, FALSE, 8,
605
columns * output_size + (columns + 1) * x_padding,
606
(guint) (rows * scale * screenshot_height + (rows + 1) * y_padding));
607
gdk_pixbuf_fill (pixbuf, 0x000000ff);
609
PROGRESS_DEBUG ("Created output pixbuf (%ux%u).", gdk_pixbuf_get_width (pixbuf), gdk_pixbuf_get_height (pixbuf));
612
/* Composite the screenshot into our gallery */
613
gdk_pixbuf_composite (screenshot, pixbuf,
614
x, y, output_size, scale * screenshot_height,
615
(gdouble) x, (gdouble) y, scale, scale,
616
GDK_INTERP_BILINEAR, 255);
617
g_object_unref (screenshot);
619
PROGRESS_DEBUG ("Composited screenshot from %" G_GINT64_FORMAT " seconds (address %u) at (%u,%u).",
620
pos, GPOINTER_TO_UINT (screenshot), x, y);
622
/* We print progress in the range 10% (MIN_PROGRESS) to 50% (MAX_PROGRESS - MIN_PROGRESS) / 2.0 */
623
PRINT_PROGRESS (MIN_PROGRESS + (current_row * columns + current_column) * (((MAX_PROGRESS - MIN_PROGRESS) / gallery) / 2.0));
625
current_column = (current_column + 1) % columns;
626
x += output_size + x_padding;
627
if (current_column == 0) {
629
y += scale * screenshot_height + y_padding;
634
PROGRESS_DEBUG ("Converting pixbuf to a Cairo surface.");
636
/* Load the pixbuf into a Cairo surface and overlay the text. The height is the height of
637
* the gallery plus the necessary height for 3 lines of header (at ~18px each), plus some
639
surface = cairo_image_surface_create (CAIRO_FORMAT_RGB24, gdk_pixbuf_get_width (pixbuf),
640
gdk_pixbuf_get_height (pixbuf) + GALLERY_HEADER_HEIGHT + y_padding);
641
cr = cairo_create (surface);
642
cairo_surface_destroy (surface);
644
/* First, copy across the gallery pixbuf */
645
gdk_cairo_set_source_pixbuf (cr, pixbuf, 0.0, GALLERY_HEADER_HEIGHT + y_padding);
646
cairo_rectangle (cr, 0.0, GALLERY_HEADER_HEIGHT + y_padding, gdk_pixbuf_get_width (pixbuf), gdk_pixbuf_get_height (pixbuf));
648
g_object_unref (pixbuf);
650
/* Build the header information */
651
duration_text = totem_time_to_string (stream_length * 1000);
653
if (strstr (input, "://")) {
655
local = g_filename_from_uri (input, NULL, NULL);
656
filename = g_path_get_basename (local);
659
if (filename == NULL)
660
filename = g_path_get_basename (input);
662
/* Translators: The first string is "Filename" (as translated); the second is an actual filename.
663
The third string is "Resolution" (as translated); the fourth and fifth are screenshot height and width, respectively.
664
The sixth string is "Duration" (as translated); the seventh is the movie duration in words. */
665
header_text = g_strdup_printf (_("<b>%s</b>: %s\n<b>%s</b>: %d\303\227%d\n<b>%s</b>: %s"),
673
g_free (duration_text);
676
PROGRESS_DEBUG ("Writing header text with Pango.");
678
/* Write out some header information */
679
layout = pango_cairo_create_layout (cr);
680
font_desc = pango_font_description_from_string ("Sans 18px");
681
pango_layout_set_font_description (layout, font_desc);
682
pango_font_description_free (font_desc);
684
pango_layout_set_markup (layout, header_text, -1);
685
g_free (header_text);
687
cairo_set_source_rgb (cr, 1.0, 1.0, 1.0); /* white */
688
cairo_move_to (cr, (gdouble) x_padding, (gdouble) y_padding);
689
pango_cairo_show_layout (cr, layout);
691
/* Go through each screenshot and write its timestamp */
692
current_column = current_row = 0;
693
x = x_padding + output_size;
694
y = y_padding * 2 + GALLERY_HEADER_HEIGHT + scale * screenshot_height;
696
font_desc = pango_font_description_from_string ("Sans 10px");
697
pango_layout_set_font_description (layout, font_desc);
698
pango_font_description_free (font_desc);
700
PROGRESS_DEBUG ("Writing screenshot timestamps with Pango.");
702
for (pos = screenshot_interval; pos <= stream_length; pos += screenshot_interval) {
703
gchar *timestamp_text;
704
gint layout_width, layout_height;
706
timestamp_text = totem_time_to_string (pos * 1000);
708
pango_layout_set_text (layout, timestamp_text, -1);
709
pango_layout_get_pixel_size (layout, &layout_width, &layout_height);
711
/* Display the timestamp in the bottom-right corner of the current screenshot */
712
cairo_move_to (cr, x - layout_width - 0.02 * output_size, y - layout_height - 0.02 * scale * screenshot_height);
714
/* We have to stroke the text so it's visible against screenshots of the same
715
* foreground color. */
716
pango_cairo_layout_path (cr, layout);
717
cairo_set_source_rgb (cr, 0.0, 0.0, 0.0); /* black */
718
cairo_stroke_preserve (cr);
719
cairo_set_source_rgb (cr, 1.0, 1.0, 1.0); /* white */
722
PROGRESS_DEBUG ("Writing timestamp \"%s\" at (%f,%f).", timestamp_text,
723
x - layout_width - 0.02 * output_size,
724
y - layout_height - 0.02 * scale * screenshot_height);
726
/* We print progress in the range 50% (MAX_PROGRESS - MIN_PROGRESS) / 2.0) to 90% (MAX_PROGRESS) */
727
PRINT_PROGRESS (MIN_PROGRESS + (MAX_PROGRESS - MIN_PROGRESS) / 2.0 + (current_row * columns + current_column) * (((MAX_PROGRESS - MIN_PROGRESS) / gallery) / 2.0));
729
g_free (timestamp_text);
731
current_column = (current_column + 1) % columns;
732
x += output_size + x_padding;
733
if (current_column == 0) {
734
x = x_padding + output_size;
735
y += scale * screenshot_height + y_padding;
740
g_object_unref (layout);
742
PROGRESS_DEBUG ("Converting Cairo surface back to pixbuf.");
744
/* Create a new pixbuf from the Cairo context */
745
pixbuf = cairo_surface_to_pixbuf (cairo_get_target (cr));
465
751
static const GOptionEntry entries[] = {
466
752
{ "jpeg", 'j', 0, G_OPTION_ARG_NONE, &jpeg_output, "Output the thumbnail as a JPEG instead of PNG", NULL },
467
{ "size", 's', 0, G_OPTION_ARG_INT, &output_size, "Size of the thumbnail in pixels", NULL },
753
{ "size", 's', 0, G_OPTION_ARG_INT, &output_size, "Size of the thumbnail in pixels (with --gallery sets the size of individual screenshots)", NULL },
468
754
{ "no-limit", 'l', G_OPTION_FLAG_REVERSE, G_OPTION_ARG_NONE, &time_limit, "Don't limit the thumbnailing time to 30 seconds", NULL },
469
755
{ "verbose", 'v', 0, G_OPTION_ARG_NONE, &verbose, "Output debug information", NULL },
470
{ "time", 't', 0, G_OPTION_ARG_INT64, &second_index, "Choose this time (in seconds) as the thumbnail", NULL },
471
{"g-fatal-warnings", 0, 0, G_OPTION_ARG_NONE, &g_fatal_warnings, "Make all warnings fatal", NULL},
756
{ "time", 't', 0, G_OPTION_ARG_INT64, &second_index, "Choose this time (in seconds) as the thumbnail (can't be used with --gallery)", NULL },
757
{ "g-fatal-warnings", 0, 0, G_OPTION_ARG_NONE, &g_fatal_warnings, "Make all warnings fatal", NULL },
758
{ "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 },
759
{ "print-progress", 'p', 0, G_OPTION_ARG_NONE, &print_progress, "Only print progress updates (can't be used with --verbose)", NULL },
472
760
{ G_OPTION_REMAINING, '\0', 0, G_OPTION_ARG_FILENAME_ARRAY, &filenames, NULL, "[FILE...]" },