/*
 *   Despeckle (adaptive median) filter for The GIMP -- an image manipulation
 *   program
 *
 *   Copyright 1997-1998 Michael Sweet (mike@easysw.com)
 *
 *   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.
 */

#include "config.h"

#include <string.h>

#ifdef __GNUC__
#warning GTK_DISABLE_DEPRECATED
#endif
#undef GTK_DISABLE_DEPRECATED

#include <gtk/gtk.h>

#include <libgimp/gimp.h>
#include <libgimp/gimpui.h>

#include "libgimp/stdplugins-intl.h"


/*
 * Constants...
 */

#define PLUG_IN_NAME	 "plug_in_despeckle"
#define PLUG_IN_VERSION	 "1.3.2 - 17 May 1998"
#define HELP_ID          "plug-in-despeckle"
#define PREVIEW_SIZE	 128
#define SCALE_WIDTH	 100
#define ENTRY_WIDTH	   3
#define MAX_RADIUS	  20
#define SORT_PIVOT	  20
#define FILTER_ADAPTIVE	 0x01
#define FILTER_RECURSIVE 0x02

#define despeckle_radius (despeckle_vals[0])	/* Radius of filter */
#define filter_type	 (despeckle_vals[1])	/* Type of filter */
#define black_level	 (despeckle_vals[2])	/* Black level */
#define white_level	 (despeckle_vals[3])	/* White level */

/*
 * 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	despeckle (GimpDrawable *drawable);

static gint	despeckle_dialog (void);

static gboolean  despeckle_dialog          (void);

static void      dialog_iscale_update      (GtkAdjustment *, gint *);
static void      dialog_adaptive_callback  (GtkWidget *, gpointer);
static void      dialog_recursive_callback (GtkWidget *, gpointer);

static void      preview_exit              (void);
static void      preview_update            (void);
static void      preview_srcoll            (void);
static void      render_despeckle          (guchar   *dst_buffer,
                                            gint      width,
                                            gint      height,
                                            gint      bpp,
                                            gint      initialradius);
static void      quick_sort                (guchar   *buffer,
                                            gint      size);
static void      q_sort                    (guchar   *buffer,
                                            gint      left,
                                            gint      right);
static void      bubble_sort               (guchar   *buffer,
                                            gint      size);


/*
 * Globals...
 */

GimpPlugInInfo PLUG_IN_INFO =
{
  NULL,  /* init  */
  NULL,  /* quit  */
  query, /* query */
  run    /* run   */
};

static GtkWidget      *preview;                 /* Preview widget */
static gint            preview_width,		/* Width of preview widget */
  		       preview_height,		/* Height of preview widget */
		       preview_x1,		/* Upper-left X of preview */
		       preview_y1,		/* Upper-left Y of preview */
		       preview_x2,		/* Lower-right X of preview */
		       preview_y2;		/* Lower-right Y of preview */
static GtkObject      *hscroll_data,		/* Horizontal scrollbar data */
	       	      *vscroll_data;		/* Vertical scrollbar data */

static GimpDrawable   *drawable = NULL;		/* Current image */
static gint	       sel_x1,			/* Selection bounds */
		       sel_y1,
		       sel_x2,
		       sel_y2;
static gint	       sel_width,		/* Selection width */
		       sel_height;		/* Selection height */


static gint despeckle_vals[4] =
{
  3,
  FILTER_ADAPTIVE,
  7,
  248
};


/*
 * 'main()' - Main entry - just call gimp_main()...
 */

MAIN ()

/*
 * 'query()' - Respond to a plug-in query...
 */

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" },
    { GIMP_PDB_INT32,    "radius",   "Filter box radius (default = 3)" },
    { GIMP_PDB_INT32,    "type",     "Filter type (0 = median, 1 = adaptive, 2 = recursive-median, 3 = recursive-adaptive)" },
    { GIMP_PDB_INT32,    "black",    "Black level (-1 to 255)" },
    { GIMP_PDB_INT32,    "white",    "White level (0 to 256)" }
  };

  gimp_install_procedure (PLUG_IN_NAME,
			  "Despeckle filter, typically used to \'despeckle\' "
			  "a photographic image.",
			  "This plug-in selectively performs a median or "
			  "adaptive box filter on an image.",
			  "Michael Sweet <mike@easysw.com>",
			  "Copyright 1997-1998 by Michael Sweet",
			  PLUG_IN_VERSION,
			  N_("Des_peckle..."),
			  "RGB*, GRAY*",
			  GIMP_PLUGIN,
			  G_N_ELEMENTS (args), 0,
			  args, NULL);

  gimp_plugin_menu_register (PLUG_IN_NAME,
                             N_("<Image>/Filters/Enhance"));
}


/*
 * 'run()' - Run the filter...
 */

static void
run (const gchar      *name,
     gint              nparams,
     const GimpParam  *param,
     gint             *nreturn_vals,
     GimpParam       **return_vals)
{
  GimpRunMode	     run_mode;
  GimpPDBStatusType  status;
  GimpParam	    *values;

  INIT_I18N ();

  /*
   * Initialize parameter data...
   */

  status   = GIMP_PDB_SUCCESS;
  run_mode = param[0].data.d_int32;

  values = g_new (GimpParam, 1);

  values[0].type          = GIMP_PDB_STATUS;
  values[0].data.d_status = status;

  *nreturn_vals = 1;
  *return_vals  = values;

  /*
   * Get drawable information...
   */

  drawable = gimp_drawable_get (param[2].data.d_drawable);
  gimp_drawable_mask_bounds (drawable->drawable_id,
                             &sel_x1, &sel_y1, &sel_x2, &sel_y2);

  sel_width  = sel_x2 - sel_x1;
  sel_height = sel_y2 - sel_y1;
  /*
   * See how we will run
   */

  switch (run_mode)
    {
    case GIMP_RUN_INTERACTIVE :
      /*
       * Possibly retrieve data...
       */

      gimp_get_data (PLUG_IN_NAME, &despeckle_radius);

      /*
       * Get information from the dialog...
       */

      if (!despeckle_dialog ())
	return;
      break;

    case GIMP_RUN_NONINTERACTIVE:
      /*
       * Make sure all the arguments are present...
       */

      if (nparams < 4 || nparams > 7)
	status = GIMP_PDB_CALLING_ERROR;
      else if (nparams == 4)
	{
	  despeckle_radius = param[3].data.d_int32;
	  filter_type      = FILTER_ADAPTIVE;
	  black_level      = 7;
	  white_level      = 248;
	}
      else if (nparams == 5)
	{
	  despeckle_radius = param[3].data.d_int32;
	  filter_type      = param[4].data.d_int32;
	  black_level      = 7;
	  white_level      = 248;
	}
      else if (nparams == 6)
	{
	  despeckle_radius = param[3].data.d_int32;
	  filter_type      = param[4].data.d_int32;
	  black_level      = param[5].data.d_int32;
	  white_level      = 248;
	}
      else
	{
	  despeckle_radius = param[3].data.d_int32;
	  filter_type      = param[4].data.d_int32;
	  black_level      = param[5].data.d_int32;
	  white_level      = param[6].data.d_int32;
	};
      break;

    case GIMP_RUN_WITH_LAST_VALS:
      /*
       * Possibly retrieve data...
       */

      INIT_I18N();
      gimp_get_data (PLUG_IN_NAME, despeckle_vals);
	break;

    default:
      status = GIMP_PDB_CALLING_ERROR;
      break;
    };

  /*
   * Despeckle the image...
   */

  if (status == GIMP_PDB_SUCCESS)
    {
      if ((gimp_drawable_is_rgb(drawable->drawable_id) ||
	   gimp_drawable_is_gray(drawable->drawable_id)))
	{
	  /*
	   * Set the tile cache size...
	   */

	  gimp_tile_cache_ntiles (2 *
                                  (drawable->width + gimp_tile_width() - 1) /
				  gimp_tile_width() + 1);

	  /*
	   * Run!
	   */

	  despeckle (drawable);

	  /*
	   * If run mode is interactive, flush displays...
	   */

	  if (run_mode != GIMP_RUN_NONINTERACTIVE)
	    gimp_displays_flush ();

	  /*
	   * Store data...
	   */

	  if (run_mode == GIMP_RUN_INTERACTIVE)
	    gimp_set_data (PLUG_IN_NAME,
			   despeckle_vals, sizeof (despeckle_vals));
	}
      else
	status = GIMP_PDB_EXECUTION_ERROR;
    };

  /*
   * Reset the current run status...
   */

  values[0].data.d_status = status;

  /*
   * Detach from the drawable...
   */

  gimp_drawable_detach (drawable);
}


/*
 * 'despeckle()' - Despeckle an image using a median filter.
 *
 * A median filter basically collects pixel values in a region around the
 * target pixel, sorts them, and uses the median value. This code uses a
 * circular row buffer to improve performance.
 *
 * The adaptive filter is based on the median filter but analizes the histogram
 * of the region around the target pixel and adjusts the despeckle radius
 * accordingly.
 */

static void
despeckle (GimpDrawable *drawable)
{
  GimpPixelRgn	src_rgn,	/* Source image region */
		dst_rgn;	/* Destination image region */
  guchar       *buffer=NULL; 	/* Source pixel rows */
  gint		bpp;		/* bytes per pixel */
  /*
   * Let the user know what we're doing...
   */

  gimp_progress_init (_("Despeckling..."));

  /*
   * Setup for filter...
   */

  bpp = gimp_drawable_bpp (drawable->drawable_id);

  gimp_pixel_rgn_init (&src_rgn, drawable,
                       sel_x1, sel_y1, sel_width, sel_height,
		       FALSE, FALSE);
  gimp_pixel_rgn_init (&dst_rgn, drawable,
                       sel_x1, sel_y1, sel_width, sel_height,
		       TRUE, TRUE);

  g_free (buffer);
  buffer = g_new (guchar, sel_width * sel_height * bpp);

  gimp_pixel_rgn_get_rect (&src_rgn,
                           buffer, sel_x1, sel_y1, sel_width, sel_height);

  render_despeckle (buffer, sel_width,  sel_height, bpp, despeckle_radius);

  gimp_pixel_rgn_set_rect (&dst_rgn,
                           buffer, sel_x1, sel_y1, sel_width, sel_height);

  g_free (buffer);


  gimp_drawable_flush (drawable);
  gimp_drawable_merge_shadow (drawable->drawable_id, TRUE);
  gimp_drawable_update (drawable->drawable_id,
			sel_x1, sel_y1, sel_width, sel_height);
}

static void
render_despeckle (guchar *dst_buffer,
               gint width,
               gint height,
               gint bpp,
               gint initialradius)
{
   gint    maskwidth;
   guchar *row_buffer = NULL;      /* Row operations buffer */
   guchar *rec_buffer = NULL;	   /* Non FILTER_RECURSIVE buffer */
   guchar *values = NULL;          /* Channel buffer */
   gint   row, col;                /* Row and column for buffer */
   gint   maskrow, maskcol;        /* Row and column for mask buffer */
   gint   absrow, abscol;          /* Absolute row/col for surrounding pixels */
   gint   valuecount;              /* Count sected pixels */
   gint   pos,rowpos,rowposalpha;  /* Position within buffer */
   gint   medpos;                  /* Median value position in value buffer */
   gint   alphasum=0;              /* Sum of alpha channel values */
   gint   radiusrgba[4];           /* Working radius */
   gint   radius,channel;          /* Non specific loop variable */
   gint  hist0   = 0;
   gint  hist255 = 0;

   radiusrgba[0] = radiusrgba[1] = radiusrgba[2] = radiusrgba[3]=
     initialradius;

   /* filter temporary buffer */
   rec_buffer = g_new (guchar, width * height * bpp);
   memcpy (rec_buffer, dst_buffer, width * height * bpp);

   /* Process each row */
   for (row = 0; row < height; row++)
     {
        /* Make sure row buffer is fresh */
        g_free (row_buffer);
        row_buffer = g_new (guchar, width * bpp);

        /* copy contents from tomporary buffer in row buffer */
        memcpy (row_buffer, (rec_buffer+(row*width*bpp)), width*bpp);

        /* process each column */
        for (col = 0 ; col < width ; col++)
    	  {
            /* Calculate starting with alpha */
            for (channel = bpp-1; channel >= 0; channel--)
              {
                /* addaptive radius per channel */
                radius = radiusrgba[channel];
                maskwidth = radius * 2 + 1;

                /* Skip edges 
                if ( (row - radius) < 0 || (row+radius) >= height ||
                     (col - radius) < 0 || (col+radius) >= width    )
                  continue;
                */

                /* Position in buffer */
                pos         = ((row * width) + col) * bpp + channel;
                rowpos      = col * bpp + channel;
                rowposalpha = col * bpp + 3;

                /* Value buffer */
                g_free (values);
                values = g_new(guchar, (SQR (maskwidth)));

                hist0      = 0;
                hist255    = 0;
                valuecount = 0;

                for (maskrow = 0 ; maskrow < maskwidth ; maskrow++)
                  {
                    for (maskcol=0 ; maskcol < maskwidth ; maskcol++)
                      {
                        abscol = col-radius+maskcol;
                        absrow = row-radius+maskrow;

                        if (abscol >= 0     &&
                            absrow >= 0     &&
                            abscol <  width &&
                            absrow < height ) 
                          {
                             pos = ((absrow * width) + abscol) * bpp + channel;
                             if (channel < 3 )
                               {
                                 if ( *(rec_buffer+pos) <= black_level)
                                   hist0 ++;
                                 else if (*(rec_buffer+pos) >= white_level)
                                   hist255 ++;
                                 if ( *(rec_buffer+pos) < white_level && 
                                      *(rec_buffer+pos) > black_level )
                                   {
                                     *(values+valuecount) = *(rec_buffer+pos);
                                     valuecount++;
                                   }
                               }
                             else
                               {                                  
                                  *(values+valuecount) = *(rec_buffer+pos);
                                  valuecount++;
                               }
                          }
                      }
                  }

                /*
                 * Sort pixels and
                 * Assign the median value...
                 */

                switch (valuecount)
                  {
                    case 0:   /* No values within histogram */
                      if (channel == 3)
                        *(row_buffer+rowpos) = 255;
                      break;

                    case 1:   /* Only one value within histogram so assign */
                      if (channel == 3)
                        alphasum =(*(values) == 0) ? 1 :  *(values);
                      *(row_buffer+rowpos)   = *(values);
                      break;

                    case 2:   /* Two values median = mean */
                      if (channel == 3 )
                        {
                          alphasum =   *(values) + *(values+1);
                          alphasum =(alphasum == 0) ? 1 : alphasum;
                          *(row_buffer+rowpos) =  alphasum/2 ;
                        }
                      else
                        {
                          alphasum =(alphasum == 0) ? 1 : alphasum;
                          if (bpp==4)
                            *(row_buffer+rowpos) = 
                                      (*(row_buffer+rowposalpha) * 
                                        (*(values) + *(values+1)))/alphasum;
                          else
                            *(row_buffer+rowpos) = (*(values) + *(values+1))/2;
                        }
                      break;

                    default:
                      if (valuecount > SORT_PIVOT)
                        quick_sort (values, valuecount);
                      else
                        bubble_sort (values, valuecount);
                      if (valuecount & 1)
                        {
                          /* compensate for base 0 */
                          medpos = (valuecount-1)/2;
                          *(row_buffer+rowpos)   = *(values+medpos);
                        }
                      else
                        {
                          if (channel == 3)
                            {
                              alphasum =   *(values) + *(values+1);
                              *(row_buffer+rowpos) =  alphasum/2 ;
                            }
                          else
                            {
                              /* compensate for base 0 */
                              medpos = (valuecount-1)/2;
                              alphasum =(alphasum == 0) ? 1 : alphasum;

                              if (bpp == 4)
                                *(row_buffer+rowpos) = 
                                  (*(row_buffer+rowposalpha) *
                                    (*(values+medpos) + *(values+medpos+1)) ) / 
                                    alphasum;
                              else
                                *(row_buffer+rowpos) = (*(values+medpos) + 
                                                        *(values+medpos+1)) / 2;
                            }
                        }
                      break;
                  }

                if (filter_type & FILTER_RECURSIVE)
                  {
                     /* Position in buffer */
                     pos = ((row * width) + col) * bpp + channel;
                     *(rec_buffer+pos) = *(row_buffer+rowpos);
                  }

                /*
                 * Check the histogram and adjust the radius accordingly...
                 */

                if (filter_type & FILTER_ADAPTIVE)
		  {
		    if (hist0 >= radius || hist255 >= radius)
		      {
		        if (radius < initialradius)
		  	radius ++;
		      }
		    else if (radius > 1)
		      radius --;

                    radiusrgba[channel]=radius;
 		  }
             } /* channel */
          } /* colum counter x */

        /* Copy temporatry buffer back to destination buffer */
        memcpy ((dst_buffer+(row*width*bpp)),row_buffer, width*bpp);
     } /* row counter y */

     g_free (values);
     g_free (row_buffer);
     g_free (rec_buffer);
}

/*
 * 'despeckle_dialog()' - Popup a dialog window for the filter box size...
 */

static gint
despeckle_dialog (void)
{
  GtkWidget *dialog;
  GtkWidget *main_vbox;
  GtkWidget *hbox;
  GtkWidget *vbox;
  GtkWidget *table;
  GtkWidget *ptable;
  GtkWidget *frame;
  GtkWidget *scrollbar;
  GtkWidget *button;
  GtkObject *adj;
  gboolean   run;

  gimp_ui_init ("despeckle", TRUE);

  dialog = gimp_dialog_new (_("Despeckle"), "despeckle",
                            NULL, 0,
			    gimp_standard_help_func, HELP_ID,

			    GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL,
			    GTK_STOCK_OK,     GTK_RESPONSE_OK,

			    NULL);

  main_vbox = gtk_vbox_new (FALSE, 12);
  gtk_container_set_border_width (GTK_CONTAINER (main_vbox), 12);
  gtk_box_pack_start (GTK_BOX (GTK_DIALOG (dialog)->vbox), main_vbox,
		      TRUE, TRUE, 0);
  gtk_widget_show (main_vbox);

  hbox = gtk_hbox_new (FALSE, 12);
  gtk_box_pack_start (GTK_BOX (main_vbox), hbox, FALSE, FALSE, 0);
  gtk_widget_show (hbox);

  /*
   * Preview window...
   */

  ptable = gtk_table_new (2, 2, FALSE);
  gtk_box_pack_start (GTK_BOX (hbox), ptable, FALSE, FALSE, 0);
  gtk_widget_show(ptable);

  frame = gtk_frame_new (NULL);
  gtk_frame_set_shadow_type (GTK_FRAME (frame), GTK_SHADOW_IN);
  gtk_table_attach (GTK_TABLE (ptable), frame, 0, 1, 0, 1, 0, 0, 0, 0);
  gtk_widget_show (frame);

  preview_width  = MIN (sel_width, PREVIEW_SIZE);
  preview_height = MIN (sel_height, PREVIEW_SIZE);

  preview = gtk_preview_new (GTK_PREVIEW_COLOR);
  gtk_preview_size (GTK_PREVIEW (preview), preview_width, preview_height);
  gtk_container_add (GTK_CONTAINER (frame), preview);
  gtk_widget_show (preview);

  hscroll_data = gtk_adjustment_new (0, 0, sel_width - 1, 1.0,
				     MIN (preview_width, sel_width),
				     MIN (preview_width, sel_width));

  g_signal_connect (hscroll_data, "value_changed",
                    G_CALLBACK (preview_srcoll),
                    NULL);

  scrollbar = gtk_hscrollbar_new (GTK_ADJUSTMENT (hscroll_data));
  gtk_range_set_update_policy (GTK_RANGE (scrollbar), GTK_UPDATE_CONTINUOUS);
  gtk_table_attach (GTK_TABLE (ptable), scrollbar,
                    0, 1, 1, 2, GTK_FILL, 0, 0, 0);
  gtk_widget_show (scrollbar);

  vscroll_data = gtk_adjustment_new (0, 0, sel_height - 1, 1.0,
				     MIN (preview_height, sel_height),
				     MIN (preview_height, sel_height));

  g_signal_connect (vscroll_data, "value_changed",
                    G_CALLBACK (preview_srcoll),
                    NULL);

  scrollbar = gtk_vscrollbar_new (GTK_ADJUSTMENT (vscroll_data));
  gtk_range_set_update_policy (GTK_RANGE (scrollbar), GTK_UPDATE_CONTINUOUS);
  gtk_table_attach (GTK_TABLE (ptable), scrollbar, 1, 2, 0, 1, 0,
		    GTK_FILL, 0, 0);
  gtk_widget_show (scrollbar);


  preview_x1 = sel_x1;
  preview_y1 = sel_y1;
  preview_x2 = preview_x1 + MIN (preview_width, sel_width);
  preview_y2 = preview_y1 + MIN (preview_height, sel_height);

  /*
   * Filter type controls...
   */

  frame = gimp_frame_new (_("Type"));
  gtk_box_pack_start (GTK_BOX (hbox), frame, TRUE, TRUE, 0);
  gtk_widget_show (frame);

  vbox = gtk_vbox_new (FALSE, 6);
  gtk_container_add (GTK_CONTAINER (frame), vbox);
  gtk_widget_show (vbox);

  button = gtk_check_button_new_with_mnemonic (_("_Adaptive"));
  gtk_box_pack_start (GTK_BOX (vbox), button, FALSE, FALSE, 0);
  gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (button),
				(filter_type & FILTER_ADAPTIVE));
  gtk_widget_show (button);

  g_signal_connect (button, "toggled",
                    G_CALLBACK (dialog_adaptive_callback),
                    NULL);

  button = gtk_check_button_new_with_mnemonic (_("R_ecursive"));
  gtk_box_pack_start (GTK_BOX (vbox), button, FALSE, FALSE, 0);
  gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (button),
				(filter_type & FILTER_ADAPTIVE));
  gtk_widget_show (button);

  g_signal_connect (button, "toggled",
                    G_CALLBACK (dialog_recursive_callback),
                    NULL);

  table = gtk_table_new (3, 3, FALSE);
  gtk_table_set_col_spacings (GTK_TABLE (table), 6);
  gtk_table_set_row_spacings (GTK_TABLE (table), 6);
  gtk_box_pack_start (GTK_BOX (main_vbox), table, FALSE, FALSE, 0);
  gtk_widget_show (table);

  /*
   * Box size (radius) control...
   */

  adj = gimp_scale_entry_new (GTK_TABLE (table), 0, 0,
			      _("_Radius:"), SCALE_WIDTH, ENTRY_WIDTH,
			      despeckle_radius, 1, MAX_RADIUS, 1, 5, 0,
			      TRUE, 0, 0,
			      NULL, NULL);
  g_signal_connect (adj, "value_changed",
                    G_CALLBACK (dialog_iscale_update),
                    &despeckle_radius);

  /*
   * Black level control...
   */

  adj = gimp_scale_entry_new (GTK_TABLE (table), 0, 1,
			      _("_Black level:"), SCALE_WIDTH, ENTRY_WIDTH,
			      black_level, -1, 255, 1, 8, 0,
			      TRUE, 0, 0,
			      NULL, NULL);
  g_signal_connect (adj, "value_changed",
                    G_CALLBACK (dialog_iscale_update),
                    &black_level);

  /*
   * White level control...
   */

  adj = gimp_scale_entry_new (GTK_TABLE (table), 0, 2,
			      _("_White level:"), SCALE_WIDTH, ENTRY_WIDTH,
			      white_level, 0, 256, 1, 8, 0,
			      TRUE, 0, 0,
			      NULL, NULL);
  g_signal_connect (adj, "value_changed",
                    G_CALLBACK (dialog_iscale_update),
                    &white_level);

  /*
   * Show it and wait for the user to do something...
   */

  gtk_widget_show (dialog);

  preview_update ();

  run = (gimp_dialog_run (GIMP_DIALOG (dialog)) == GTK_RESPONSE_OK);

  gtk_widget_destroy (dialog);

  /*
   * Return ok/cancel...
   */

  return run;
}


/*
 * 'preview_update()' - Update the preview window.
 */
static void
preview_update (void)
{
   GimpPixelRgn	src_rgn;	/* Source image region */
   gint	        x, y, offset;	/* Current location in image */
   guchar	check;		/* Checkerboard pattern */
   guchar      *preview_src = NULL;
   gint         radius      = despeckle_radius;
   gint         pos, preview_pos;       /* Position within buffer        */
   guchar      *render_buffer = NULL;   /* Buffer to hold rendered image */
   gint         bpp;

   /*
    * Setup for filter...
    */
   gimp_pixel_rgn_init (&src_rgn, drawable,
                        preview_x1, preview_y1, preview_width, preview_height,
                        FALSE, FALSE);
   bpp = gimp_drawable_bpp (drawable->drawable_id);

   /*
    * Pre-load the preview rectangle...
    */
   g_free (preview_src);
   preview_src  = g_new (guchar, preview_width * preview_height * bpp);
   gimp_pixel_rgn_get_rect (&src_rgn, preview_src, preview_x1, preview_y1,
                            preview_width, preview_height);

   render_despeckle (preview_src, preview_width,  preview_height, bpp, radius);

   /* Preview is RGB */
   g_free (render_buffer);
   render_buffer = g_new (guchar, preview_width * preview_height * 3);

   for ( y = 0 ; y < preview_height ; y++ )
     for ( x = 0 ; x < preview_width ; x++ )
       {
         pos = (y*preview_width+x)*bpp;
         preview_pos = (y*preview_width+x)*3;

         switch (bpp)
           {
             case 1:
               *(render_buffer+preview_pos)   = *(preview_src+pos);
               *(render_buffer+preview_pos+1) = *(preview_src+pos);
               *(render_buffer+preview_pos+2) = *(preview_src+pos);
               break;
             case 2:
               if (*(preview_src+pos) == 255)
                 {
                   *(render_buffer+preview_pos)   = *(preview_src+pos);
                   *(render_buffer+preview_pos+1) = *(preview_src+pos);
                   *(render_buffer+preview_pos+2) = *(preview_src+pos);
                 }
               else
                 {
                   if ((y & GIMP_CHECK_SIZE) ^ (x & GIMP_CHECK_SIZE))
                     check = GIMP_CHECK_LIGHT * 255;
                   else
                check = GIMP_CHECK_DARK * 255;
                   if (*(preview_src+pos) == 0)
                     {
                       *(render_buffer+preview_pos)   = check;
                       *(render_buffer+preview_pos+1) = check;
                       *(render_buffer+preview_pos+2) = check;
                     }
                   else
                     {
                       *(render_buffer+preview_pos)   = check + 
                         ((*(preview_src+pos) - check) * *(preview_src+pos+1)) / 
                         255;
                       *(render_buffer+preview_pos+1) = 
                         *(render_buffer+preview_pos);
                       *(render_buffer+preview_pos+2) = 
                         *(render_buffer+preview_pos);
                     }
            };
               break;
            case 3:
               *(render_buffer+preview_pos)   = *(preview_src+pos);
               *(render_buffer+preview_pos+1) = *(preview_src+pos+1);
               *(render_buffer+preview_pos+2) = *(preview_src+pos+2);
               break;
             case 4:
               if (*(preview_src+pos+3) == 255)
                 {
                    *(render_buffer+preview_pos)   = *(preview_src+pos);
                    *(render_buffer+preview_pos+1) = *(preview_src+pos+1);
                    *(render_buffer+preview_pos+2) = *(preview_src+pos+2);
                 }
               else
                 {
                   if ((y & GIMP_CHECK_SIZE) ^ (x & GIMP_CHECK_SIZE))
                     check = GIMP_CHECK_LIGHT * 255;
                   else
                     check = GIMP_CHECK_DARK * 255;
                   if (*(preview_src+pos+3) == 0)
                     {
                        *(render_buffer+preview_pos)   = check;
                        *(render_buffer+preview_pos+1) = check;
                        *(render_buffer+preview_pos+2) = check;
                     }
                   else
                     {
                        *(render_buffer+preview_pos)   = check + 
                          ((*(preview_src+pos+0) - check) * 
                          *(preview_src+pos+3)) / 255;
                        *(render_buffer+preview_pos+1) = check + 
                          ((*(preview_src+pos+1) - check) * 
                          *(preview_src+pos+3)) / 255;
                        *(render_buffer+preview_pos+2) = check + 
                          ((*(preview_src+pos+2) - check) * 
                          *(preview_src+pos+3)) / 255;
                     }
                 }
               break;
  	    }

       }


   /*
    * Update the screen...
    */
   for (y = 0; y < preview_height; y++)
     {
        offset = y * 3 * preview_width;
        gtk_preview_draw_row (GTK_PREVIEW (preview),
                              render_buffer + offset, 0, y, preview_width);
     }

   gtk_widget_queue_draw (preview);

   g_free (render_buffer);
   g_free (preview_src);
   preview_exit();
}

/*
 * 'preview_scroll()' - Update the preview when a scrollbar is moved.
 */

static void
preview_srcoll (void)
{
  preview_x1 = sel_x1 + GTK_ADJUSTMENT (hscroll_data)->value;
  preview_y1 = sel_y1 + GTK_ADJUSTMENT (vscroll_data)->value;
  preview_x2 = preview_x1 + MIN (preview_width, sel_width);
  preview_y2 = preview_y1 + MIN (preview_height, sel_height);

  preview_update ();
}

static void
preview_exit (void)
{
}

static void
dialog_iscale_update (GtkAdjustment *adjustment,
		      gint          *value)
{
  *value = adjustment->value;

  preview_update ();
}

static void
dialog_adaptive_callback (GtkWidget *widget,
			  gpointer   data)
{
  if (GTK_TOGGLE_BUTTON (widget)->active)
    filter_type |= FILTER_ADAPTIVE;
  else
    filter_type &= ~FILTER_ADAPTIVE;

  preview_update ();
}

static void
dialog_recursive_callback (GtkWidget *widget,
			   gpointer  data)
{
  if (GTK_TOGGLE_BUTTON (widget)->active)
    filter_type |= FILTER_RECURSIVE;
  else
    filter_type &= ~FILTER_RECURSIVE;

  preview_update ();
}

/* School book sort */
static void
quick_sort (guchar *buffer,
            gint    size)
{
  q_sort (buffer, 0, size - 1);
}

static void
q_sort (guchar *buffer,
        gint    left,
        gint    right)
{
  gint p, l, r;

  l = left;
  r = right;
  p = buffer[left];

  while (left < right)
    {
      while ((buffer[right] >= p) && (left < right))
        right--;

      if (left != right)
        {
          buffer[left] = buffer[right];
          left++;
        }
      while ((buffer[left] <= p) && (left < right))
        left++;
      if (left != right)
        {
          buffer[right] = buffer[left];
          right--;
        }
    }

  buffer[left] = p;
  p = left;
  left = l;
  right = r;

  if (left < p)
    q_sort (buffer, l, p-1);
  if (right > p)
    q_sort (buffer, p+1, r);
}

/* School book sort */
static void
bubble_sort (guchar *buffer,
             gint    size)
{
  gint i, j, t;

  for (i = (size - 1); i >= 0; i--)
    {
      for (j = 1; j <= i; j++)
        {
          if (buffer[j-1] > buffer[j])
            {
              t = buffer[j-1];
              buffer[j-1] = buffer[j];
              buffer[j] = t;
            }
        }
    }
}
