/* * Animation Playback plug-in version 0.98.8 * * (c) Adam D. Moss : 1997-2000 : adam@gimp.org : adam@foxbox.org * * The GIMP -- an image manipulation program * Copyright (C) 1995 Spencer Kimball and Peter Mattis * * 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. */ /* * BUGS: * Gets understandably upset if the source image is deleted * while the animation is playing. Decent solution welcome. * * In shaped mode, the shaped-window's mask and its pixmap contents * can get way out of sync (specifically, the mask changes but * the contents are frozen). Starvation of GTK's redrawing thread? * How do I fix this? * * Any more? Let me know! */ /* * TODO: * pdb interface - should we bother? * * speedups (caching? most bottlenecks seem to be in pixelrgns) * -> do pixelrgns properly! * * write other half of the user interface (default timing, disposal &c) */ #include "config.h" #include #include #include #include #include #include #include "libgimp/stdplugins-intl.h" #define DITHERTYPE GDK_RGB_DITHER_NORMAL typedef enum { DISPOSE_UNDEFINED = 0x00, DISPOSE_COMBINE = 0x01, DISPOSE_REPLACE = 0x02 } DisposeType; /* Declare local functions. */ static void query (void); static void run (const gchar *name, gint nparams, const GimpParam *param, gint *nreturn_vals, GimpParam **return_vals); static void do_playback (void); static void window_response (GtkWidget *widget, gint response_id, gpointer data); static void playstop_callback (GtkWidget *widget, gpointer data); static void rewind_callback (GtkWidget *widget, gpointer data); static void step_callback (GtkWidget *widget, gpointer data); static gboolean repaint_sda (GtkWidget *darea, GdkEventExpose *event, gpointer data); static gboolean repaint_da (GtkWidget *darea, GdkEventExpose *event, gpointer data); static void render_frame (gint32 whichframe); static void show_frame (void); static void total_alpha_preview (guchar* ptr); static void init_preview_misc (void); /* tag util functions*/ static int parse_ms_tag (const char *str); static DisposeType parse_disposal_tag (const char *str); static DisposeType get_frame_disposal (guint whichframe); static guint32 get_frame_duration (guint whichframe); static int is_disposal_tag (const char *str, DisposeType *disposal, int *taglength); static int is_ms_tag (const char *str, int *duration, int *taglength); GimpPlugInInfo PLUG_IN_INFO = { NULL, /* init_proc */ NULL, /* quit_proc */ query, /* query_proc */ run, /* run_proc */ }; /* Global widgets'n'stuff */ static GtkWidget *dlg = NULL; static guchar *preview_data = NULL; static GtkWidget *drawing_area = NULL; static GtkWidget *shape_drawing_area = NULL; static guchar *shape_drawing_area_data = NULL; static guchar *drawing_area_data = NULL; static GtkProgressBar *progress; static guint width, height; static guchar *preview_alpha1_data; static guchar *preview_alpha2_data; static gint32 image_id; static gint32 total_frames; static guint frame_number; static gint32 *layers; static GimpDrawable *drawable; static gboolean playing = FALSE; static guint timer = 0; static GimpImageBaseType imagetype; static guchar *palette; static gint ncolours; static GtkWidget *psbutton; /* for shaping */ typedef struct { gint x, y; } CursorOffset; static gchar *shape_preview_mask; static GtkWidget *shape_window; static gint shaping = 0; static GdkWindow *root_win = NULL; MAIN () static void query (void) { static GimpParamDef args[] = { { GIMP_PDB_INT32, "run_mode", "Interactive, non-interactive" }, { GIMP_PDB_IMAGE, "image", "Input image" }, { GIMP_PDB_DRAWABLE, "drawable", "Input drawable (unused)" } }; gimp_install_procedure ("plug_in_animationplay", "This plugin allows you to preview a GIMP layer-based animation.", "", "Adam D. Moss ", "Adam D. Moss ", "1997, 1998...", N_("_Playback..."), "RGB*, INDEXED*, GRAY*", GIMP_PLUGIN, G_N_ELEMENTS (args), 0, args, NULL); gimp_plugin_menu_register ("plug_in_animationplay", N_("/Filters/Animation")); } static void run (const gchar *name, gint n_params, const GimpParam *param, gint *nreturn_vals, GimpParam **return_vals) { static GimpParam values[1]; GimpRunMode run_mode; GimpPDBStatusType status = GIMP_PDB_SUCCESS; *nreturn_vals = 1; *return_vals = values; run_mode = param[0].data.d_int32; INIT_I18N (); if (run_mode == GIMP_RUN_NONINTERACTIVE && n_params != 3) { status = GIMP_PDB_CALLING_ERROR; } if (status == GIMP_PDB_SUCCESS) { image_id = param[1].data.d_image; do_playback(); if (run_mode != GIMP_RUN_NONINTERACTIVE) gimp_displays_flush(); } values[0].type = GIMP_PDB_STATUS; values[0].data.d_status = status; } static void reshape_from_bitmap (gchar* bitmap) { GdkBitmap *shape_mask; static gchar *prev_bitmap = NULL; if ((!prev_bitmap) || (memcmp(prev_bitmap, bitmap, (width*height)/8 +height))) { shape_mask = gdk_bitmap_create_from_data(shape_window->window, bitmap, width, height); gtk_widget_shape_combine_mask (shape_window, shape_mask, 0, 0); g_object_unref (shape_mask); if (!prev_bitmap) { prev_bitmap = g_malloc ((width*height)/8 +height); } memcpy (prev_bitmap, bitmap, (width*height)/8 +height); } } static gboolean shape_pressed (GtkWidget *widget, GdkEventButton *event) { CursorOffset *p; /* ignore double and triple click */ if (event->type != GDK_BUTTON_PRESS) return FALSE; p = g_object_get_data (G_OBJECT(widget), "cursor-offset"); if (!p) return FALSE; p->x = (int) event->x; p->y = (int) event->y; gtk_grab_add (widget); gdk_pointer_grab (widget->window, TRUE, GDK_BUTTON_RELEASE_MASK | GDK_BUTTON_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK, NULL, NULL, 0); gdk_window_raise (widget->window); return FALSE; } static gboolean maybeblocked_expose (GtkWidget *widget, GdkEventExpose *event) { if (playing) return TRUE; return repaint_sda (widget, event, NULL); } static gboolean shape_released (GtkWidget *widget) { gtk_grab_remove (widget); gdk_display_pointer_ungrab (gtk_widget_get_display (widget), 0); gdk_flush (); return FALSE; } static gboolean shape_motion (GtkWidget *widget, GdkEventMotion *event) { gint xp, yp; CursorOffset *p; GdkModifierType mask; gdk_window_get_pointer (root_win, &xp, &yp, &mask); /* if a button is still held by the time we process this event... */ if (mask & (GDK_BUTTON1_MASK| GDK_BUTTON2_MASK| GDK_BUTTON3_MASK| GDK_BUTTON4_MASK| GDK_BUTTON5_MASK)) { p = g_object_get_data (G_OBJECT (widget), "cursor-offset"); if (!p) return FALSE; gtk_window_move (GTK_WINDOW (widget), xp - p->x, yp - p->y); } else /* the user has released all buttons */ { shape_released(widget); } return FALSE; } static gboolean repaint_da (GtkWidget *darea, GdkEventExpose *event, gpointer data) { gdk_draw_rgb_image (drawing_area->window, drawing_area->style->white_gc, 0, 0, width, height, (total_frames==1)?GDK_RGB_DITHER_MAX:DITHERTYPE, drawing_area_data, width * 3); return TRUE; } static gboolean repaint_sda (GtkWidget *darea, GdkEventExpose *event, gpointer data) { gdk_draw_rgb_image (shape_drawing_area->window, shape_drawing_area->style->white_gc, 0, 0, width, height, (total_frames==1)?GDK_RGB_DITHER_MAX:DITHERTYPE, shape_drawing_area_data, width * 3); return TRUE; } static gboolean preview_pressed (GtkWidget *widget, GdkEventButton *event) { gint xp, yp; GdkModifierType mask; if (shaping) return FALSE; /* Create a total-alpha buffer merely for the not-shaped drawing area to now display. */ drawing_area_data = g_malloc (width * height * 3); total_alpha_preview (drawing_area_data); gdk_window_get_pointer (root_win, &xp, &yp, &mask); gtk_window_move (GTK_WINDOW (shape_window), xp - event->x, yp - event->y); gtk_widget_show (shape_window); gdk_window_set_back_pixmap (shape_window->window, NULL, 0); gdk_window_set_back_pixmap (shape_drawing_area->window, NULL, 1); show_frame (); shaping = 1; memset (shape_preview_mask, 0, (width*height)/8 + height); render_frame (frame_number); show_frame (); repaint_da (NULL, NULL, NULL); /* mildly amusing hack */ return shape_pressed (shape_window, event); } static void build_dialog (GimpImageBaseType basetype, gchar *imagename) { gchar *windowname; CursorOffset *icon_pos; GtkWidget *button; GtkWidget *frame; GtkWidget *frame2; GtkWidget *vbox; GtkWidget *hbox; GtkWidget *hbox2; GtkWidget *eventbox; GdkCursor *cursor; gimp_ui_init ("animationplay", TRUE); windowname = g_strconcat (_("Animation Playback:"), " ", imagename, NULL); dlg = gimp_dialog_new (windowname, "animationplay", NULL, 0, gimp_standard_help_func, "plug-in-animationplay", GTK_STOCK_CLOSE, GTK_RESPONSE_CLOSE, NULL); g_free (windowname); g_signal_connect (dlg, "response", G_CALLBACK (window_response), NULL); /* The 'playback' half of the dialog */ if (total_frames > 1) windowname = g_strconcat (_("Playback:"), " ",imagename, NULL); else windowname = g_strdup (imagename); frame = gimp_frame_new (windowname); g_free (windowname); gtk_container_set_border_width (GTK_CONTAINER (frame), 12); gtk_box_pack_start (GTK_BOX (GTK_DIALOG (dlg)->vbox), frame, TRUE, TRUE, 0); gtk_widget_show (frame); hbox = gtk_hbox_new (FALSE, 6); gtk_container_add (GTK_CONTAINER (frame), hbox); gtk_widget_show (hbox); vbox = gtk_vbox_new (FALSE, 6); gtk_container_add (GTK_CONTAINER (hbox), vbox); gtk_widget_show (vbox); hbox2 = gtk_hbox_new (FALSE, 0); gtk_box_pack_start (GTK_BOX (vbox), hbox2, TRUE, TRUE, 0); if (total_frames > 1) gtk_widget_show (hbox2); psbutton = gtk_toggle_button_new_with_label ( _("Play/Stop")); g_signal_connect (psbutton, "toggled", G_CALLBACK (playstop_callback), NULL); gtk_box_pack_start (GTK_BOX (hbox2), psbutton, TRUE, TRUE, 0); gtk_widget_show (psbutton); button = gtk_button_new_with_label ( _("Rewind")); g_signal_connect (button, "clicked", G_CALLBACK (rewind_callback), NULL); gtk_box_pack_start (GTK_BOX (hbox2), button, TRUE, TRUE, 0); gtk_widget_show (button); button = gtk_button_new_with_label ( _("Step")); g_signal_connect (button, "clicked", G_CALLBACK (step_callback), NULL); gtk_box_pack_start (GTK_BOX (hbox2), button, TRUE, TRUE, 0); gtk_widget_show (button); hbox2 = gtk_hbox_new (TRUE, 0); gtk_box_pack_start (GTK_BOX (vbox), hbox2, FALSE, FALSE, 0); gtk_widget_show (hbox2); frame2 = gtk_frame_new (NULL); gtk_frame_set_shadow_type (GTK_FRAME (frame2), GTK_SHADOW_IN); gtk_box_pack_start (GTK_BOX (hbox2), frame2, FALSE, FALSE, 0); gtk_widget_show (frame2); eventbox = gtk_event_box_new(); gtk_widget_add_events (eventbox, GDK_BUTTON_PRESS_MASK); gtk_container_add (GTK_CONTAINER (frame2), GTK_WIDGET (eventbox)); gtk_widget_show (eventbox); drawing_area = gtk_drawing_area_new (); gtk_widget_set_size_request (drawing_area, width, height); gtk_container_add (GTK_CONTAINER (eventbox), GTK_WIDGET (drawing_area)); gtk_widget_show (drawing_area); progress = GTK_PROGRESS_BAR (gtk_progress_bar_new ()); gtk_box_pack_start (GTK_BOX (vbox), GTK_WIDGET (progress), TRUE, TRUE, 0); if (total_frames > 1) gtk_widget_show (GTK_WIDGET (progress)); gtk_widget_show (dlg); /* let's get into shape. */ shape_window = gtk_window_new (GTK_WINDOW_POPUP); shape_drawing_area = gtk_drawing_area_new (); gtk_widget_set_size_request (shape_drawing_area, width, height); gtk_container_add (GTK_CONTAINER (shape_window), shape_drawing_area); gtk_widget_show (shape_drawing_area); gtk_widget_add_events (shape_drawing_area, GDK_BUTTON_PRESS_MASK); gtk_widget_realize (shape_window); gdk_window_set_back_pixmap (shape_window->window, NULL, 0); cursor = gdk_cursor_new_for_display (gtk_widget_get_display (shape_window), GDK_CENTER_PTR); gdk_window_set_cursor(shape_window->window, cursor); gdk_cursor_unref (cursor); g_signal_connect (shape_window, "button_press_event", G_CALLBACK (shape_pressed),NULL); g_signal_connect (shape_window, "button_release_event", G_CALLBACK (shape_released),NULL); g_signal_connect (shape_window, "motion_notify_event", G_CALLBACK (shape_motion),NULL); icon_pos = g_new (CursorOffset, 1); g_object_set_data (G_OBJECT (shape_window), "cursor-offset", icon_pos); /* gtk_widget_show (shape_window);*/ g_signal_connect (eventbox, "button_press_event", G_CALLBACK (preview_pressed), NULL); g_signal_connect (drawing_area, "expose_event", G_CALLBACK (repaint_da), drawing_area); g_signal_connect (shape_drawing_area, "expose_event", G_CALLBACK (maybeblocked_expose), shape_drawing_area); root_win = gdk_get_default_root_window (); } static void do_playback (void) { int i; width = gimp_image_width (image_id); height = gimp_image_height (image_id); layers = gimp_image_get_layers (image_id, &total_frames); imagetype = gimp_image_base_type (image_id); if (imagetype == GIMP_INDEXED) palette = gimp_image_get_cmap (image_id, &ncolours); else if (imagetype == GIMP_GRAY) { /* This is a bit sick, until this plugin ever gets real GRAY support (not worth it?) */ palette = g_malloc(768); for (i=0;i<256;i++) { palette[i*3] = palette[i*3+1] = palette[i*3+2] = i; } ncolours = 256; } frame_number = 0; /* cache hint "cache nothing", since we iterate over every tile in every layer. */ gimp_tile_cache_size (0); init_preview_misc(); build_dialog (gimp_image_base_type (image_id), gimp_image_get_name (image_id)); /* Make sure that whole preview is dirtied with pure-alpha */ total_alpha_preview(preview_data); render_frame(0); show_frame(); gtk_main (); gdk_flush (); } /* Rendering Functions */ static void render_frame (gint32 whichframe) { GimpPixelRgn pixel_rgn; static guchar *rawframe = NULL; static gint rawwidth = 0, rawheight = 0, rawbpp = 0; gint rawx = 0, rawy = 0; guchar *srcptr; guchar *destptr; gint i, j, k; /* imaginative loop variables */ DisposeType dispose; if (whichframe >= total_frames) { printf( "playback: Asked for frame number %d in a %d-frame animation!\n", (int) (whichframe+1), (int) total_frames); exit(-1); } drawable = gimp_drawable_get (layers[total_frames-(whichframe+1)]); /* Lame attempt to catch the case that a user has closed the image. */ if (! (drawable->width > 0 && drawable->height > 0)) { gtk_main_quit (); return; } dispose = get_frame_disposal (frame_number); /* Image has been closed/etc since we got the layer list? */ /* FIXME - How do we tell if a gimp_drawable_get() fails? */ if (gimp_drawable_width (drawable->drawable_id) == 0) gtk_dialog_response (GTK_DIALOG (dlg), GTK_RESPONSE_CLOSE); if (((dispose==DISPOSE_REPLACE)||(whichframe==0)) && gimp_drawable_has_alpha (drawable->drawable_id)) { total_alpha_preview (preview_data); } /* only get a new 'raw' drawable-data buffer if this and the previous raw buffer were different sizes*/ if ((rawwidth*rawheight*rawbpp) != ((gimp_drawable_width(drawable->drawable_id)* gimp_drawable_height(drawable->drawable_id)* gimp_drawable_bpp(drawable->drawable_id)))) { if (rawframe != NULL) g_free(rawframe); rawframe = g_malloc((gimp_drawable_width(drawable->drawable_id)) * (gimp_drawable_height(drawable->drawable_id)) * (gimp_drawable_bpp(drawable->drawable_id))); } rawwidth = gimp_drawable_width(drawable->drawable_id); rawheight = gimp_drawable_height(drawable->drawable_id); rawbpp = gimp_drawable_bpp(drawable->drawable_id); /* Initialise and fetch the whole raw new frame */ gimp_pixel_rgn_init (&pixel_rgn, drawable, 0, 0, drawable->width, drawable->height, FALSE, FALSE); gimp_pixel_rgn_get_rect (&pixel_rgn, rawframe, 0, 0, drawable->width, drawable->height); /* gimp_pixel_rgns_register (1, &pixel_rgn);*/ gimp_drawable_offsets (drawable->drawable_id, &rawx, &rawy); /* render... */ switch (imagetype) { case GIMP_RGB: if ((rawwidth==width) && (rawheight==height) && (rawx==0) && (rawy==0)) { /* --- These cases are for the best cases, in --- */ /* --- which this frame is the same size and position --- */ /* --- as the preview buffer itself --- */ if (gimp_drawable_has_alpha (drawable->drawable_id)) { /* alpha */ destptr = preview_data; srcptr = rawframe; i = rawwidth*rawheight; while (i--) { if (!(*(srcptr+3)&128)) { srcptr += 4; destptr += 3; continue; } *(destptr++) = *(srcptr++); *(destptr++) = *(srcptr++); *(destptr++) = *(srcptr++); srcptr++; } /* calculate the shape mask */ if (shaping) { srcptr = rawframe + 3; for (j=0;jwindow, shape_drawing_area->style->white_gc, 0, 0, width, height, (total_frames==1)?GDK_RGB_DITHER_MAX :DITHERTYPE, preview_data, width * 3); } else { reshape_from_bitmap (shape_preview_mask); gdk_draw_rgb_image (drawing_area->window, drawing_area->style->white_gc, 0, 0, width, height, (total_frames==1)?GDK_RGB_DITHER_MAX :DITHERTYPE, preview_data, width * 3); } } else { /* --- These are suboptimal catch-all cases for when --- */ /* --- this frame is bigger/smaller than the preview --- */ /* --- buffer, and/or offset within it. --- */ if (gimp_drawable_has_alpha (drawable->drawable_id)) { /* alpha */ srcptr = rawframe; for (j=rawy; j=0 && i=0 && j=0 && i=0 && j=0 && i=0 && jwindow, shape_drawing_area->style->white_gc, 0, top, width, bottom-top, (total_frames==1)?GDK_RGB_DITHER_MAX :DITHERTYPE, &preview_data[3*top*width], width * 3); } else { reshape_from_bitmap (shape_preview_mask); gdk_draw_rgb_image (shape_drawing_area->window, shape_drawing_area->style->white_gc, 0, 0, width, height, (total_frames==1)?GDK_RGB_DITHER_MAX :DITHERTYPE, preview_data, width * 3); } } else { if ((dispose!=DISPOSE_REPLACE)&&(whichframe!=0)) { int top, bottom; top = (rawy < 0) ? 0 : rawy; bottom = (rawy+rawheight) < height ? (rawy+rawheight) : height-1; gdk_draw_rgb_image (drawing_area->window, drawing_area->style->white_gc, 0, top, width, bottom-top, (total_frames==1)?GDK_RGB_DITHER_MAX :DITHERTYPE, &preview_data[3*top*width], width * 3); } else { gdk_draw_rgb_image (drawing_area->window, drawing_area->style->white_gc, 0, 0, width, height, (total_frames==1)?GDK_RGB_DITHER_MAX :DITHERTYPE, preview_data, width * 3); } } } break; case GIMP_GRAY: case GIMP_INDEXED: if ((rawwidth==width) && (rawheight==height) && (rawx==0) && (rawy==0)) { /* --- These cases are for the best cases, in --- */ /* --- which this frame is the same size and position --- */ /* --- as the preview buffer itself --- */ if (gimp_drawable_has_alpha (drawable->drawable_id)) { /* alpha */ destptr = preview_data; srcptr = rawframe; i = rawwidth*rawheight; while (i--) { if (!(*(srcptr+1))) { srcptr += 2; destptr += 3; continue; } *(destptr++) = palette[3*(*(srcptr))]; *(destptr++) = palette[1+3*(*(srcptr))]; *(destptr++) = palette[2+3*(*(srcptr))]; srcptr+=2; } /* calculate the shape mask */ if (shaping) { srcptr = rawframe + 1; for (j=0;jwindow, shape_drawing_area->style->white_gc, 0, 0, width, height, (total_frames==1)?GDK_RGB_DITHER_MAX :DITHERTYPE, preview_data, width * 3); } else { gdk_draw_rgb_image (drawing_area->window, drawing_area->style->white_gc, 0, 0, width, height, (total_frames==1)?GDK_RGB_DITHER_MAX :DITHERTYPE, preview_data, width * 3); } } else { /* --- These are suboptimal catch-all cases for when --- */ /* --- this frame is bigger/smaller than the preview --- */ /* --- buffer, and/or offset within it. --- */ if (gimp_drawable_has_alpha (drawable->drawable_id)) { /* alpha */ srcptr = rawframe; for (j=rawy; j=0 && i=0 && j=0 && i=0 && j=0 && i=0 && jwindow, shape_drawing_area->style->white_gc, 0, top, width, bottom-top, (total_frames==1)?GDK_RGB_DITHER_MAX :DITHERTYPE, &preview_data[3*top*width], width * 3); } else { reshape_from_bitmap (shape_preview_mask); gdk_draw_rgb_image (shape_drawing_area->window, shape_drawing_area->style->white_gc, 0, 0, width, height, (total_frames==1)?GDK_RGB_DITHER_MAX :DITHERTYPE, preview_data, width * 3); } } else { if ((dispose!=DISPOSE_REPLACE)&&(whichframe!=0)) { int top, bottom; top = (rawy < 0) ? 0 : rawy; bottom = (rawy+rawheight) < height ? (rawy+rawheight) : height-1; gdk_draw_rgb_image (drawing_area->window, drawing_area->style->white_gc, 0, top, width, bottom-top, (total_frames==1)?GDK_RGB_DITHER_MAX :DITHERTYPE, &preview_data[3*top*width], width * 3); } else { gdk_draw_rgb_image (drawing_area->window, drawing_area->style->white_gc, 0, 0, width, height, (total_frames==1)?GDK_RGB_DITHER_MAX :DITHERTYPE, preview_data, width * 3); } } } break; } /* clean up */ gimp_drawable_detach(drawable); } /* If we're using GDKRGB, we don't reshape in this function because it's too late (GDKRGB is synchronous). So this just updates the progress bar. */ static void show_frame (void) { gchar *text; /* update the dialog's progress bar */ gtk_progress_bar_set_fraction (progress, ((float)frame_number / (float)(total_frames-0.999))); text = g_strdup_printf (_("Frame %d of %d"), frame_number + 1, total_frames); gtk_progress_bar_set_text (progress, text); g_free (text); } static void init_preview_misc (void) { int i; preview_data = g_malloc(width*height*3); shape_preview_mask = g_malloc((width*height)/8 + 1 + height); preview_alpha1_data = g_malloc(width*3); preview_alpha2_data = g_malloc(width*3); for (i=0;i=length) || (!g_ascii_isdigit (str[offset]))) return 0; do { sum *= 10; sum += str[offset] - '0'; offset++; } while ((offset