diff --git a/ChangeLog b/ChangeLog index 48cc441dbb..6cb3fa4b58 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,12 @@ +Sun Aug 29 17:54:58 BST 1999 Adam D. Moss + + * app/convert.c: *->I: Deterministic colour dithering + to arbitrary palettes. Ideal for animations that are + going to be delta-optimized or simply don't want to look + 'busy' in static areas. Also a bunch of bugfixes and tweaks. + No PDB interface to the new features yet, although a + convert_image2() is ready and waiting. + Sun Aug 29 00:40:20 BST 1999 Adam D. Moss * app/convert.h diff --git a/app/convert.c b/app/convert.c index 011e922a21..7f6fe24502 100644 --- a/app/convert.c +++ b/app/convert.c @@ -20,12 +20,16 @@ * TODO for Convert: * * Use palette of another open INDEXED image - * Different dither types (deterministic colour dithering) * * Do error-splitting trick for GREY->INDEXED */ /* + * 99/08/29 - Deterministic colour dithering to arbitrary palettes. + * Ideal for animations that are going to be delta-optimized or simply + * don't want to look 'busy' in static areas. Also a bunch of bugfixes + * and tweaks. [Adam] + * * 99/08/28 - Deterministic alpha dithering over layers, reduced bleeding * of transparent values into opaque values, added optional stage to * remove duplicate or unused colour entries from final colourmap. [Adam] @@ -101,6 +105,7 @@ #define NODITHER 0 #define FSDITHER 1 #define NODESTRUCTDITHER 2 +#define FIXEDDITHER 3 #define PRECISION_R 6 #define PRECISION_G 6 @@ -370,7 +375,9 @@ typedef struct GtkWidget* shell; GtkWidget* custom_frame; GimpImage* gimage; - int dither; /* flag */ + int nodither_flag; + int fsdither_flag; + int fixeddither_flag; int alphadither; /* flag */ int remdups; /* flag */ int num_cols; @@ -388,7 +395,6 @@ static gint indexed_delete_callback (GtkWidget *, GdkEvent *, gpointer); static void indexed_num_cols_update (GtkWidget *, gpointer); static void indexed_radio_update (GtkWidget *, gpointer); static void frame_sensitivity_update (GtkWidget *, gpointer); -static void indexed_dither_update (GtkWidget *, gpointer); static void indexed_alphadither_update (GtkWidget *, gpointer); static void indexed_remdups_update (GtkWidget *, gpointer); @@ -421,7 +427,9 @@ PaletteEntriesP theCustomPalette = NULL; /* Defaults */ static int snum_cols = 256; -static gboolean sdither = TRUE; +static gboolean sfsdither_flag = TRUE; +static gboolean snodither_flag = FALSE; +static gboolean sfixeddither_flag = FALSE; static gboolean smakepal_flag = TRUE; static gboolean salphadither_flag = FALSE; static gboolean sremdups_flag = TRUE; @@ -472,7 +480,9 @@ convert_to_indexed (GimpImage *gimage) dialog->custom_frame = NULL; dialog->num_cols = snum_cols; - dialog->dither = sdither; + dialog->nodither_flag = snodither_flag; + dialog->fsdither_flag = sfsdither_flag; + dialog->fixeddither_flag = sfixeddither_flag; dialog->alphadither = salphadither_flag; dialog->remdups = sremdups_flag; dialog->makepal_flag = smakepal_flag; @@ -505,7 +515,7 @@ convert_to_indexed (GimpImage *gimage) gtk_container_set_border_width (GTK_CONTAINER (vbox), 2); gtk_box_pack_start (GTK_BOX (vbox), hbox, FALSE, FALSE, 0); - toggle = gtk_radio_button_new_with_label (group, _("Generate optimal palette: ")); + toggle = gtk_radio_button_new_with_label (NULL, _("Generate optimal palette: ")); group = gtk_radio_button_group (GTK_RADIO_BUTTON (toggle)); gtk_box_pack_start (GTK_BOX (hbox), toggle, TRUE, TRUE, 0); gtk_signal_connect (GTK_OBJECT (toggle), "toggled", @@ -637,21 +647,60 @@ convert_to_indexed (GimpImage *gimage) gtk_container_add (GTK_CONTAINER (frame), vbox); gtk_widget_show(vbox); - /* The dither toggle */ + /* The dither radio buttons */ hbox = gtk_hbox_new (FALSE, 1); { gtk_box_pack_start (GTK_BOX (vbox), hbox, FALSE, FALSE, 0); - toggle = gtk_check_button_new_with_label (_("Enable Floyd-Steinberg dithering")); - gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (toggle), dialog->dither); - gtk_box_pack_start (GTK_BOX (hbox), toggle, TRUE, FALSE, 0); + toggle = gtk_radio_button_new_with_label (NULL, _("No colour dithering")); + group = gtk_radio_button_group (GTK_RADIO_BUTTON (toggle)); + + gtk_box_pack_start (GTK_BOX (hbox), toggle, FALSE, FALSE, 0); + gtk_signal_connect (GTK_OBJECT (toggle), "toggled", - (GtkSignalFunc) indexed_dither_update, - dialog); - gtk_widget_show (label); + (GtkSignalFunc) indexed_radio_update, + &(dialog->nodither_flag)); + gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (toggle), dialog->nodither_flag); + gtk_widget_show (toggle); } gtk_widget_show (hbox); + hbox = gtk_hbox_new (FALSE, 1); + { + gtk_box_pack_start (GTK_BOX (vbox), hbox, FALSE, FALSE, 0); + + toggle = gtk_radio_button_new_with_label (group, _("Positioned colour dithering")); + group = gtk_radio_button_group (GTK_RADIO_BUTTON (toggle)); + + gtk_box_pack_start (GTK_BOX (hbox), toggle, FALSE, FALSE, 0); + + gtk_signal_connect (GTK_OBJECT (toggle), "toggled", + (GtkSignalFunc) indexed_radio_update, + &(dialog->fixeddither_flag)); + gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (toggle), dialog->fixeddither_flag); + + gtk_widget_show (toggle); + } + gtk_widget_show (hbox); + hbox = gtk_hbox_new (FALSE, 1); + { + gtk_box_pack_start (GTK_BOX (vbox), hbox, FALSE, FALSE, 0); + + toggle = gtk_radio_button_new_with_label (group, _("Floyd-Steinberg colour dithering")); + group = gtk_radio_button_group (GTK_RADIO_BUTTON (toggle)); + + gtk_box_pack_start (GTK_BOX (hbox), toggle, FALSE, FALSE, 0); + + gtk_signal_connect (GTK_OBJECT (toggle), "toggled", + (GtkSignalFunc) indexed_radio_update, + &(dialog->fsdither_flag)); + gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (toggle), dialog->fsdither_flag); + + gtk_widget_show (toggle); + } + gtk_widget_show (hbox); + + /* The alpha-dither toggle */ hbox = gtk_hbox_new (FALSE, 1); @@ -660,7 +709,7 @@ convert_to_indexed (GimpImage *gimage) toggle = gtk_check_button_new_with_label (_("Enable dithering of transparency")); gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (toggle), dialog->alphadither); - gtk_box_pack_start (GTK_BOX (hbox), toggle, TRUE, FALSE, 0); + gtk_box_pack_start (GTK_BOX (hbox), toggle, FALSE, FALSE, 0); gtk_signal_connect (GTK_OBJECT (toggle), "toggled", (GtkSignalFunc) indexed_alphadither_update, dialog); @@ -837,6 +886,7 @@ indexed_ok_callback (GtkWidget *widget, { IndexedDialog *dialog; int palette_type; + int dither_type; dialog = (IndexedDialog *) client_data; @@ -849,16 +899,25 @@ indexed_ok_callback (GtkWidget *widget, if (dialog->makepal_flag) palette_type = MAKE_PALETTE; else palette_type = REUSE_PALETTE; + + if (dialog->nodither_flag) dither_type = NODITHER; + else + if (dialog->fsdither_flag) dither_type = FSDITHER; + else + dither_type = FIXEDDITHER; + /* Convert the image to indexed color */ convert_image2 (dialog->gimage, INDEXED, dialog->num_cols, - dialog->dither, dialog->alphadither, + dither_type, dialog->alphadither, dialog->remdups, palette_type); gdisplays_flush (); /* Save defaults for next time */ snum_cols = dialog->num_cols; - sdither = dialog->dither; + snodither_flag = dialog->nodither_flag; + sfsdither_flag = dialog->fsdither_flag; + sfixeddither_flag = dialog->fixeddither_flag; salphadither_flag = dialog->alphadither; sremdups_flag = dialog->remdups; smakepal_flag = dialog->makepal_flag; @@ -925,20 +984,6 @@ frame_sensitivity_update (GtkWidget *widget, gtk_widget_set_sensitive (dialog->custom_frame, dialog->custompal_flag); } -static void -indexed_dither_update (GtkWidget *w, - gpointer data) -{ - IndexedDialog *dialog; - - dialog = (IndexedDialog *) data; - - if (GTK_TOGGLE_BUTTON (w)->active) - dialog->dither = TRUE; - else - dialog->dither = FALSE; -} - static void indexed_alphadither_update (GtkWidget *w, gpointer data) @@ -1204,9 +1249,9 @@ convert_image2 (GImage *gimage, /* don't dither if the input is grayscale and we are simply mapping every color */ if (old_type == GRAY && num_cols == 256 && palette_type == MAKE_PALETTE) - dither = FALSE; + dither = NODITHER; - quantobj = initialize_median_cut (old_type, num_cols, dither ? FSDITHER : NODITHER, + quantobj = initialize_median_cut (old_type, num_cols, dither, palette_type, alpha_dither); if (palette_type == MAKE_PALETTE) @@ -2981,14 +3026,13 @@ median_cut_pass2_no_dither_gray (QuantizeObj *quantobj, int row, col; int pixel; int has_alpha; + unsigned long* index_used_count = quantobj->index_used_count; int alpha_dither = quantobj->want_alpha_dither; int offsetx, offsety; void *pr; drawable_offsets (GIMP_DRAWABLE(layer), &offsetx, &offsety); - zero_histogram_gray (histogram); - has_alpha = layer_has_alpha (layer); pixel_region_init (&srcPR, GIMP_DRAWABLE(layer)->tiles, 0, 0, GIMP_DRAWABLE(layer)->width, GIMP_DRAWABLE(layer)->height, FALSE); pixel_region_init (&destPR, new_tiles, 0, 0, GIMP_DRAWABLE(layer)->width, GIMP_DRAWABLE(layer)->height, TRUE); @@ -3007,14 +3051,99 @@ median_cut_pass2_no_dither_gray (QuantizeObj *quantobj, /* and update the cache */ if (*cachep == 0) fill_inverse_cmap_gray (quantobj, histogram, pixel); - /* Now emit the colormap index for this cell */ - dest[INDEXED_PIX] = *cachep - 1; + if (has_alpha) - dest[ALPHA_I_PIX] = - (alpha_dither ? - ((src[ALPHA_G_PIX] << 6) > (255 * DM[(col+offsetx+srcPR.x)&DM_WIDTHMASK][(row+offsety+srcPR.y)&DM_HEIGHTMASK])) : - (src[ALPHA_G_PIX] > 127) - ) ? 255 : 0; + { + if ((dest[ALPHA_I_PIX] = + ( + (alpha_dither ? + ((src[ALPHA_G_PIX] << 6) > (255 * DM[(col+offsetx+srcPR.x)&DM_WIDTHMASK][(row+offsety+srcPR.y)&DM_HEIGHTMASK])) : + (src[ALPHA_G_PIX] > 127) + ) ? 255 : 0))) + index_used_count[dest[INDEXED_PIX] = *cachep - 1]++; + } + else + { + /* Now emit the colormap index for this cell */ + index_used_count[dest[INDEXED_PIX] = *cachep - 1]++; + } + + src += srcPR.bytes; + dest += destPR.bytes; + } + } + } +} + +static void +median_cut_pass2_fixed_dither_gray (QuantizeObj *quantobj, + Layer *layer, + TileManager *new_tiles) +{ + PixelRegion srcPR, destPR; + Histogram histogram = quantobj->histogram; + ColorFreq* cachep; + Color* color; + unsigned char *src, *dest; + int row, col; + int pixel; + int re, R; + unsigned long* index_used_count = quantobj->index_used_count; + int has_alpha; + int alpha_dither = quantobj->want_alpha_dither; + int offsetx, offsety; + void *pr; + + drawable_offsets (GIMP_DRAWABLE(layer), &offsetx, &offsety); + + has_alpha = layer_has_alpha (layer); + pixel_region_init (&srcPR, GIMP_DRAWABLE(layer)->tiles, 0, 0, GIMP_DRAWABLE(layer)->width, GIMP_DRAWABLE(layer)->height, FALSE); + pixel_region_init (&destPR, new_tiles, 0, 0, GIMP_DRAWABLE(layer)->width, GIMP_DRAWABLE(layer)->height, TRUE); + for (pr = pixel_regions_register (2, &srcPR, &destPR); pr != NULL; pr = pixel_regions_process (pr)) + { + src = srcPR.data; + dest = destPR.data; + for (row = 0; row < srcPR.h; row++) + { + for (col = 0; col < srcPR.w; col++) + { + int dmval = + DM[(col+offsetx+srcPR.x)&DM_WIDTHMASK] + [(row+offsety+srcPR.y)&DM_HEIGHTMASK]; + + /* get pixel value and index into the cache */ + pixel = src[GRAY_PIX]; + cachep = &histogram[pixel]; + /* If we have not seen this color before, find nearest colormap entry */ + /* and update the cache */ + if (*cachep == 0) + fill_inverse_cmap_gray (quantobj, histogram, pixel); + + color = &quantobj->cmap[*cachep - 1]; + re = src[GRAY_PIX] - color->red; + re = (re * dmval * 2) / 63; + R = (CLAMP0255(color->red + re)); + + cachep = &histogram[R]; + /* If we have not seen this color before, find nearest + colormap entry and update the cache */ + if (*cachep == 0) + fill_inverse_cmap_gray (quantobj, histogram, R); + + if (has_alpha) + { + if ((dest[ALPHA_I_PIX] = + ((alpha_dither ? + ((src[ALPHA_G_PIX] << 6) > (255 * dmval)) : + (src[ALPHA_G_PIX] > 127) + ) ? 255 : 0))) + index_used_count[dest[INDEXED_PIX] = *cachep - 1]++; + } + else + { + /* Now emit the colormap index for this cell, barfbarf */ + index_used_count[dest[INDEXED_PIX] = *cachep - 1]++; + } src += srcPR.bytes; dest += destPR.bytes; @@ -3098,6 +3227,112 @@ median_cut_pass2_no_dither_rgb (QuantizeObj *quantobj, } } +static void +median_cut_pass2_fixed_dither_rgb (QuantizeObj *quantobj, + Layer *layer, + TileManager *new_tiles) +{ + PixelRegion srcPR, destPR; + Histogram histogram = quantobj->histogram; + ColorFreq* cachep; + Color* color; + unsigned char *src, *dest; + int R, G, B; + int row, col; + int has_alpha; + int re, ge, be; + void* pr; + int red_pix = RED_PIX; + int green_pix = GREEN_PIX; + int blue_pix = BLUE_PIX; + int alpha_pix = ALPHA_PIX; + int alpha_dither = quantobj->want_alpha_dither; + int offsetx, offsety; + unsigned long* index_used_count = quantobj->index_used_count; + + drawable_offsets (GIMP_DRAWABLE(layer), &offsetx, &offsety); + + /* In the case of web/mono palettes, we actually force + * grayscale drawables through the rgb pass2 functions + */ + if (drawable_gray (GIMP_DRAWABLE(layer))) + red_pix = green_pix = blue_pix = GRAY_PIX; + + has_alpha = layer_has_alpha (layer); + pixel_region_init (&srcPR, GIMP_DRAWABLE(layer)->tiles, 0, 0, GIMP_DRAWABLE(layer)->width, GIMP_DRAWABLE(layer)->height, FALSE); + pixel_region_init (&destPR, new_tiles, 0, 0, GIMP_DRAWABLE(layer)->width, GIMP_DRAWABLE(layer)->height, TRUE); + for (pr = pixel_regions_register (2, &srcPR, &destPR); pr != NULL; pr = pixel_regions_process (pr)) + { + src = srcPR.data; + dest = destPR.data; + for (row = 0; row < srcPR.h; row++) + { + for (col = 0; col < srcPR.w; col++) + { + int dmval = + DM[(col+offsetx+srcPR.x)&DM_WIDTHMASK] + [(row+offsety+srcPR.y)&DM_HEIGHTMASK]; + + if (has_alpha) + { + if ((dest[ALPHA_I_PIX] = + (alpha_dither ? + ((src[alpha_pix] << 6) > (255 * dmval)) : + (src[alpha_pix] > 127) + ) ? 255 : 0) + == 0) + { + goto next_pixel; + } + } + + /* get pixel value and index into the cache */ + R = (src[red_pix]) >> R_SHIFT; + G = (src[green_pix]) >> G_SHIFT; + B = (src[blue_pix]) >> B_SHIFT; + cachep = &histogram[R*MR + G*MG + B]; + /* If we have not seen this color before, find nearest + colormap entry and update the cache */ + if (*cachep == 0) + fill_inverse_cmap_rgb (quantobj, histogram, R, G, B); + + /* Get the error and modulate it between 0x and 2x according + to the fixed dither matrix, then add it back to the 0x colour + and look up the new histogram entry. To do better fixed + dithering, I believe that we need to be able to find the + closest colour match on the 'other side' of the desired colour, + which is not information which we have cheap access to. */ + color = &quantobj->cmap[*cachep - 1]; + re = src[red_pix] - color->red; + ge = src[green_pix] - color->green; + be = src[blue_pix] - color->blue; + + re = (re * dmval * 2) / 63; + ge = (ge * dmval * 2) / 63; + be = (be * dmval * 2) / 63; + + R = (CLAMP0255(color->red + re)) >> R_SHIFT; + G = (CLAMP0255(color->green + ge)) >> G_SHIFT; + B = (CLAMP0255(color->blue + be)) >> B_SHIFT; + + cachep = &histogram[R*MR + G*MG + B]; + /* If we have not seen this color before, find nearest + colormap entry and update the cache */ + if (*cachep == 0) + fill_inverse_cmap_rgb (quantobj, histogram, R, G, B); + + /* Now emit the colormap index for this cell, barfbarf */ + index_used_count[dest[INDEXED_PIX] = *cachep - 1]++; + + next_pixel: + + src += srcPR.bytes; + dest += destPR.bytes; + } + } + } +} + static void median_cut_pass2_nodestruct_dither_rgb (QuantizeObj *quantobj, Layer *layer, @@ -3291,11 +3526,10 @@ median_cut_pass2_fs_dither_gray (QuantizeObj *quantobj, int offsetx, offsety; int alpha_dither = quantobj->want_alpha_dither; int width, height; + unsigned long* index_used_count = quantobj->index_used_count; drawable_offsets (GIMP_DRAWABLE(layer), &offsetx, &offsety); - zero_histogram_gray (histogram); - has_alpha = layer_has_alpha (layer); pixel_region_init (&srcPR, GIMP_DRAWABLE(layer)->tiles, 0, 0, GIMP_DRAWABLE(layer)->width, GIMP_DRAWABLE(layer)->height, FALSE); pixel_region_init (&destPR, new_tiles, 0, 0, GIMP_DRAWABLE(layer)->width, GIMP_DRAWABLE(layer)->height, TRUE); @@ -3364,15 +3598,42 @@ median_cut_pass2_fs_dither_gray (QuantizeObj *quantobj, if (*cachep == 0) fill_inverse_cmap_gray (quantobj, histogram, pixel); - index = *cachep - 1; - dest[INDEXED_PIX] = index; - if (has_alpha) - dest[ALPHA_I_PIX] = - (alpha_dither ? - ((src[ALPHA_G_PIX] << 6) > (255 * DM[(col+offsetx)&DM_WIDTHMASK][(row+offsety)&DM_HEIGHTMASK])) : - (src[ALPHA_G_PIX] > 127) - ) ? 255 : 0; + { + if (odd_row) + { + if ((dest[ALPHA_I_PIX] = + (alpha_dither ? + ((src[ALPHA_G_PIX] << 6) > (255 * DM[((width-col)+offsetx-1)&DM_WIDTHMASK][(row+offsety)&DM_HEIGHTMASK])) : + (src[ALPHA_G_PIX] > 127) + ) ? 255 : 0) + == 0) + { + pr--; + nr--; + *(nr - 1) = 0; + goto next_pixel; + } + } + else + { + if ((dest[ALPHA_I_PIX] = + (alpha_dither ? + ((src[ALPHA_G_PIX] << 6) > (255 * DM[(col+offsetx)&DM_WIDTHMASK][(row+offsety)&DM_HEIGHTMASK])) : + (src[ALPHA_G_PIX] > 127) + ) ? 255 : 0) + == 0) + { + pr++; + nr++; + *(nr + 1) = 0; + goto next_pixel; + } + } + } + + index = *cachep - 1; + index_used_count[dest[INDEXED_PIX] = index]++; color = &quantobj->cmap[index]; pixele = pixel - color->red; @@ -3392,6 +3653,8 @@ median_cut_pass2_fs_dither_gray (QuantizeObj *quantobj, *(nr+1) = fs_err4[pixele]; } + next_pixel: + dest += step_dest; src += step_src; } @@ -3421,6 +3684,15 @@ median_cut_pass2_rgb_init (QuantizeObj *quantobj) memset (quantobj->index_used_count, 0, 256 * sizeof(unsigned long)); } +static void +median_cut_pass2_gray_init (QuantizeObj *quantobj) +{ + zero_histogram_gray (quantobj->histogram); + + /* Mark all indices as currently unused */ + memset (quantobj->index_used_count, 0, 256 * sizeof(unsigned long)); +} + static void median_cut_pass2_fs_dither_rgb (QuantizeObj *quantobj, Layer *layer, @@ -3739,18 +4011,26 @@ initialize_median_cut (int type, quantobj->second_pass_init = median_cut_pass2_rgb_init; quantobj->second_pass = median_cut_pass2_fs_dither_rgb; break; + case FIXEDDITHER: + quantobj->second_pass_init = median_cut_pass2_rgb_init; + quantobj->second_pass = median_cut_pass2_fixed_dither_rgb; + break; } else switch (dither_type) { case NODITHER: - quantobj->second_pass_init = NULL; + quantobj->second_pass_init = median_cut_pass2_gray_init; quantobj->second_pass = median_cut_pass2_no_dither_gray; break; case FSDITHER: - quantobj->second_pass_init = NULL; + quantobj->second_pass_init = median_cut_pass2_gray_init; quantobj->second_pass = median_cut_pass2_fs_dither_gray; break; + case FIXEDDITHER: + quantobj->second_pass_init = median_cut_pass2_gray_init; + quantobj->second_pass = median_cut_pass2_fixed_dither_gray; + break; } break; case RGB: @@ -3785,6 +4065,10 @@ initialize_median_cut (int type, quantobj->second_pass_init = NULL; quantobj->second_pass = median_cut_pass2_nodestruct_dither_rgb; break; + case FIXEDDITHER: + quantobj->second_pass_init = median_cut_pass2_rgb_init; + quantobj->second_pass = median_cut_pass2_fixed_dither_rgb; + break; } break; } diff --git a/app/core/gimpimage-convert.c b/app/core/gimpimage-convert.c index 011e922a21..7f6fe24502 100644 --- a/app/core/gimpimage-convert.c +++ b/app/core/gimpimage-convert.c @@ -20,12 +20,16 @@ * TODO for Convert: * * Use palette of another open INDEXED image - * Different dither types (deterministic colour dithering) * * Do error-splitting trick for GREY->INDEXED */ /* + * 99/08/29 - Deterministic colour dithering to arbitrary palettes. + * Ideal for animations that are going to be delta-optimized or simply + * don't want to look 'busy' in static areas. Also a bunch of bugfixes + * and tweaks. [Adam] + * * 99/08/28 - Deterministic alpha dithering over layers, reduced bleeding * of transparent values into opaque values, added optional stage to * remove duplicate or unused colour entries from final colourmap. [Adam] @@ -101,6 +105,7 @@ #define NODITHER 0 #define FSDITHER 1 #define NODESTRUCTDITHER 2 +#define FIXEDDITHER 3 #define PRECISION_R 6 #define PRECISION_G 6 @@ -370,7 +375,9 @@ typedef struct GtkWidget* shell; GtkWidget* custom_frame; GimpImage* gimage; - int dither; /* flag */ + int nodither_flag; + int fsdither_flag; + int fixeddither_flag; int alphadither; /* flag */ int remdups; /* flag */ int num_cols; @@ -388,7 +395,6 @@ static gint indexed_delete_callback (GtkWidget *, GdkEvent *, gpointer); static void indexed_num_cols_update (GtkWidget *, gpointer); static void indexed_radio_update (GtkWidget *, gpointer); static void frame_sensitivity_update (GtkWidget *, gpointer); -static void indexed_dither_update (GtkWidget *, gpointer); static void indexed_alphadither_update (GtkWidget *, gpointer); static void indexed_remdups_update (GtkWidget *, gpointer); @@ -421,7 +427,9 @@ PaletteEntriesP theCustomPalette = NULL; /* Defaults */ static int snum_cols = 256; -static gboolean sdither = TRUE; +static gboolean sfsdither_flag = TRUE; +static gboolean snodither_flag = FALSE; +static gboolean sfixeddither_flag = FALSE; static gboolean smakepal_flag = TRUE; static gboolean salphadither_flag = FALSE; static gboolean sremdups_flag = TRUE; @@ -472,7 +480,9 @@ convert_to_indexed (GimpImage *gimage) dialog->custom_frame = NULL; dialog->num_cols = snum_cols; - dialog->dither = sdither; + dialog->nodither_flag = snodither_flag; + dialog->fsdither_flag = sfsdither_flag; + dialog->fixeddither_flag = sfixeddither_flag; dialog->alphadither = salphadither_flag; dialog->remdups = sremdups_flag; dialog->makepal_flag = smakepal_flag; @@ -505,7 +515,7 @@ convert_to_indexed (GimpImage *gimage) gtk_container_set_border_width (GTK_CONTAINER (vbox), 2); gtk_box_pack_start (GTK_BOX (vbox), hbox, FALSE, FALSE, 0); - toggle = gtk_radio_button_new_with_label (group, _("Generate optimal palette: ")); + toggle = gtk_radio_button_new_with_label (NULL, _("Generate optimal palette: ")); group = gtk_radio_button_group (GTK_RADIO_BUTTON (toggle)); gtk_box_pack_start (GTK_BOX (hbox), toggle, TRUE, TRUE, 0); gtk_signal_connect (GTK_OBJECT (toggle), "toggled", @@ -637,21 +647,60 @@ convert_to_indexed (GimpImage *gimage) gtk_container_add (GTK_CONTAINER (frame), vbox); gtk_widget_show(vbox); - /* The dither toggle */ + /* The dither radio buttons */ hbox = gtk_hbox_new (FALSE, 1); { gtk_box_pack_start (GTK_BOX (vbox), hbox, FALSE, FALSE, 0); - toggle = gtk_check_button_new_with_label (_("Enable Floyd-Steinberg dithering")); - gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (toggle), dialog->dither); - gtk_box_pack_start (GTK_BOX (hbox), toggle, TRUE, FALSE, 0); + toggle = gtk_radio_button_new_with_label (NULL, _("No colour dithering")); + group = gtk_radio_button_group (GTK_RADIO_BUTTON (toggle)); + + gtk_box_pack_start (GTK_BOX (hbox), toggle, FALSE, FALSE, 0); + gtk_signal_connect (GTK_OBJECT (toggle), "toggled", - (GtkSignalFunc) indexed_dither_update, - dialog); - gtk_widget_show (label); + (GtkSignalFunc) indexed_radio_update, + &(dialog->nodither_flag)); + gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (toggle), dialog->nodither_flag); + gtk_widget_show (toggle); } gtk_widget_show (hbox); + hbox = gtk_hbox_new (FALSE, 1); + { + gtk_box_pack_start (GTK_BOX (vbox), hbox, FALSE, FALSE, 0); + + toggle = gtk_radio_button_new_with_label (group, _("Positioned colour dithering")); + group = gtk_radio_button_group (GTK_RADIO_BUTTON (toggle)); + + gtk_box_pack_start (GTK_BOX (hbox), toggle, FALSE, FALSE, 0); + + gtk_signal_connect (GTK_OBJECT (toggle), "toggled", + (GtkSignalFunc) indexed_radio_update, + &(dialog->fixeddither_flag)); + gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (toggle), dialog->fixeddither_flag); + + gtk_widget_show (toggle); + } + gtk_widget_show (hbox); + hbox = gtk_hbox_new (FALSE, 1); + { + gtk_box_pack_start (GTK_BOX (vbox), hbox, FALSE, FALSE, 0); + + toggle = gtk_radio_button_new_with_label (group, _("Floyd-Steinberg colour dithering")); + group = gtk_radio_button_group (GTK_RADIO_BUTTON (toggle)); + + gtk_box_pack_start (GTK_BOX (hbox), toggle, FALSE, FALSE, 0); + + gtk_signal_connect (GTK_OBJECT (toggle), "toggled", + (GtkSignalFunc) indexed_radio_update, + &(dialog->fsdither_flag)); + gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (toggle), dialog->fsdither_flag); + + gtk_widget_show (toggle); + } + gtk_widget_show (hbox); + + /* The alpha-dither toggle */ hbox = gtk_hbox_new (FALSE, 1); @@ -660,7 +709,7 @@ convert_to_indexed (GimpImage *gimage) toggle = gtk_check_button_new_with_label (_("Enable dithering of transparency")); gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (toggle), dialog->alphadither); - gtk_box_pack_start (GTK_BOX (hbox), toggle, TRUE, FALSE, 0); + gtk_box_pack_start (GTK_BOX (hbox), toggle, FALSE, FALSE, 0); gtk_signal_connect (GTK_OBJECT (toggle), "toggled", (GtkSignalFunc) indexed_alphadither_update, dialog); @@ -837,6 +886,7 @@ indexed_ok_callback (GtkWidget *widget, { IndexedDialog *dialog; int palette_type; + int dither_type; dialog = (IndexedDialog *) client_data; @@ -849,16 +899,25 @@ indexed_ok_callback (GtkWidget *widget, if (dialog->makepal_flag) palette_type = MAKE_PALETTE; else palette_type = REUSE_PALETTE; + + if (dialog->nodither_flag) dither_type = NODITHER; + else + if (dialog->fsdither_flag) dither_type = FSDITHER; + else + dither_type = FIXEDDITHER; + /* Convert the image to indexed color */ convert_image2 (dialog->gimage, INDEXED, dialog->num_cols, - dialog->dither, dialog->alphadither, + dither_type, dialog->alphadither, dialog->remdups, palette_type); gdisplays_flush (); /* Save defaults for next time */ snum_cols = dialog->num_cols; - sdither = dialog->dither; + snodither_flag = dialog->nodither_flag; + sfsdither_flag = dialog->fsdither_flag; + sfixeddither_flag = dialog->fixeddither_flag; salphadither_flag = dialog->alphadither; sremdups_flag = dialog->remdups; smakepal_flag = dialog->makepal_flag; @@ -925,20 +984,6 @@ frame_sensitivity_update (GtkWidget *widget, gtk_widget_set_sensitive (dialog->custom_frame, dialog->custompal_flag); } -static void -indexed_dither_update (GtkWidget *w, - gpointer data) -{ - IndexedDialog *dialog; - - dialog = (IndexedDialog *) data; - - if (GTK_TOGGLE_BUTTON (w)->active) - dialog->dither = TRUE; - else - dialog->dither = FALSE; -} - static void indexed_alphadither_update (GtkWidget *w, gpointer data) @@ -1204,9 +1249,9 @@ convert_image2 (GImage *gimage, /* don't dither if the input is grayscale and we are simply mapping every color */ if (old_type == GRAY && num_cols == 256 && palette_type == MAKE_PALETTE) - dither = FALSE; + dither = NODITHER; - quantobj = initialize_median_cut (old_type, num_cols, dither ? FSDITHER : NODITHER, + quantobj = initialize_median_cut (old_type, num_cols, dither, palette_type, alpha_dither); if (palette_type == MAKE_PALETTE) @@ -2981,14 +3026,13 @@ median_cut_pass2_no_dither_gray (QuantizeObj *quantobj, int row, col; int pixel; int has_alpha; + unsigned long* index_used_count = quantobj->index_used_count; int alpha_dither = quantobj->want_alpha_dither; int offsetx, offsety; void *pr; drawable_offsets (GIMP_DRAWABLE(layer), &offsetx, &offsety); - zero_histogram_gray (histogram); - has_alpha = layer_has_alpha (layer); pixel_region_init (&srcPR, GIMP_DRAWABLE(layer)->tiles, 0, 0, GIMP_DRAWABLE(layer)->width, GIMP_DRAWABLE(layer)->height, FALSE); pixel_region_init (&destPR, new_tiles, 0, 0, GIMP_DRAWABLE(layer)->width, GIMP_DRAWABLE(layer)->height, TRUE); @@ -3007,14 +3051,99 @@ median_cut_pass2_no_dither_gray (QuantizeObj *quantobj, /* and update the cache */ if (*cachep == 0) fill_inverse_cmap_gray (quantobj, histogram, pixel); - /* Now emit the colormap index for this cell */ - dest[INDEXED_PIX] = *cachep - 1; + if (has_alpha) - dest[ALPHA_I_PIX] = - (alpha_dither ? - ((src[ALPHA_G_PIX] << 6) > (255 * DM[(col+offsetx+srcPR.x)&DM_WIDTHMASK][(row+offsety+srcPR.y)&DM_HEIGHTMASK])) : - (src[ALPHA_G_PIX] > 127) - ) ? 255 : 0; + { + if ((dest[ALPHA_I_PIX] = + ( + (alpha_dither ? + ((src[ALPHA_G_PIX] << 6) > (255 * DM[(col+offsetx+srcPR.x)&DM_WIDTHMASK][(row+offsety+srcPR.y)&DM_HEIGHTMASK])) : + (src[ALPHA_G_PIX] > 127) + ) ? 255 : 0))) + index_used_count[dest[INDEXED_PIX] = *cachep - 1]++; + } + else + { + /* Now emit the colormap index for this cell */ + index_used_count[dest[INDEXED_PIX] = *cachep - 1]++; + } + + src += srcPR.bytes; + dest += destPR.bytes; + } + } + } +} + +static void +median_cut_pass2_fixed_dither_gray (QuantizeObj *quantobj, + Layer *layer, + TileManager *new_tiles) +{ + PixelRegion srcPR, destPR; + Histogram histogram = quantobj->histogram; + ColorFreq* cachep; + Color* color; + unsigned char *src, *dest; + int row, col; + int pixel; + int re, R; + unsigned long* index_used_count = quantobj->index_used_count; + int has_alpha; + int alpha_dither = quantobj->want_alpha_dither; + int offsetx, offsety; + void *pr; + + drawable_offsets (GIMP_DRAWABLE(layer), &offsetx, &offsety); + + has_alpha = layer_has_alpha (layer); + pixel_region_init (&srcPR, GIMP_DRAWABLE(layer)->tiles, 0, 0, GIMP_DRAWABLE(layer)->width, GIMP_DRAWABLE(layer)->height, FALSE); + pixel_region_init (&destPR, new_tiles, 0, 0, GIMP_DRAWABLE(layer)->width, GIMP_DRAWABLE(layer)->height, TRUE); + for (pr = pixel_regions_register (2, &srcPR, &destPR); pr != NULL; pr = pixel_regions_process (pr)) + { + src = srcPR.data; + dest = destPR.data; + for (row = 0; row < srcPR.h; row++) + { + for (col = 0; col < srcPR.w; col++) + { + int dmval = + DM[(col+offsetx+srcPR.x)&DM_WIDTHMASK] + [(row+offsety+srcPR.y)&DM_HEIGHTMASK]; + + /* get pixel value and index into the cache */ + pixel = src[GRAY_PIX]; + cachep = &histogram[pixel]; + /* If we have not seen this color before, find nearest colormap entry */ + /* and update the cache */ + if (*cachep == 0) + fill_inverse_cmap_gray (quantobj, histogram, pixel); + + color = &quantobj->cmap[*cachep - 1]; + re = src[GRAY_PIX] - color->red; + re = (re * dmval * 2) / 63; + R = (CLAMP0255(color->red + re)); + + cachep = &histogram[R]; + /* If we have not seen this color before, find nearest + colormap entry and update the cache */ + if (*cachep == 0) + fill_inverse_cmap_gray (quantobj, histogram, R); + + if (has_alpha) + { + if ((dest[ALPHA_I_PIX] = + ((alpha_dither ? + ((src[ALPHA_G_PIX] << 6) > (255 * dmval)) : + (src[ALPHA_G_PIX] > 127) + ) ? 255 : 0))) + index_used_count[dest[INDEXED_PIX] = *cachep - 1]++; + } + else + { + /* Now emit the colormap index for this cell, barfbarf */ + index_used_count[dest[INDEXED_PIX] = *cachep - 1]++; + } src += srcPR.bytes; dest += destPR.bytes; @@ -3098,6 +3227,112 @@ median_cut_pass2_no_dither_rgb (QuantizeObj *quantobj, } } +static void +median_cut_pass2_fixed_dither_rgb (QuantizeObj *quantobj, + Layer *layer, + TileManager *new_tiles) +{ + PixelRegion srcPR, destPR; + Histogram histogram = quantobj->histogram; + ColorFreq* cachep; + Color* color; + unsigned char *src, *dest; + int R, G, B; + int row, col; + int has_alpha; + int re, ge, be; + void* pr; + int red_pix = RED_PIX; + int green_pix = GREEN_PIX; + int blue_pix = BLUE_PIX; + int alpha_pix = ALPHA_PIX; + int alpha_dither = quantobj->want_alpha_dither; + int offsetx, offsety; + unsigned long* index_used_count = quantobj->index_used_count; + + drawable_offsets (GIMP_DRAWABLE(layer), &offsetx, &offsety); + + /* In the case of web/mono palettes, we actually force + * grayscale drawables through the rgb pass2 functions + */ + if (drawable_gray (GIMP_DRAWABLE(layer))) + red_pix = green_pix = blue_pix = GRAY_PIX; + + has_alpha = layer_has_alpha (layer); + pixel_region_init (&srcPR, GIMP_DRAWABLE(layer)->tiles, 0, 0, GIMP_DRAWABLE(layer)->width, GIMP_DRAWABLE(layer)->height, FALSE); + pixel_region_init (&destPR, new_tiles, 0, 0, GIMP_DRAWABLE(layer)->width, GIMP_DRAWABLE(layer)->height, TRUE); + for (pr = pixel_regions_register (2, &srcPR, &destPR); pr != NULL; pr = pixel_regions_process (pr)) + { + src = srcPR.data; + dest = destPR.data; + for (row = 0; row < srcPR.h; row++) + { + for (col = 0; col < srcPR.w; col++) + { + int dmval = + DM[(col+offsetx+srcPR.x)&DM_WIDTHMASK] + [(row+offsety+srcPR.y)&DM_HEIGHTMASK]; + + if (has_alpha) + { + if ((dest[ALPHA_I_PIX] = + (alpha_dither ? + ((src[alpha_pix] << 6) > (255 * dmval)) : + (src[alpha_pix] > 127) + ) ? 255 : 0) + == 0) + { + goto next_pixel; + } + } + + /* get pixel value and index into the cache */ + R = (src[red_pix]) >> R_SHIFT; + G = (src[green_pix]) >> G_SHIFT; + B = (src[blue_pix]) >> B_SHIFT; + cachep = &histogram[R*MR + G*MG + B]; + /* If we have not seen this color before, find nearest + colormap entry and update the cache */ + if (*cachep == 0) + fill_inverse_cmap_rgb (quantobj, histogram, R, G, B); + + /* Get the error and modulate it between 0x and 2x according + to the fixed dither matrix, then add it back to the 0x colour + and look up the new histogram entry. To do better fixed + dithering, I believe that we need to be able to find the + closest colour match on the 'other side' of the desired colour, + which is not information which we have cheap access to. */ + color = &quantobj->cmap[*cachep - 1]; + re = src[red_pix] - color->red; + ge = src[green_pix] - color->green; + be = src[blue_pix] - color->blue; + + re = (re * dmval * 2) / 63; + ge = (ge * dmval * 2) / 63; + be = (be * dmval * 2) / 63; + + R = (CLAMP0255(color->red + re)) >> R_SHIFT; + G = (CLAMP0255(color->green + ge)) >> G_SHIFT; + B = (CLAMP0255(color->blue + be)) >> B_SHIFT; + + cachep = &histogram[R*MR + G*MG + B]; + /* If we have not seen this color before, find nearest + colormap entry and update the cache */ + if (*cachep == 0) + fill_inverse_cmap_rgb (quantobj, histogram, R, G, B); + + /* Now emit the colormap index for this cell, barfbarf */ + index_used_count[dest[INDEXED_PIX] = *cachep - 1]++; + + next_pixel: + + src += srcPR.bytes; + dest += destPR.bytes; + } + } + } +} + static void median_cut_pass2_nodestruct_dither_rgb (QuantizeObj *quantobj, Layer *layer, @@ -3291,11 +3526,10 @@ median_cut_pass2_fs_dither_gray (QuantizeObj *quantobj, int offsetx, offsety; int alpha_dither = quantobj->want_alpha_dither; int width, height; + unsigned long* index_used_count = quantobj->index_used_count; drawable_offsets (GIMP_DRAWABLE(layer), &offsetx, &offsety); - zero_histogram_gray (histogram); - has_alpha = layer_has_alpha (layer); pixel_region_init (&srcPR, GIMP_DRAWABLE(layer)->tiles, 0, 0, GIMP_DRAWABLE(layer)->width, GIMP_DRAWABLE(layer)->height, FALSE); pixel_region_init (&destPR, new_tiles, 0, 0, GIMP_DRAWABLE(layer)->width, GIMP_DRAWABLE(layer)->height, TRUE); @@ -3364,15 +3598,42 @@ median_cut_pass2_fs_dither_gray (QuantizeObj *quantobj, if (*cachep == 0) fill_inverse_cmap_gray (quantobj, histogram, pixel); - index = *cachep - 1; - dest[INDEXED_PIX] = index; - if (has_alpha) - dest[ALPHA_I_PIX] = - (alpha_dither ? - ((src[ALPHA_G_PIX] << 6) > (255 * DM[(col+offsetx)&DM_WIDTHMASK][(row+offsety)&DM_HEIGHTMASK])) : - (src[ALPHA_G_PIX] > 127) - ) ? 255 : 0; + { + if (odd_row) + { + if ((dest[ALPHA_I_PIX] = + (alpha_dither ? + ((src[ALPHA_G_PIX] << 6) > (255 * DM[((width-col)+offsetx-1)&DM_WIDTHMASK][(row+offsety)&DM_HEIGHTMASK])) : + (src[ALPHA_G_PIX] > 127) + ) ? 255 : 0) + == 0) + { + pr--; + nr--; + *(nr - 1) = 0; + goto next_pixel; + } + } + else + { + if ((dest[ALPHA_I_PIX] = + (alpha_dither ? + ((src[ALPHA_G_PIX] << 6) > (255 * DM[(col+offsetx)&DM_WIDTHMASK][(row+offsety)&DM_HEIGHTMASK])) : + (src[ALPHA_G_PIX] > 127) + ) ? 255 : 0) + == 0) + { + pr++; + nr++; + *(nr + 1) = 0; + goto next_pixel; + } + } + } + + index = *cachep - 1; + index_used_count[dest[INDEXED_PIX] = index]++; color = &quantobj->cmap[index]; pixele = pixel - color->red; @@ -3392,6 +3653,8 @@ median_cut_pass2_fs_dither_gray (QuantizeObj *quantobj, *(nr+1) = fs_err4[pixele]; } + next_pixel: + dest += step_dest; src += step_src; } @@ -3421,6 +3684,15 @@ median_cut_pass2_rgb_init (QuantizeObj *quantobj) memset (quantobj->index_used_count, 0, 256 * sizeof(unsigned long)); } +static void +median_cut_pass2_gray_init (QuantizeObj *quantobj) +{ + zero_histogram_gray (quantobj->histogram); + + /* Mark all indices as currently unused */ + memset (quantobj->index_used_count, 0, 256 * sizeof(unsigned long)); +} + static void median_cut_pass2_fs_dither_rgb (QuantizeObj *quantobj, Layer *layer, @@ -3739,18 +4011,26 @@ initialize_median_cut (int type, quantobj->second_pass_init = median_cut_pass2_rgb_init; quantobj->second_pass = median_cut_pass2_fs_dither_rgb; break; + case FIXEDDITHER: + quantobj->second_pass_init = median_cut_pass2_rgb_init; + quantobj->second_pass = median_cut_pass2_fixed_dither_rgb; + break; } else switch (dither_type) { case NODITHER: - quantobj->second_pass_init = NULL; + quantobj->second_pass_init = median_cut_pass2_gray_init; quantobj->second_pass = median_cut_pass2_no_dither_gray; break; case FSDITHER: - quantobj->second_pass_init = NULL; + quantobj->second_pass_init = median_cut_pass2_gray_init; quantobj->second_pass = median_cut_pass2_fs_dither_gray; break; + case FIXEDDITHER: + quantobj->second_pass_init = median_cut_pass2_gray_init; + quantobj->second_pass = median_cut_pass2_fixed_dither_gray; + break; } break; case RGB: @@ -3785,6 +4065,10 @@ initialize_median_cut (int type, quantobj->second_pass_init = NULL; quantobj->second_pass = median_cut_pass2_nodestruct_dither_rgb; break; + case FIXEDDITHER: + quantobj->second_pass_init = median_cut_pass2_rgb_init; + quantobj->second_pass = median_cut_pass2_fixed_dither_rgb; + break; } break; } diff --git a/app/gimpimage-convert.c b/app/gimpimage-convert.c index 011e922a21..7f6fe24502 100644 --- a/app/gimpimage-convert.c +++ b/app/gimpimage-convert.c @@ -20,12 +20,16 @@ * TODO for Convert: * * Use palette of another open INDEXED image - * Different dither types (deterministic colour dithering) * * Do error-splitting trick for GREY->INDEXED */ /* + * 99/08/29 - Deterministic colour dithering to arbitrary palettes. + * Ideal for animations that are going to be delta-optimized or simply + * don't want to look 'busy' in static areas. Also a bunch of bugfixes + * and tweaks. [Adam] + * * 99/08/28 - Deterministic alpha dithering over layers, reduced bleeding * of transparent values into opaque values, added optional stage to * remove duplicate or unused colour entries from final colourmap. [Adam] @@ -101,6 +105,7 @@ #define NODITHER 0 #define FSDITHER 1 #define NODESTRUCTDITHER 2 +#define FIXEDDITHER 3 #define PRECISION_R 6 #define PRECISION_G 6 @@ -370,7 +375,9 @@ typedef struct GtkWidget* shell; GtkWidget* custom_frame; GimpImage* gimage; - int dither; /* flag */ + int nodither_flag; + int fsdither_flag; + int fixeddither_flag; int alphadither; /* flag */ int remdups; /* flag */ int num_cols; @@ -388,7 +395,6 @@ static gint indexed_delete_callback (GtkWidget *, GdkEvent *, gpointer); static void indexed_num_cols_update (GtkWidget *, gpointer); static void indexed_radio_update (GtkWidget *, gpointer); static void frame_sensitivity_update (GtkWidget *, gpointer); -static void indexed_dither_update (GtkWidget *, gpointer); static void indexed_alphadither_update (GtkWidget *, gpointer); static void indexed_remdups_update (GtkWidget *, gpointer); @@ -421,7 +427,9 @@ PaletteEntriesP theCustomPalette = NULL; /* Defaults */ static int snum_cols = 256; -static gboolean sdither = TRUE; +static gboolean sfsdither_flag = TRUE; +static gboolean snodither_flag = FALSE; +static gboolean sfixeddither_flag = FALSE; static gboolean smakepal_flag = TRUE; static gboolean salphadither_flag = FALSE; static gboolean sremdups_flag = TRUE; @@ -472,7 +480,9 @@ convert_to_indexed (GimpImage *gimage) dialog->custom_frame = NULL; dialog->num_cols = snum_cols; - dialog->dither = sdither; + dialog->nodither_flag = snodither_flag; + dialog->fsdither_flag = sfsdither_flag; + dialog->fixeddither_flag = sfixeddither_flag; dialog->alphadither = salphadither_flag; dialog->remdups = sremdups_flag; dialog->makepal_flag = smakepal_flag; @@ -505,7 +515,7 @@ convert_to_indexed (GimpImage *gimage) gtk_container_set_border_width (GTK_CONTAINER (vbox), 2); gtk_box_pack_start (GTK_BOX (vbox), hbox, FALSE, FALSE, 0); - toggle = gtk_radio_button_new_with_label (group, _("Generate optimal palette: ")); + toggle = gtk_radio_button_new_with_label (NULL, _("Generate optimal palette: ")); group = gtk_radio_button_group (GTK_RADIO_BUTTON (toggle)); gtk_box_pack_start (GTK_BOX (hbox), toggle, TRUE, TRUE, 0); gtk_signal_connect (GTK_OBJECT (toggle), "toggled", @@ -637,21 +647,60 @@ convert_to_indexed (GimpImage *gimage) gtk_container_add (GTK_CONTAINER (frame), vbox); gtk_widget_show(vbox); - /* The dither toggle */ + /* The dither radio buttons */ hbox = gtk_hbox_new (FALSE, 1); { gtk_box_pack_start (GTK_BOX (vbox), hbox, FALSE, FALSE, 0); - toggle = gtk_check_button_new_with_label (_("Enable Floyd-Steinberg dithering")); - gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (toggle), dialog->dither); - gtk_box_pack_start (GTK_BOX (hbox), toggle, TRUE, FALSE, 0); + toggle = gtk_radio_button_new_with_label (NULL, _("No colour dithering")); + group = gtk_radio_button_group (GTK_RADIO_BUTTON (toggle)); + + gtk_box_pack_start (GTK_BOX (hbox), toggle, FALSE, FALSE, 0); + gtk_signal_connect (GTK_OBJECT (toggle), "toggled", - (GtkSignalFunc) indexed_dither_update, - dialog); - gtk_widget_show (label); + (GtkSignalFunc) indexed_radio_update, + &(dialog->nodither_flag)); + gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (toggle), dialog->nodither_flag); + gtk_widget_show (toggle); } gtk_widget_show (hbox); + hbox = gtk_hbox_new (FALSE, 1); + { + gtk_box_pack_start (GTK_BOX (vbox), hbox, FALSE, FALSE, 0); + + toggle = gtk_radio_button_new_with_label (group, _("Positioned colour dithering")); + group = gtk_radio_button_group (GTK_RADIO_BUTTON (toggle)); + + gtk_box_pack_start (GTK_BOX (hbox), toggle, FALSE, FALSE, 0); + + gtk_signal_connect (GTK_OBJECT (toggle), "toggled", + (GtkSignalFunc) indexed_radio_update, + &(dialog->fixeddither_flag)); + gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (toggle), dialog->fixeddither_flag); + + gtk_widget_show (toggle); + } + gtk_widget_show (hbox); + hbox = gtk_hbox_new (FALSE, 1); + { + gtk_box_pack_start (GTK_BOX (vbox), hbox, FALSE, FALSE, 0); + + toggle = gtk_radio_button_new_with_label (group, _("Floyd-Steinberg colour dithering")); + group = gtk_radio_button_group (GTK_RADIO_BUTTON (toggle)); + + gtk_box_pack_start (GTK_BOX (hbox), toggle, FALSE, FALSE, 0); + + gtk_signal_connect (GTK_OBJECT (toggle), "toggled", + (GtkSignalFunc) indexed_radio_update, + &(dialog->fsdither_flag)); + gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (toggle), dialog->fsdither_flag); + + gtk_widget_show (toggle); + } + gtk_widget_show (hbox); + + /* The alpha-dither toggle */ hbox = gtk_hbox_new (FALSE, 1); @@ -660,7 +709,7 @@ convert_to_indexed (GimpImage *gimage) toggle = gtk_check_button_new_with_label (_("Enable dithering of transparency")); gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (toggle), dialog->alphadither); - gtk_box_pack_start (GTK_BOX (hbox), toggle, TRUE, FALSE, 0); + gtk_box_pack_start (GTK_BOX (hbox), toggle, FALSE, FALSE, 0); gtk_signal_connect (GTK_OBJECT (toggle), "toggled", (GtkSignalFunc) indexed_alphadither_update, dialog); @@ -837,6 +886,7 @@ indexed_ok_callback (GtkWidget *widget, { IndexedDialog *dialog; int palette_type; + int dither_type; dialog = (IndexedDialog *) client_data; @@ -849,16 +899,25 @@ indexed_ok_callback (GtkWidget *widget, if (dialog->makepal_flag) palette_type = MAKE_PALETTE; else palette_type = REUSE_PALETTE; + + if (dialog->nodither_flag) dither_type = NODITHER; + else + if (dialog->fsdither_flag) dither_type = FSDITHER; + else + dither_type = FIXEDDITHER; + /* Convert the image to indexed color */ convert_image2 (dialog->gimage, INDEXED, dialog->num_cols, - dialog->dither, dialog->alphadither, + dither_type, dialog->alphadither, dialog->remdups, palette_type); gdisplays_flush (); /* Save defaults for next time */ snum_cols = dialog->num_cols; - sdither = dialog->dither; + snodither_flag = dialog->nodither_flag; + sfsdither_flag = dialog->fsdither_flag; + sfixeddither_flag = dialog->fixeddither_flag; salphadither_flag = dialog->alphadither; sremdups_flag = dialog->remdups; smakepal_flag = dialog->makepal_flag; @@ -925,20 +984,6 @@ frame_sensitivity_update (GtkWidget *widget, gtk_widget_set_sensitive (dialog->custom_frame, dialog->custompal_flag); } -static void -indexed_dither_update (GtkWidget *w, - gpointer data) -{ - IndexedDialog *dialog; - - dialog = (IndexedDialog *) data; - - if (GTK_TOGGLE_BUTTON (w)->active) - dialog->dither = TRUE; - else - dialog->dither = FALSE; -} - static void indexed_alphadither_update (GtkWidget *w, gpointer data) @@ -1204,9 +1249,9 @@ convert_image2 (GImage *gimage, /* don't dither if the input is grayscale and we are simply mapping every color */ if (old_type == GRAY && num_cols == 256 && palette_type == MAKE_PALETTE) - dither = FALSE; + dither = NODITHER; - quantobj = initialize_median_cut (old_type, num_cols, dither ? FSDITHER : NODITHER, + quantobj = initialize_median_cut (old_type, num_cols, dither, palette_type, alpha_dither); if (palette_type == MAKE_PALETTE) @@ -2981,14 +3026,13 @@ median_cut_pass2_no_dither_gray (QuantizeObj *quantobj, int row, col; int pixel; int has_alpha; + unsigned long* index_used_count = quantobj->index_used_count; int alpha_dither = quantobj->want_alpha_dither; int offsetx, offsety; void *pr; drawable_offsets (GIMP_DRAWABLE(layer), &offsetx, &offsety); - zero_histogram_gray (histogram); - has_alpha = layer_has_alpha (layer); pixel_region_init (&srcPR, GIMP_DRAWABLE(layer)->tiles, 0, 0, GIMP_DRAWABLE(layer)->width, GIMP_DRAWABLE(layer)->height, FALSE); pixel_region_init (&destPR, new_tiles, 0, 0, GIMP_DRAWABLE(layer)->width, GIMP_DRAWABLE(layer)->height, TRUE); @@ -3007,14 +3051,99 @@ median_cut_pass2_no_dither_gray (QuantizeObj *quantobj, /* and update the cache */ if (*cachep == 0) fill_inverse_cmap_gray (quantobj, histogram, pixel); - /* Now emit the colormap index for this cell */ - dest[INDEXED_PIX] = *cachep - 1; + if (has_alpha) - dest[ALPHA_I_PIX] = - (alpha_dither ? - ((src[ALPHA_G_PIX] << 6) > (255 * DM[(col+offsetx+srcPR.x)&DM_WIDTHMASK][(row+offsety+srcPR.y)&DM_HEIGHTMASK])) : - (src[ALPHA_G_PIX] > 127) - ) ? 255 : 0; + { + if ((dest[ALPHA_I_PIX] = + ( + (alpha_dither ? + ((src[ALPHA_G_PIX] << 6) > (255 * DM[(col+offsetx+srcPR.x)&DM_WIDTHMASK][(row+offsety+srcPR.y)&DM_HEIGHTMASK])) : + (src[ALPHA_G_PIX] > 127) + ) ? 255 : 0))) + index_used_count[dest[INDEXED_PIX] = *cachep - 1]++; + } + else + { + /* Now emit the colormap index for this cell */ + index_used_count[dest[INDEXED_PIX] = *cachep - 1]++; + } + + src += srcPR.bytes; + dest += destPR.bytes; + } + } + } +} + +static void +median_cut_pass2_fixed_dither_gray (QuantizeObj *quantobj, + Layer *layer, + TileManager *new_tiles) +{ + PixelRegion srcPR, destPR; + Histogram histogram = quantobj->histogram; + ColorFreq* cachep; + Color* color; + unsigned char *src, *dest; + int row, col; + int pixel; + int re, R; + unsigned long* index_used_count = quantobj->index_used_count; + int has_alpha; + int alpha_dither = quantobj->want_alpha_dither; + int offsetx, offsety; + void *pr; + + drawable_offsets (GIMP_DRAWABLE(layer), &offsetx, &offsety); + + has_alpha = layer_has_alpha (layer); + pixel_region_init (&srcPR, GIMP_DRAWABLE(layer)->tiles, 0, 0, GIMP_DRAWABLE(layer)->width, GIMP_DRAWABLE(layer)->height, FALSE); + pixel_region_init (&destPR, new_tiles, 0, 0, GIMP_DRAWABLE(layer)->width, GIMP_DRAWABLE(layer)->height, TRUE); + for (pr = pixel_regions_register (2, &srcPR, &destPR); pr != NULL; pr = pixel_regions_process (pr)) + { + src = srcPR.data; + dest = destPR.data; + for (row = 0; row < srcPR.h; row++) + { + for (col = 0; col < srcPR.w; col++) + { + int dmval = + DM[(col+offsetx+srcPR.x)&DM_WIDTHMASK] + [(row+offsety+srcPR.y)&DM_HEIGHTMASK]; + + /* get pixel value and index into the cache */ + pixel = src[GRAY_PIX]; + cachep = &histogram[pixel]; + /* If we have not seen this color before, find nearest colormap entry */ + /* and update the cache */ + if (*cachep == 0) + fill_inverse_cmap_gray (quantobj, histogram, pixel); + + color = &quantobj->cmap[*cachep - 1]; + re = src[GRAY_PIX] - color->red; + re = (re * dmval * 2) / 63; + R = (CLAMP0255(color->red + re)); + + cachep = &histogram[R]; + /* If we have not seen this color before, find nearest + colormap entry and update the cache */ + if (*cachep == 0) + fill_inverse_cmap_gray (quantobj, histogram, R); + + if (has_alpha) + { + if ((dest[ALPHA_I_PIX] = + ((alpha_dither ? + ((src[ALPHA_G_PIX] << 6) > (255 * dmval)) : + (src[ALPHA_G_PIX] > 127) + ) ? 255 : 0))) + index_used_count[dest[INDEXED_PIX] = *cachep - 1]++; + } + else + { + /* Now emit the colormap index for this cell, barfbarf */ + index_used_count[dest[INDEXED_PIX] = *cachep - 1]++; + } src += srcPR.bytes; dest += destPR.bytes; @@ -3098,6 +3227,112 @@ median_cut_pass2_no_dither_rgb (QuantizeObj *quantobj, } } +static void +median_cut_pass2_fixed_dither_rgb (QuantizeObj *quantobj, + Layer *layer, + TileManager *new_tiles) +{ + PixelRegion srcPR, destPR; + Histogram histogram = quantobj->histogram; + ColorFreq* cachep; + Color* color; + unsigned char *src, *dest; + int R, G, B; + int row, col; + int has_alpha; + int re, ge, be; + void* pr; + int red_pix = RED_PIX; + int green_pix = GREEN_PIX; + int blue_pix = BLUE_PIX; + int alpha_pix = ALPHA_PIX; + int alpha_dither = quantobj->want_alpha_dither; + int offsetx, offsety; + unsigned long* index_used_count = quantobj->index_used_count; + + drawable_offsets (GIMP_DRAWABLE(layer), &offsetx, &offsety); + + /* In the case of web/mono palettes, we actually force + * grayscale drawables through the rgb pass2 functions + */ + if (drawable_gray (GIMP_DRAWABLE(layer))) + red_pix = green_pix = blue_pix = GRAY_PIX; + + has_alpha = layer_has_alpha (layer); + pixel_region_init (&srcPR, GIMP_DRAWABLE(layer)->tiles, 0, 0, GIMP_DRAWABLE(layer)->width, GIMP_DRAWABLE(layer)->height, FALSE); + pixel_region_init (&destPR, new_tiles, 0, 0, GIMP_DRAWABLE(layer)->width, GIMP_DRAWABLE(layer)->height, TRUE); + for (pr = pixel_regions_register (2, &srcPR, &destPR); pr != NULL; pr = pixel_regions_process (pr)) + { + src = srcPR.data; + dest = destPR.data; + for (row = 0; row < srcPR.h; row++) + { + for (col = 0; col < srcPR.w; col++) + { + int dmval = + DM[(col+offsetx+srcPR.x)&DM_WIDTHMASK] + [(row+offsety+srcPR.y)&DM_HEIGHTMASK]; + + if (has_alpha) + { + if ((dest[ALPHA_I_PIX] = + (alpha_dither ? + ((src[alpha_pix] << 6) > (255 * dmval)) : + (src[alpha_pix] > 127) + ) ? 255 : 0) + == 0) + { + goto next_pixel; + } + } + + /* get pixel value and index into the cache */ + R = (src[red_pix]) >> R_SHIFT; + G = (src[green_pix]) >> G_SHIFT; + B = (src[blue_pix]) >> B_SHIFT; + cachep = &histogram[R*MR + G*MG + B]; + /* If we have not seen this color before, find nearest + colormap entry and update the cache */ + if (*cachep == 0) + fill_inverse_cmap_rgb (quantobj, histogram, R, G, B); + + /* Get the error and modulate it between 0x and 2x according + to the fixed dither matrix, then add it back to the 0x colour + and look up the new histogram entry. To do better fixed + dithering, I believe that we need to be able to find the + closest colour match on the 'other side' of the desired colour, + which is not information which we have cheap access to. */ + color = &quantobj->cmap[*cachep - 1]; + re = src[red_pix] - color->red; + ge = src[green_pix] - color->green; + be = src[blue_pix] - color->blue; + + re = (re * dmval * 2) / 63; + ge = (ge * dmval * 2) / 63; + be = (be * dmval * 2) / 63; + + R = (CLAMP0255(color->red + re)) >> R_SHIFT; + G = (CLAMP0255(color->green + ge)) >> G_SHIFT; + B = (CLAMP0255(color->blue + be)) >> B_SHIFT; + + cachep = &histogram[R*MR + G*MG + B]; + /* If we have not seen this color before, find nearest + colormap entry and update the cache */ + if (*cachep == 0) + fill_inverse_cmap_rgb (quantobj, histogram, R, G, B); + + /* Now emit the colormap index for this cell, barfbarf */ + index_used_count[dest[INDEXED_PIX] = *cachep - 1]++; + + next_pixel: + + src += srcPR.bytes; + dest += destPR.bytes; + } + } + } +} + static void median_cut_pass2_nodestruct_dither_rgb (QuantizeObj *quantobj, Layer *layer, @@ -3291,11 +3526,10 @@ median_cut_pass2_fs_dither_gray (QuantizeObj *quantobj, int offsetx, offsety; int alpha_dither = quantobj->want_alpha_dither; int width, height; + unsigned long* index_used_count = quantobj->index_used_count; drawable_offsets (GIMP_DRAWABLE(layer), &offsetx, &offsety); - zero_histogram_gray (histogram); - has_alpha = layer_has_alpha (layer); pixel_region_init (&srcPR, GIMP_DRAWABLE(layer)->tiles, 0, 0, GIMP_DRAWABLE(layer)->width, GIMP_DRAWABLE(layer)->height, FALSE); pixel_region_init (&destPR, new_tiles, 0, 0, GIMP_DRAWABLE(layer)->width, GIMP_DRAWABLE(layer)->height, TRUE); @@ -3364,15 +3598,42 @@ median_cut_pass2_fs_dither_gray (QuantizeObj *quantobj, if (*cachep == 0) fill_inverse_cmap_gray (quantobj, histogram, pixel); - index = *cachep - 1; - dest[INDEXED_PIX] = index; - if (has_alpha) - dest[ALPHA_I_PIX] = - (alpha_dither ? - ((src[ALPHA_G_PIX] << 6) > (255 * DM[(col+offsetx)&DM_WIDTHMASK][(row+offsety)&DM_HEIGHTMASK])) : - (src[ALPHA_G_PIX] > 127) - ) ? 255 : 0; + { + if (odd_row) + { + if ((dest[ALPHA_I_PIX] = + (alpha_dither ? + ((src[ALPHA_G_PIX] << 6) > (255 * DM[((width-col)+offsetx-1)&DM_WIDTHMASK][(row+offsety)&DM_HEIGHTMASK])) : + (src[ALPHA_G_PIX] > 127) + ) ? 255 : 0) + == 0) + { + pr--; + nr--; + *(nr - 1) = 0; + goto next_pixel; + } + } + else + { + if ((dest[ALPHA_I_PIX] = + (alpha_dither ? + ((src[ALPHA_G_PIX] << 6) > (255 * DM[(col+offsetx)&DM_WIDTHMASK][(row+offsety)&DM_HEIGHTMASK])) : + (src[ALPHA_G_PIX] > 127) + ) ? 255 : 0) + == 0) + { + pr++; + nr++; + *(nr + 1) = 0; + goto next_pixel; + } + } + } + + index = *cachep - 1; + index_used_count[dest[INDEXED_PIX] = index]++; color = &quantobj->cmap[index]; pixele = pixel - color->red; @@ -3392,6 +3653,8 @@ median_cut_pass2_fs_dither_gray (QuantizeObj *quantobj, *(nr+1) = fs_err4[pixele]; } + next_pixel: + dest += step_dest; src += step_src; } @@ -3421,6 +3684,15 @@ median_cut_pass2_rgb_init (QuantizeObj *quantobj) memset (quantobj->index_used_count, 0, 256 * sizeof(unsigned long)); } +static void +median_cut_pass2_gray_init (QuantizeObj *quantobj) +{ + zero_histogram_gray (quantobj->histogram); + + /* Mark all indices as currently unused */ + memset (quantobj->index_used_count, 0, 256 * sizeof(unsigned long)); +} + static void median_cut_pass2_fs_dither_rgb (QuantizeObj *quantobj, Layer *layer, @@ -3739,18 +4011,26 @@ initialize_median_cut (int type, quantobj->second_pass_init = median_cut_pass2_rgb_init; quantobj->second_pass = median_cut_pass2_fs_dither_rgb; break; + case FIXEDDITHER: + quantobj->second_pass_init = median_cut_pass2_rgb_init; + quantobj->second_pass = median_cut_pass2_fixed_dither_rgb; + break; } else switch (dither_type) { case NODITHER: - quantobj->second_pass_init = NULL; + quantobj->second_pass_init = median_cut_pass2_gray_init; quantobj->second_pass = median_cut_pass2_no_dither_gray; break; case FSDITHER: - quantobj->second_pass_init = NULL; + quantobj->second_pass_init = median_cut_pass2_gray_init; quantobj->second_pass = median_cut_pass2_fs_dither_gray; break; + case FIXEDDITHER: + quantobj->second_pass_init = median_cut_pass2_gray_init; + quantobj->second_pass = median_cut_pass2_fixed_dither_gray; + break; } break; case RGB: @@ -3785,6 +4065,10 @@ initialize_median_cut (int type, quantobj->second_pass_init = NULL; quantobj->second_pass = median_cut_pass2_nodestruct_dither_rgb; break; + case FIXEDDITHER: + quantobj->second_pass_init = median_cut_pass2_rgb_init; + quantobj->second_pass = median_cut_pass2_fixed_dither_rgb; + break; } break; }