/* The GIMP -- an image manipulation program * Copyright (C) 1995 Spencer Kimball and Peter Mattis * * newsprint plug-in * Copyright (C) 1997-1998 Austin Donnelly * * 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., 675 Mass Ave, Cambridge, MA 02139, USA. * * Portions of this plug-in are copyright 1991-1992 Adobe Systems * Incorporated. See the spot_PS*() functions for details. */ /* * version 0.52 * This version requires gtk-1.0.4 or above. * * This plug-in puts an image through a screen at a particular angle * and lines per inch, to arrive at a newspaper-style effect. * * Austin Donnelly * http://www.cl.cam.ac.uk/~and1000/newsprint/ * * Richard Mortier did the spot_round() function * with correct tonal balance. * * Tim Harris provided valuable feedback on * pre-press issues. * * * 0.52: 10 Jan 1999 * gtk_label_set() -> gtk_label_set_text() * 0.60: 18 Jun 2001 * fixed long-standing bug where newsprint() function in GREYA images * treated them as RGB (bpp rather than colour_bpp) to select * colourspace to use. Thanks to warner-gnome.bugzilla@lothar.com for * spotting this and providing the patch. Bug #52981. */ #include "config.h" #include #include #include #include #include #include #include "libgimp/stdplugins-intl.h" #ifdef RCSID static char rcsid[] = "$Id$"; #endif #define VERSION "v0.60" /* Some useful macros */ #ifdef DEBUG #define DEBUG_PRINT(x) printf x #else #define DEBUG_PRINT(x) #endif /* HACK so we compile with old gtks. Won't work on machines where the * size of an int is different from the size of a void*, eg Alphas */ #ifndef GINT_TO_POINTER #warning glib did not define GINT_TO_POINTER, assuming same size as int #define GINT_TO_POINTER(x) ((gpointer)(x)) #endif #ifndef GPOINTER_TO_INT #warning glib did not define GPOINTER_TO_INT, assuming same size as int #define GPOINTER_TO_INT(x) ((int)(x)) #endif /*#define TIMINGS*/ #ifdef TIMINGS #include #include #define TM_INIT() struct timeval tm_start, tm_end #define TM_START() gettimeofday(&tm_start, NULL) #define TM_END() \ do { \ gettimeofday(&tm_end, NULL); \ tm_end.tv_sec -= tm_start.tv_sec; \ tm_end.tv_usec -= tm_start.tv_usec; \ if (tm_end.tv_usec < 0) \ { \ tm_end.tv_usec += 1000000; \ tm_end.tv_sec -= 1; \ } \ printf("operation took %ld.%06ld\n", tm_end.tv_sec, tm_end.tv_usec); \ } while(0) #else #define TM_INIT() #define TM_START() #define TM_END() #endif #define TILE_CACHE_SIZE 16 #define SCALE_WIDTH 125 #define DEF_OVERSAMPLE 1 /* default for how much to oversample by */ #define SPOT_PREVIEW_SZ 16 #define PREVIEW_OVERSAMPLE 3 #define ISNEG(x) (((x) < 0)? 1 : 0) #define DEG2RAD(d) ((d) * G_PI / 180) #define VALID_BOOL(x) ((x) == TRUE || (x) == FALSE) #define CLAMPED_ADD(a, b) (((a)+(b) > 0xff)? 0xff : (a) + (b)) /* Bartlett window supersampling weight function. See table 4.1, page * 123 of Alan Watt and Mark Watt, Advanced Animation and Rendering * Techniques, theory and practice. Addison-Wesley, 1992. ISBN * 0-201-54412-1 */ #define BARTLETT(x,y) (((oversample/2)+1-ABS(x)) * ((oversample/2)+1-ABS(y))) #define WGT(x,y) wgt[((y+oversample/2)*oversample) + x+oversample/2] /* colourspaces we can separate to: */ #define CS_GREY 0 #define CS_RGB 1 #define CS_CMYK 2 #define CS_INTENSITY 3 #define NUM_CS 4 #define VALID_CS(x) ((x) >= 0 && (x) <= NUM_CS-1) /* Spot function related types and definitions */ typedef gdouble spotfn_t (gdouble x, gdouble y); /* forward declaration of the functions themselves */ static spotfn_t spot_round; static spotfn_t spot_line; static spotfn_t spot_diamond; static spotfn_t spot_PSsquare; static spotfn_t spot_PSdiamond; typedef struct { const gchar *name; /* function's name */ spotfn_t *fn; /* function itself */ guchar *thresh; /* cached threshold matrix */ gdouble prev_lvl[3]; /* intensities to preview */ guchar *prev_thresh; /* preview-sized threshold matrix */ gint balanced; /* TRUE if spot fn is already balanced */ } spot_info_t; /* This is all the info needed per spot function. Functions are refered to * by their index into this array. */ static spot_info_t spotfn_list[] = { #define SPOTFN_DOT 0 { N_("Round"), spot_round, NULL, { .22, .50, .90 }, NULL, FALSE }, { N_("Line"), spot_line, NULL, { .15, .50, .80 }, NULL, FALSE }, { N_("Diamond"), spot_diamond, NULL, { .15, .50, .80 }, NULL, TRUE }, { N_("PS Square (Euclidean Dot)"), spot_PSsquare, NULL, { .15, .50, .90 }, NULL, FALSE }, { N_("PS Diamond"), spot_PSdiamond, NULL, { .15, .50, .90 }, NULL, FALSE }, /* NULL-name terminates */ { NULL, NULL, NULL, { 0.0, 0.0, 0.0 }, NULL, FALSE } }; #define NUM_SPOTFN ((sizeof(spotfn_list) / sizeof(spot_info_t)) - 1) #define VALID_SPOTFN(x) ((x) >= 0 && (x) < NUM_SPOTFN) #define THRESH(x,y) (thresh[(y)*width + (x)]) #define THRESHn(n,x,y) ((thresh[n])[(y)*width + (x)]) /* Arguments to filter */ /* Some of these are here merely to save them across calls. They are * marked as "UI use". Everything else is a valid arg. */ typedef struct { /* resolution section: */ gint cell_width; /* screening section: */ gint colourspace; /* 0: RGB, 1: CMYK, 2: Intensity */ gint k_pullout; /* percentage of black to pull out */ /* grey screen (only used if greyscale drawable) */ gdouble gry_ang; gint gry_spotfn; /* red / cyan screen */ gdouble red_ang; gint red_spotfn; /* green / magenta screen */ gdouble grn_ang; gint grn_spotfn; /* blue / yellow screen */ gdouble blu_ang; gint blu_spotfn; /* anti-alias section */ gint oversample; /* 1 == no anti-aliasing, else small odd int */ } NewsprintValues; /* bits of state used by the UI, but not visible from the PDB */ typedef struct { gint input_spi; /* input samples per inch */ gdouble output_lpi; /* desired output lines per inch */ gint lock_channels; /* changes to one channel affect all */ } NewsprintUIValues; typedef struct { gint run; } NewsprintInterface; /* state for the preview widgets */ typedef struct { GtkWidget *widget; /* preview widget itself */ GtkWidget *label; /* the label below it */ } preview_st; /* state for the channel notebook pages */ typedef struct _channel_st channel_st; struct _channel_st { GtkWidget *vbox; /* vbox of this channel */ gint *spotfn_num; /* which spotfn the menu is controlling */ preview_st prev[3]; /* state for 3 preview widgets */ GtkObject *angle_adj; /* angle adjustment */ GtkWidget *option_menu; /* popup for spot function */ GtkWidget *menuitem[NUM_SPOTFN]; /* menuitems for each spot function */ GtkWidget *ch_menuitem; /* menuitem for the channel selector */ gint ch_menu_num; /* this channel's position in the selector */ channel_st *next; /* circular list of channels in locked group */ }; /* State associated with the configuration dialog and passed to its * callback functions */ typedef struct { GtkWidget *dlg; /* main dialog itself */ GtkWidget *pull_table; GtkObject *pull; /* black pullout percentage */ GtkObject *input_spi; GtkObject *output_lpi; GtkObject *cellsize; GtkWidget *vbox; /* container for screen info */ /* Notebook for the channels (one per colorspace) */ GtkWidget *channel_notebook[NUM_CS]; /* room for up to 4 channels per colourspace */ channel_st *chst[NUM_CS][4]; } NewsprintDialog_st; /***** Local vars *****/ /* defaults */ /* fixed copy so we can reset them */ static const NewsprintValues factory_defaults = { /* resolution stuff */ 10, /* cell width */ /* screen setup (default is the classic rosette pattern) */ CS_RGB, /* use RGB, not CMYK or Intensity */ 100, /* max pullout */ /* grey/black */ 45.0, SPOTFN_DOT, /* red/cyan */ 15.0, SPOTFN_DOT, /* green/magenta */ 75.0, SPOTFN_DOT, /* blue/yellow */ 0.0, SPOTFN_DOT, /* anti-alias control */ DEF_OVERSAMPLE }; static const NewsprintUIValues factory_defaults_ui = { 72, /* input spi */ 7.2, /* output lpi */ FALSE /* lock channels */ }; /* Mutable copy for normal use. Initialised in run(). */ static NewsprintValues pvals; static NewsprintUIValues pvals_ui; static NewsprintInterface pint = { FALSE /* run */ }; /* channel templates */ typedef struct { const gchar *name; /* pointers to the variables this channel updates */ gdouble *angle; gint *spotfn; /* factory defaults for the angle and spot function (as pointers so * the silly compiler can see they're really constants) */ const gdouble *factory_angle; const gint *factory_spotfn; } chan_tmpl; static const chan_tmpl grey_tmpl[] = { { N_("Grey"), &pvals.gry_ang, &pvals.gry_spotfn, &factory_defaults.gry_ang, &factory_defaults.gry_spotfn }, { NULL, NULL, NULL, NULL, NULL } }; static const chan_tmpl rgb_tmpl[] = { { N_("Red"), &pvals.red_ang, &pvals.red_spotfn, &factory_defaults.red_ang, &factory_defaults.red_spotfn }, { N_("Green"), &pvals.grn_ang, &pvals.grn_spotfn, &factory_defaults.grn_ang, &factory_defaults.grn_spotfn }, { N_("Blue"), &pvals.blu_ang, &pvals.blu_spotfn, &factory_defaults.blu_ang, &factory_defaults.blu_spotfn }, { NULL, NULL, NULL, NULL, NULL } }; static const chan_tmpl cmyk_tmpl[] = { { N_("Cyan"), &pvals.red_ang, &pvals.red_spotfn, &factory_defaults.red_ang, &factory_defaults.red_spotfn }, { N_("Magenta"), &pvals.grn_ang, &pvals.grn_spotfn, &factory_defaults.grn_ang, &factory_defaults.grn_spotfn }, { N_("Yellow"), &pvals.blu_ang, &pvals.blu_spotfn, &factory_defaults.blu_ang, &factory_defaults.blu_spotfn }, { N_("Black"), &pvals.gry_ang, &pvals.gry_spotfn, &factory_defaults.gry_ang, &factory_defaults.gry_spotfn }, { NULL, NULL, NULL, NULL, NULL } }; static const chan_tmpl intensity_tmpl[] = { { N_("Intensity"), &pvals.gry_ang, &pvals.gry_spotfn, &factory_defaults.gry_ang, &factory_defaults.gry_spotfn }, { NULL, NULL, NULL, NULL, NULL } }; /* cspace_chan_tmpl is indexed by colourspace, and gives an array of * channel templates for that colourspace */ static const chan_tmpl *cspace_chan_tmpl[] = { grey_tmpl, rgb_tmpl, cmyk_tmpl, intensity_tmpl }; #define NCHANS(x) ((sizeof(x) / sizeof(chan_tmpl)) - 1) /* cspace_nchans gives a quick way of finding the number of channels * in a colourspace. Alternatively, if you're walking the channel * template, you can use the NULL entry at the end to stop. */ static const gint cspace_nchans[] = { NCHANS (grey_tmpl), NCHANS (rgb_tmpl), NCHANS (cmyk_tmpl), NCHANS (intensity_tmpl) }; /* Declare local functions. */ static void query (void); static void run (gchar *name, gint nparams, GimpParam *param, gint *nreturn_vals, GimpParam **return_vals); static gint newsprint_dialog (GimpDrawable *drawable); static void newsprint_ok_callback (GtkWidget *widget, gpointer data); static void newsprint_cspace_update (GtkWidget *widget, gpointer data); static void newsprint (GimpDrawable *drawable); static guchar * spot2thresh (gint type, gint width); GimpPlugInInfo PLUG_IN_INFO = { NULL, /* init_proc */ NULL, /* quit_proc */ query, /* query_proc */ run /* run_proc */ }; /***** Functions *****/ MAIN () static void query (void) { static GimpParamDef args[]= { { GIMP_PDB_INT32, "run_mode", "Interactive, non-interactive" }, { GIMP_PDB_IMAGE, "image", "Input image (unused)" }, { GIMP_PDB_DRAWABLE, "drawable", "Input drawable" }, { GIMP_PDB_INT32, "cell_width", "screen cell width, in pixels" }, { GIMP_PDB_INT32, "colorspace", "separate to 0:RGB, 1:CMYK, 2:Intensity" }, { GIMP_PDB_INT32, "k_pullout", "Percentage of black to pullout (CMYK only)" }, { GIMP_PDB_FLOAT, "gry_ang", "Grey/black screen angle (degrees)" }, { GIMP_PDB_INT32, "gry_spotfn", "Grey/black spot function (0=dots, 1=lines, 2=diamonds, 3=euclidean dot, 4=PS diamond)" }, { GIMP_PDB_FLOAT, "red_ang", "Red/cyan screen angle (degrees)" }, { GIMP_PDB_INT32, "red_spotfn", "Red/cyan spot function (values as gry_spotfn)" }, { GIMP_PDB_FLOAT, "grn_ang", "Green/magenta screen angle (degrees)" }, { GIMP_PDB_INT32, "grn_spotfn", "Green/magenta spot function (values as gry_spotfn)" }, { GIMP_PDB_FLOAT, "blu_ang", "Blue/yellow screen angle (degrees)" }, { GIMP_PDB_INT32, "blu_spotfn", "Blue/yellow spot function (values as gry_spotfn)" }, { GIMP_PDB_INT32, "oversample", "how many times to oversample spot fn" } /* 15 args */ }; static gint nargs = sizeof (args) / sizeof (args[0]); gimp_install_procedure ("plug_in_newsprint", "Re-sample the image to give a newspaper-like effect", "Halftone the image, trading off resolution to " "represent colors or grey levels using the process " "described both in the PostScript language " "definition, and also by Robert Ulichney, \"Digital " "halftoning\", MIT Press, 1987.", "Austin Donnelly", "Austin Donnelly", "1998 (" VERSION ")", N_("/Filters/Distorts/Newsprint..."), "RGB*, GRAY*", GIMP_PLUGIN, nargs, 0, args, NULL); } static void run (gchar *name, gint nparams, GimpParam *param, gint *nreturn_vals, GimpParam **return_vals) { static GimpParam values[1]; GimpDrawable *drawable; GimpRunModeType run_mode; GimpPDBStatusType status = GIMP_PDB_SUCCESS; run_mode = param[0].data.d_int32; *nreturn_vals = 1; *return_vals = values; values[0].type = GIMP_PDB_STATUS; values[0].data.d_status = status; /* basic defaults */ pvals = factory_defaults; pvals_ui = factory_defaults_ui; /* Get the specified drawable */ drawable = gimp_drawable_get (param[2].data.d_drawable); switch (run_mode) { case GIMP_RUN_INTERACTIVE: INIT_I18N_UI(); /* Possibly retrieve data */ gimp_get_data ("plug_in_newsprint", &pvals); gimp_get_data ("plug_in_newsprint_ui", &pvals_ui); /* First acquire information with a dialog */ if (! newsprint_dialog (drawable)) { gimp_drawable_detach (drawable); return; } break; case GIMP_RUN_NONINTERACTIVE: INIT_I18N(); /* Make sure all the arguments are there! */ if (nparams != 15) { status = GIMP_PDB_CALLING_ERROR; break; } pvals.cell_width = param[3].data.d_int32; pvals.colourspace = param[4].data.d_int32; pvals.k_pullout = param[5].data.d_int32; pvals.gry_ang = param[6].data.d_float; pvals.gry_spotfn = param[7].data.d_int32; pvals.red_ang = param[8].data.d_float; pvals.red_spotfn = param[9].data.d_int32; pvals.grn_ang = param[10].data.d_float; pvals.grn_spotfn = param[11].data.d_int32; pvals.blu_ang = param[12].data.d_float; pvals.blu_spotfn = param[13].data.d_int32; pvals.oversample = param[14].data.d_int32; /* check values are within permitted ranges */ if (!VALID_SPOTFN (pvals.gry_spotfn) || !VALID_SPOTFN (pvals.red_spotfn) || !VALID_SPOTFN (pvals.grn_spotfn) || !VALID_SPOTFN (pvals.blu_spotfn) || !VALID_CS (pvals.colourspace) || pvals.k_pullout < 0 || pvals.k_pullout > 100) { status = GIMP_PDB_CALLING_ERROR; } break; case GIMP_RUN_WITH_LAST_VALS: INIT_I18N(); /* Possibly retrieve data */ gimp_get_data ("plug_in_newsprint", &pvals); break; default: break; } if (status == GIMP_PDB_SUCCESS) { /* Make sure that the drawable is gray or RGB color */ if (gimp_drawable_is_rgb (drawable->drawable_id) || gimp_drawable_is_gray (drawable->drawable_id)) { gimp_progress_init (_("Newsprintifing...")); /* set the tile cache size */ gimp_tile_cache_ntiles (TILE_CACHE_SIZE); /* run the newsprint effect */ newsprint (drawable); if (run_mode != GIMP_RUN_NONINTERACTIVE) gimp_displays_flush (); /* Store data */ if (run_mode == GIMP_RUN_INTERACTIVE) { gimp_set_data ("plug_in_newsprint", &pvals, sizeof (NewsprintValues)); gimp_set_data ("plug_in_newsprint_ui", &pvals_ui, sizeof (NewsprintUIValues)); } } else { /*gimp_message ("newsprint: cannot operate on indexed images");*/ status = GIMP_PDB_EXECUTION_ERROR; } } values[0].data.d_status = status; gimp_drawable_detach (drawable); } /* create new menu state, and the preview widgets for it */ static channel_st * new_preview (gint *sfn) { channel_st *st; GtkWidget *preview; GtkWidget *label; gint i; st = g_new (channel_st, 1); st->spotfn_num = sfn; /* make the preview widgets */ for (i = 0; i < 3; i++) { preview = gtk_preview_new (GTK_PREVIEW_COLOR); gtk_preview_size (GTK_PREVIEW (preview), SPOT_PREVIEW_SZ*2 + 1, SPOT_PREVIEW_SZ*2 + 1); gtk_widget_show (preview); label = gtk_label_new (""); gtk_widget_show (label); st->prev[i].widget = preview; st->prev[i].label = label; /* st->prev[i].value changed by preview_update */ } return st; } /* the popup menu "st" has changed, so the previews associated with it * need re-calculation */ static void preview_update (channel_st *st) { gint sfn = *(st->spotfn_num); preview_st *prev; gint i; gint x; gint y; gint width = SPOT_PREVIEW_SZ * PREVIEW_OVERSAMPLE; gint oversample = PREVIEW_OVERSAMPLE; guchar *thresh; gchar pct[12]; guchar value; guchar row[3 * (2 * SPOT_PREVIEW_SZ + 1)]; /* If we don't yet have a preview threshold matrix for this spot * function, generate one now. */ if (!spotfn_list[sfn].prev_thresh) { spotfn_list[sfn].prev_thresh = spot2thresh (sfn, SPOT_PREVIEW_SZ*PREVIEW_OVERSAMPLE); } thresh = spotfn_list[sfn].prev_thresh; for (i = 0; i < 3; i++) { prev = &st->prev[i]; value = spotfn_list[sfn].prev_lvl[i] * 0xff; for (y = 0; y <= SPOT_PREVIEW_SZ*2; y++) { for (x = 0; x <= SPOT_PREVIEW_SZ*2; x++) { guint32 sum = 0; gint sx, sy; gint tx, ty; gint rx, ry; rx = x * PREVIEW_OVERSAMPLE; ry = y * PREVIEW_OVERSAMPLE; for (sy = -oversample/2; sy <= oversample/2; sy++) for (sx = -oversample/2; sx <= oversample/2; sx++) { tx = rx+sx; ty = ry+sy; while (tx < 0) tx += width; while (ty < 0) ty += width; while (tx >= width) tx -= width; while (ty >= width) ty -= width; if (value > THRESH (tx, ty)) sum += 0xff * BARTLETT (sx, sy); } sum /= BARTLETT (0, 0) * BARTLETT (0, 0); /* blue lines on cell boundaries */ if ((x % SPOT_PREVIEW_SZ) == 0 || (y % SPOT_PREVIEW_SZ) == 0) { row[x*3+0] = 0; row[x*3+1] = 0; row[x*3+2] = 0xff; } else { row[x*3+0] = sum; row[x*3+1] = sum; row[x*3+2] = sum; } } gtk_preview_draw_row (GTK_PREVIEW (prev->widget), row, 0, y, SPOT_PREVIEW_SZ*2+1); } /* redraw preview widget */ gtk_widget_draw (prev->widget, NULL); g_snprintf (pct, sizeof (pct), "%2d%%", (int) RINT (spotfn_list[sfn].prev_lvl[i] * 100)); gtk_label_set_text (GTK_LABEL(prev->label), pct); } gdk_flush (); } static void newsprint_menu_callback (GtkWidget *widget, gpointer data) { channel_st *st = data; gpointer ud; gint menufn; static gint in_progress = FALSE; /* we shouldn't need recursion protection, but if lock_channels is * set, and gtk_option_menu_set_history ever generates an * "activated" signal, then we'll get back here. So we've defensive. */ if (in_progress) { printf ("newsprint_menu_callback: unexpected recursion: can't happen\n"); return; } in_progress = TRUE; ud = gtk_object_get_user_data (GTK_OBJECT (widget)); menufn = GPOINTER_TO_INT (ud); *(st->spotfn_num) = menufn; preview_update (st); /* ripple the change to the other popups if the channels are * locked together. */ if (pvals_ui.lock_channels) { channel_st *c = st->next; gint oldfn; while (c != st) { gtk_option_menu_set_history (GTK_OPTION_MENU (c->option_menu), menufn); oldfn = *(c->spotfn_num); *(c->spotfn_num) = menufn; if (oldfn != menufn) preview_update (c); c = c->next; } } in_progress = FALSE; } static void angle_callback (GtkAdjustment *adjustment, gpointer data) { channel_st *st = data; channel_st *c; gdouble *angle_ptr; static gint in_progress = FALSE; angle_ptr = gtk_object_get_user_data (GTK_OBJECT (adjustment)); gimp_double_adjustment_update (adjustment, angle_ptr); if (pvals_ui.lock_channels && !in_progress) { in_progress = TRUE; c = st->next; while (c != st) { gtk_adjustment_set_value (GTK_ADJUSTMENT (c->angle_adj), *angle_ptr); c = c->next; } in_progress = FALSE; } } static void spi_callback (GtkAdjustment *adjustment, gpointer data) { NewsprintDialog_st *st = data; gimp_double_adjustment_update (adjustment, &pvals_ui.input_spi); gtk_signal_handler_block_by_data (GTK_OBJECT (st->output_lpi), data); gtk_adjustment_set_value (GTK_ADJUSTMENT (st->output_lpi), pvals_ui.input_spi / pvals.cell_width); gtk_signal_handler_unblock_by_data (GTK_OBJECT (st->output_lpi), data); } static void lpi_callback (GtkAdjustment *adjustment, gpointer data) { NewsprintDialog_st *st = data; gimp_double_adjustment_update (adjustment, &pvals_ui.output_lpi); gtk_signal_handler_block_by_data (GTK_OBJECT (st->cellsize), data); gtk_adjustment_set_value (GTK_ADJUSTMENT (st->cellsize), pvals_ui.input_spi / pvals_ui.output_lpi); gtk_signal_handler_unblock_by_data (GTK_OBJECT (st->cellsize), data); } static void cellsize_callback (GtkAdjustment *adjustment, gpointer data) { NewsprintDialog_st *st = data; gimp_int_adjustment_update (adjustment, &pvals.cell_width); gtk_signal_handler_block_by_data (GTK_OBJECT (st->output_lpi), data); gtk_adjustment_set_value (GTK_ADJUSTMENT (st->output_lpi), pvals_ui.input_spi / pvals.cell_width); gtk_signal_handler_unblock_by_data (GTK_OBJECT (st->output_lpi), data); } static void newsprint_defaults_callback (GtkWidget *widget, gpointer data) { NewsprintDialog_st *st = data; gint saved_lock; gint cspace; channel_st **chst; const chan_tmpl *ct; gint spotfn; gint c; /* temporarily turn off channel lock */ saved_lock = pvals_ui.lock_channels; pvals_ui.lock_channels = FALSE; /* for each colourspace, reset its channel info */ for (cspace = 0; cspace < NUM_CS; cspace++) { chst = st->chst[cspace]; ct = cspace_chan_tmpl[cspace]; /* skip this colourspace if we haven't used it yet */ if (!chst[0]) continue; c = 0; while (ct->name) { gtk_adjustment_set_value (GTK_ADJUSTMENT (chst[c]->angle_adj), *(ct->factory_angle)); /* change the popup menu and also activate the menuitem in * question, in order to run the handler that re-computes * the preview area */ spotfn = *(ct->factory_spotfn); gtk_option_menu_set_history (GTK_OPTION_MENU (chst[c]->option_menu), spotfn); gtk_menu_item_activate (GTK_MENU_ITEM (chst[c]->menuitem[spotfn])); c++; ct++; } } pvals_ui.lock_channels = saved_lock; } /* Create (but don't yet show) a new vbox for a channel 'widget'. * Return the channel state, so caller can find the vbox and place it * in the notebook. */ static channel_st * new_channel (const chan_tmpl *ct) { GtkWidget *table; GtkWidget *hbox; GtkWidget *hbox2; GtkWidget *abox; GtkWidget *label; GtkWidget *menu; spot_info_t *sf; channel_st *chst; gint i; /* create the channel state record */ chst = new_preview (ct->spotfn); chst->vbox = gtk_vbox_new (FALSE, 4); gtk_container_set_border_width (GTK_CONTAINER (chst->vbox), 4); table = gtk_table_new (1, 3, FALSE); gtk_table_set_col_spacings (GTK_TABLE (table), 4); gtk_box_pack_start (GTK_BOX (chst->vbox), table, FALSE, FALSE, 0); gtk_widget_show (table); /* angle slider */ chst->angle_adj = gimp_scale_entry_new (GTK_TABLE (table), 0, 0, _("Angle:"), SCALE_WIDTH, 0, *ct->angle, -90, 90, 1, 15, 1, TRUE, 0, 0, NULL, NULL); gtk_object_set_user_data (chst->angle_adj, ct->angle); gtk_signal_connect (GTK_OBJECT (chst->angle_adj), "value_changed", GTK_SIGNAL_FUNC (angle_callback), chst); /* spot function popup */ hbox = gtk_hbox_new (FALSE, 6); gtk_box_pack_start (GTK_BOX (chst->vbox), hbox, FALSE, FALSE, 0); gtk_widget_show (hbox); abox = gtk_alignment_new (0.5, 0.0, 0.0, 0.0); gtk_box_pack_start (GTK_BOX (hbox), abox, FALSE, FALSE, 0); gtk_widget_show (abox); hbox2 = gtk_hbox_new (FALSE, 4); gtk_container_add (GTK_CONTAINER (abox), hbox2); gtk_widget_show (hbox2); label = gtk_label_new( _("Spot Function:")); gtk_misc_set_alignment (GTK_MISC (label), 1.0, 0.5); gtk_box_pack_start (GTK_BOX (hbox2), label, FALSE, FALSE, 0); gtk_widget_show (label); chst->option_menu = gtk_option_menu_new (); gtk_box_pack_start (GTK_BOX (hbox2), chst->option_menu, FALSE, FALSE, 0); gtk_widget_show (chst->option_menu); menu = gtk_menu_new (); sf = spotfn_list; i = 0; while (sf->name) { chst->menuitem[i] = gtk_menu_item_new_with_label( gettext(sf->name)); gtk_signal_connect (GTK_OBJECT (chst->menuitem[i]), "activate", GTK_SIGNAL_FUNC (newsprint_menu_callback), chst); gtk_object_set_user_data (GTK_OBJECT (chst->menuitem[i]), GINT_TO_POINTER (i)); gtk_widget_show (chst->menuitem[i]); gtk_menu_shell_append (GTK_MENU_SHELL (menu), GTK_WIDGET (chst->menuitem[i])); sf++; i++; } gtk_menu_set_active (GTK_MENU (menu), *ct->spotfn); gtk_option_menu_set_menu (GTK_OPTION_MENU (chst->option_menu), menu); gtk_widget_show (chst->option_menu); /* spot function previews */ { GtkWidget *sub; sub = gtk_table_new (2, 3, FALSE); gtk_table_set_col_spacings (GTK_TABLE (sub), 6); gtk_table_set_row_spacings (GTK_TABLE (sub), 1); gtk_box_pack_start (GTK_BOX (hbox), sub, FALSE, FALSE, 0); for (i = 0; i < 3; i++) { gtk_table_attach (GTK_TABLE (sub), chst->prev[i].widget, i, i+1, 0, 1, GTK_SHRINK | GTK_FILL, GTK_FILL, 0, 0); gtk_table_attach (GTK_TABLE (sub), chst->prev[i].label, i, i+1, 1, 2, GTK_SHRINK | GTK_FILL, GTK_FILL, 0, 0); } gtk_widget_show (sub); } /* fire the update once to make sure we start with something * in the preview windows */ preview_update (chst); gtk_widget_show (table); /* create the menuitem used to select this channel for editing */ chst->ch_menuitem = gtk_menu_item_new_with_label (gettext (ct->name)); gtk_object_set_user_data (GTK_OBJECT (chst->ch_menuitem), chst); /* signal attachment and showing left to caller */ /* deliberately don't show the chst->frame, leave that up to * the caller */ return chst; } /* Make all the channels needed for "colourspace", and fill in * the respective channel state fields in "st". */ static void gen_channels (NewsprintDialog_st *st, gint colourspace) { const chan_tmpl *ct; channel_st **chst; channel_st *base = NULL; gint i; chst = st->chst[colourspace]; ct = cspace_chan_tmpl[colourspace]; i = 0; st->channel_notebook[colourspace] = gtk_notebook_new (); gtk_box_pack_start (GTK_BOX (st->vbox), st->channel_notebook[colourspace], FALSE, FALSE, 0); gtk_box_reorder_child (GTK_BOX (st->vbox), st->channel_notebook[colourspace], 3); gtk_widget_show (st->channel_notebook[colourspace]); while (ct->name) { chst[i] = new_channel (ct); if (i) chst[i-1]->next = chst[i]; else base = chst[i]; gtk_notebook_append_page (GTK_NOTEBOOK (st->channel_notebook[colourspace]), chst[i]->vbox, gtk_label_new (gettext (ct->name))); gtk_widget_show (chst[i]->vbox); i++; ct++; } /* make the list circular */ chst[i-1]->next = base; } static gint newsprint_dialog (GimpDrawable *drawable) { /* widgets we need from callbacks stored here */ NewsprintDialog_st st; GtkWidget *main_vbox; GtkWidget *frame; GtkWidget *table; GtkObject *adj; GSList *group = NULL; gint bpp; gint i; gdouble xres, yres; gimp_ui_init ("newsprint", TRUE); /* flag values to say we haven't filled these channel * states in yet */ for(i=0; idrawable_id); if (gimp_drawable_has_alpha (drawable->drawable_id)) bpp--; /* force greyscale if it's the only thing we can do */ if (bpp == 1) { pvals.colourspace = CS_GREY; } else { if (pvals.colourspace == CS_GREY) pvals.colourspace = CS_RGB; } st.dlg = gimp_dialog_new (_("Newsprint"), "newsprint", gimp_standard_help_func, "filters/newsprint.html", GTK_WIN_POS_MOUSE, FALSE, TRUE, FALSE, GTK_STOCK_CANCEL, gtk_widget_destroy, NULL, 1, NULL, FALSE, TRUE, GTK_STOCK_OK, newsprint_ok_callback, NULL, NULL, NULL, TRUE, FALSE, NULL); gtk_signal_connect (GTK_OBJECT (st.dlg), "destroy", GTK_SIGNAL_FUNC (gtk_main_quit), NULL); main_vbox = gtk_vbox_new (FALSE, 4); gtk_container_set_border_width (GTK_CONTAINER (main_vbox), 6); gtk_box_pack_start (GTK_BOX (GTK_DIALOG (st.dlg)->vbox), main_vbox, TRUE, TRUE, 0); gtk_widget_show (main_vbox); /* resolution settings */ frame = gtk_frame_new (_("Resolution")); gtk_frame_set_shadow_type (GTK_FRAME (frame), GTK_SHADOW_ETCHED_IN); gtk_box_pack_start (GTK_BOX (main_vbox), frame, FALSE, FALSE, 0); table = gtk_table_new (3, 3, FALSE); gtk_table_set_col_spacings (GTK_TABLE (table), 4); gtk_table_set_row_spacings (GTK_TABLE (table), 2); gtk_container_set_border_width (GTK_CONTAINER (table), 4); gtk_container_add (GTK_CONTAINER (frame), table); gimp_image_get_resolution (gimp_drawable_image_id( drawable->drawable_id), &xres, &yres); /* XXX hack: should really note both resolutions, and use * rectangular cells, not square cells. But I'm being lazy, * and the majority of the world works with xres == yres */ pvals_ui.input_spi = xres; st.input_spi = gimp_scale_entry_new (GTK_TABLE (table), 0, 0, _("Input SPI:"), SCALE_WIDTH, 0, pvals_ui.input_spi, 1.0, 1200.0, 1.0, 10.0, 0, FALSE, GIMP_MIN_RESOLUTION, GIMP_MAX_RESOLUTION, NULL, NULL); gtk_signal_connect (GTK_OBJECT (st.input_spi), "value_changed", GTK_SIGNAL_FUNC (spi_callback), &st); st.output_lpi = gimp_scale_entry_new (GTK_TABLE (table), 0, 1, _("Output LPI:"), SCALE_WIDTH, 0, pvals_ui.output_lpi, 1.0, 1200.0, 1.0, 10.0, 1, FALSE, GIMP_MIN_RESOLUTION, GIMP_MAX_RESOLUTION, NULL, NULL); gtk_signal_connect (GTK_OBJECT (st.output_lpi), "value_changed", GTK_SIGNAL_FUNC (lpi_callback), &st); st.cellsize = gimp_scale_entry_new (GTK_TABLE (table), 0, 2, _("Cell Size:"), SCALE_WIDTH, 0, pvals.cell_width, 3.0, 100.0, 1.0, 5.0, 0, FALSE, 3.0, GIMP_MAX_IMAGE_SIZE, NULL, NULL); gtk_signal_connect (GTK_OBJECT (st.cellsize), "value_changed", GTK_SIGNAL_FUNC (cellsize_callback), &st); gtk_widget_show (table); gtk_widget_show (frame); /* screen settings */ frame = gtk_frame_new (_("Screen")); gtk_frame_set_shadow_type (GTK_FRAME (frame), GTK_SHADOW_ETCHED_IN); gtk_box_pack_start (GTK_BOX (main_vbox), frame, FALSE, FALSE, 0); st.vbox = gtk_vbox_new (FALSE, 4); gtk_container_set_border_width (GTK_CONTAINER (st.vbox), 4); gtk_container_add (GTK_CONTAINER (frame), st.vbox); /* optional portion begins */ if (bpp != 1) { GtkWidget *hbox; GtkWidget *label; GtkWidget *sep; GtkWidget *button; GtkWidget *toggle; st.pull_table = gtk_table_new (1, 3, FALSE); gtk_table_set_col_spacings (GTK_TABLE (st.pull_table), 4); gtk_table_set_row_spacings (GTK_TABLE (st.pull_table), 2); /* black pullout */ st.pull = gimp_scale_entry_new (GTK_TABLE (st.pull_table), 0, 0, _("Black Pullout (%):"), SCALE_WIDTH, 0, pvals.k_pullout, 0, 100, 1, 10, 0, TRUE, 0, 0, NULL, NULL); gtk_signal_connect (GTK_OBJECT (st.pull), "value_changed", GTK_SIGNAL_FUNC (gimp_int_adjustment_update), &pvals.k_pullout); gtk_widget_set_sensitive (st.pull_table, (pvals.colourspace == CS_CMYK)); gtk_widget_show (st.pull_table); /* RGB / CMYK / Intensity select */ hbox = gtk_hbox_new (FALSE, 6); gtk_box_pack_start (GTK_BOX (st.vbox), hbox, FALSE, FALSE, 0); sep = gtk_hseparator_new (); gtk_box_pack_start (GTK_BOX (st.vbox), sep, FALSE, FALSE, 0); gtk_widget_show (sep); /* pack the scaleentry table */ gtk_box_pack_start (GTK_BOX (st.vbox), st.pull_table, FALSE, FALSE, 0); label = gtk_label_new( _("Separate to:")); gtk_box_pack_start (GTK_BOX (hbox), label, FALSE, FALSE, 0); gtk_widget_show(label); toggle = gtk_radio_button_new_with_label(group, _("RGB")); group = gtk_radio_button_group (GTK_RADIO_BUTTON (toggle)); gtk_box_pack_start (GTK_BOX (hbox), toggle, TRUE, TRUE, 0); gtk_object_set_user_data(GTK_OBJECT(toggle), &st); gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (toggle), (pvals.colourspace == CS_RGB)); gtk_signal_connect (GTK_OBJECT (toggle), "toggled", (GtkSignalFunc) newsprint_cspace_update, GINT_TO_POINTER(CS_RGB)); gtk_widget_show (toggle); toggle = gtk_radio_button_new_with_label (group, _("CMYK")); group = gtk_radio_button_group (GTK_RADIO_BUTTON (toggle)); gtk_box_pack_start (GTK_BOX (hbox), toggle, TRUE, TRUE, 0); gtk_object_set_user_data(GTK_OBJECT(toggle), &st); gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (toggle), (pvals.colourspace == CS_CMYK)); gtk_signal_connect (GTK_OBJECT (toggle), "toggled", (GtkSignalFunc) newsprint_cspace_update, GINT_TO_POINTER(CS_CMYK)); gtk_widget_show (toggle); toggle = gtk_radio_button_new_with_label (group, _("Intensity")); group = gtk_radio_button_group (GTK_RADIO_BUTTON (toggle)); gtk_box_pack_start (GTK_BOX (hbox), toggle, TRUE, TRUE, 0); gtk_object_set_user_data(GTK_OBJECT(toggle), &st); gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (toggle), (pvals.colourspace == CS_INTENSITY)); gtk_signal_connect (GTK_OBJECT (toggle), "toggled", (GtkSignalFunc) newsprint_cspace_update, GINT_TO_POINTER(CS_INTENSITY)); gtk_widget_show (toggle); gtk_widget_show (hbox); /* channel lock & factory defaults button */ hbox = gtk_hbutton_box_new (); gtk_button_box_set_spacing (GTK_BUTTON_BOX (hbox), 10); gtk_box_pack_start (GTK_BOX (st.vbox), hbox, FALSE, FALSE, 0); gtk_widget_show (hbox); toggle = gtk_check_button_new_with_label (_("Lock Channels")); gtk_signal_connect (GTK_OBJECT (toggle), "toggled", GTK_SIGNAL_FUNC (gimp_toggle_button_update), &pvals_ui.lock_channels); gtk_object_set_user_data (GTK_OBJECT (toggle), NULL); gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (toggle), pvals_ui.lock_channels); gtk_box_pack_start (GTK_BOX (hbox), toggle, FALSE, FALSE, 0); gtk_widget_show (toggle); button = gtk_button_new_with_label (_("Factory Defaults")); gtk_signal_connect(GTK_OBJECT (button), "clicked", GTK_SIGNAL_FUNC (newsprint_defaults_callback), &st); gtk_box_pack_end (GTK_BOX (hbox), button, FALSE, FALSE, 0); gtk_widget_show (button); } /* Make the channels appropriate for this colourspace and * currently selected defaults. They may have already been * created as a result of callbacks to cspace_update from * gtk_toggle_button_set_active(). */ if (!st.chst[pvals.colourspace][0]) { gen_channels (&st, pvals.colourspace); } gtk_widget_show (st.vbox); gtk_widget_show (frame); /* anti-alias control */ frame = gtk_frame_new (_("Antialiasing")); gtk_frame_set_shadow_type (GTK_FRAME (frame), GTK_SHADOW_ETCHED_IN); gtk_box_pack_start (GTK_BOX (main_vbox), frame, FALSE, FALSE, 0); table = gtk_table_new (1, 3, FALSE); gtk_table_set_col_spacings (GTK_TABLE (table), 4); gtk_container_set_border_width (GTK_CONTAINER (table), 4); gtk_container_add (GTK_CONTAINER (frame), table); adj = gimp_scale_entry_new (GTK_TABLE (table), 0, 0, _("Oversample:"), SCALE_WIDTH, 0, pvals.oversample, 1.0, 15.0, 1.0, 5.0, 0, TRUE, 0, 0, NULL, NULL); gtk_signal_connect (GTK_OBJECT (adj), "value_changed", GTK_SIGNAL_FUNC (gimp_int_adjustment_update), &pvals.oversample); gtk_widget_show (table); gtk_widget_show (frame); gtk_widget_show (st.dlg); gtk_main (); gdk_flush (); return pint.run; } /* Newsprint interface functions */ static void newsprint_ok_callback (GtkWidget *widget, gpointer data) { pint.run = TRUE; gtk_widget_destroy (GTK_WIDGET (data)); } static void newsprint_cspace_update (GtkWidget *widget, gpointer data) { NewsprintDialog_st *st; gint new_cs = GPOINTER_TO_INT (data); gint old_cs = pvals.colourspace; st = gtk_object_get_user_data (GTK_OBJECT (widget)); if (!st) printf ("newsprint: cspace_update: no state, can't happen!\n"); if (st) { /* the CMYK widget looks after the black pullout widget */ if (new_cs == CS_CMYK) { gtk_widget_set_sensitive (st->pull_table, GTK_TOGGLE_BUTTON (widget)->active); } /* if we're not activate, then there's nothing more to do */ if (!GTK_TOGGLE_BUTTON (widget)->active) return; pvals.colourspace = new_cs; /* make sure we have the necessary channels for the new * colourspace */ if (!st->chst[new_cs][0]) gen_channels (st, new_cs); /* hide the old channels (if any) */ if (st->channel_notebook[old_cs]) { gtk_widget_hide (st->channel_notebook[old_cs]); } /* show the new channels */ gtk_widget_show (st->channel_notebook[new_cs]); gtk_notebook_set_current_page (GTK_NOTEBOOK (st->channel_notebook[new_cs]), 0); } } /* * Newsprint Effect */ /*************************************************************/ /* Spot functions */ /* Spot functions define the order in which pixels should be whitened * as a cell lightened in colour. They are defined over the entire * cell, and are called over each pixel in the cell. The cell * co-ordinate space ranges from -1.0 .. +1.0 inclusive, in both x- and * y-axes. * * This means the spot function f(x, y) must be defined for: * -1 <= x <= +1, where x is a real number, and * -1 <= y <= +1, where y is a real number. * * The function f's range is -1.0 .. +1.0 inclusive, but it is * permissible for f to return values outside this range: the nearest * valid value will be used instead. NOTE: this is in contrast with * PostScript spot functions, where it is a RangeError for the spot * function to go outside these limits. * * An initially black cell is filled from lowest spot function value * to highest. The actual values returned do not matter - it is their * relative orderings that count. This means that spot functions do * not need to be tonally balanced. A tonally balanced spot function * is one which for all slices though the function (eg say at z), the * area of the slice = 4z. In particular, almost all PostScript spot * functions are _not_ tonally balanced. */ /* The classic growing dot spot function. This one isn't tonally * balanced. It can be made so, but it's _really_ ugly. Thanks to * Richard Mortier for this one: * * #define a(r) \ * ((r<=1)? G_PI * (r*r) : \ * 4 * sqrt(r*r - 1) + G_PI*r*r - 4*r*r*acos(1/r)) * * radius = sqrt(x*x + y*y); * * return a(radius) / 4; */ static gdouble spot_round (gdouble x, gdouble y) { return 1 - (x * x + y * y); } /* Another commonly seen spot function is the v-shaped wedge. Tonally * balanced. */ static gdouble spot_line (gdouble x, gdouble y) { return ABS (y); } /* Square/Diamond dot that never becomes round. Tonally balanced. */ static gdouble spot_diamond (gdouble x, gdouble y) { gdouble xy = ABS (x) + ABS (y); /* spot only valid for 0 <= xy <= 2 */ return ((xy <= 1) ? 2 * xy * xy : 2 * xy * xy - 4 * (xy - 1) * (xy - 1)) / 4; } /* The following functions were derived from a peice of PostScript by * Peter Fink and published in his book, "PostScript Screening: Adobe * Accurate Screens" (Adobe Press, 1992). Adobe Systems Incorporated * allow its use, provided the following copyright message is present: * * % Film Test Pages for Screenset Development * % Copyright (c) 1991 and 1992 Adobe Systems Incorporated * % All rights reserved. * % * % NOTICE: This code is copyrighted by Adobe Systems Incorporated, and * % may not be reproduced for sale except by permission of Adobe Systems * % Incorporated. Adobe Systems Incorporated grants permission to use * % this code for the development of screen sets for use with Adobe * % Accurate Screens software, as long as the copyright notice remains * % intact. * % * % By Peter Fink 1991/1992 */ /* Square (or Euclidean) dot. Also very common. */ static gdouble spot_PSsquare (gdouble x, gdouble y) { gdouble ax = ABS (x); gdouble ay = ABS (y); return ((ax + ay) > 1 ? ((ay - 1) * (ay - 1) + (ax - 1) * (ax - 1)) - 1 : 1 - (ay * ay + ax * ax)); } /* Diamond spot function, again from Peter Fink's PostScript * original. Copyright as for previous function. */ static gdouble spot_PSdiamond (gdouble x, gdouble y) { gdouble ax = ABS (x); gdouble ay = ABS (y); return ((ax + ay) <= 0.75 ? 1 - (ax * ax + ay * ay) : /* dot */ ((ax + ay) <= 1.23 ? 1 - ((ay * 0.76) + ax) : /* to diamond (0.76 distort) */ ((ay - 1) * (ay - 1) + (ax - 1) * (ax - 1)) -1)); /* back to dot */ } /* end of Adobe Systems Incorporated copyrighted functions */ /*************************************************************/ /* Spot function to threshold matrix conversion */ /* Each call of the spot function results in one of these */ typedef struct { gint index; /* (y * width) + x */ gdouble value; /* return value of the spot function */ } order_t; /* qsort(3) compare function */ static gint order_cmp (const void *va, const void *vb) { const order_t *a = va; const order_t *b = vb; return (a->value < b->value) ? -1 : ((a->value > b->value)? + 1 : 0); } /* Convert spot function "type" to a threshold matrix of size "width" * times "width". Returns newly allocated threshold matrix. The * reason for qsort()ing the results rather than just using the spot * function's value directly as the threshold value is that we want to * ensure that the threshold matrix is tonally balanced - that is, for * a threshold value of x%, x% of the values in the matrix are < x%. * * Actually, it turns out that qsort()ing a function which is already * balanced can quite significantly detract from the quality of the * final result. This is particularly noticable with the line or * diamond spot functions at 45 degrees. This is because if the spot * function has multiple locations with the same value, qsort may use * them in any order. Often, there is quite clearly an optimal order * however. By marking functions as pre-balanced, this random * shuffling is avoided. WARNING: a non-balanced spot function marked * as pre-balanced is bad: you'll end up with dark areas becoming too * dark or too light, and vice versa for light areas. This is most * easily checked by halftoning an area, then bluring it back - you * should get the same colour back again. The only way of getting a * correctly balanced function is by getting a formula for the spot's * area as a function of x and y - this can be fairly tough (ie * possiblly an integral in two dimensions that must be solved * analytically). * * The threshold matrix is used to compare against image values. If * the image value is greater than the threshold value, then the * output pixel is illuminated. This means that a threshold matrix * entry of 0 never causes output pixels to be illuminated. */ static guchar * spot2thresh (gint type, gint width) { gdouble sx, sy; gdouble val; spotfn_t *spotfn; guchar *thresh; order_t *order; gint x, y; gint i; gint wid2 = width * width; gint balanced = spotfn_list[type].balanced; thresh = g_new (guchar, wid2); spotfn = spotfn_list[type].fn; order = g_new (order_t, wid2); i = 0; for (y = 0; y < width; y++) { for (x = 0; x < width; x++) { /* scale x & y to -1 ... +1 inclusive */ sx = (((gdouble)x) / (width-1) - 0.5) * 2; sy = (((gdouble)y) / (width-1) - 0.5) * 2; val = spotfn(sx, sy); val = CLAMP (val, -1, 1); /* interval is inclusive */ order[i].index = i; order[i].value = val; i++; } } if (!balanced) { /* now sort array of (point, value) pairs in ascending order */ qsort (order, wid2, sizeof (order_t), order_cmp); } /* compile threshold matrix in order from darkest to lightest */ for (i = 0; i < wid2; i++) { /* thresh[] contains values from 0 .. 254. The reason for not * including 255 is so that an image value of 255 remains * unmolested. It would be bad to filter a completely white * image and end up with black speckles. */ if (balanced) thresh[order[i].index] = order[i].value * 0xfe; else thresh[order[i].index] = i * 0xff / wid2; } g_free (order); /* TODO: this is where the code to apply a transfer or dot gain * function to the threshold matrix would go. */ return thresh; } /**************************************************************/ /* Main loop */ /* This function operates on the image, striding across it in tiles. */ static void newsprint (GimpDrawable *drawable) { GimpPixelRgn src_rgn, dest_rgn; guchar *src_row, *dest_row; guchar *src, *dest; guchar *thresh[4]; gdouble r; gdouble theta; gdouble rot[4]; gdouble k_pullout; gint bpp, colour_bpp; gint has_alpha; gint b; gint tile_width; gint width; gint row, col; gint x, y, x_step, y_step; gint x1, y1, x2, y2; gint rx, ry; gint progress, max_progress; gint oversample; gint colourspace; gpointer pr; gint w002; TM_INIT (); width = pvals.cell_width; if (width < 0) width = -width; if (width < 1) width = 1; oversample = pvals.oversample; k_pullout = ((gdouble) pvals.k_pullout) / 100.0; width *= oversample; tile_width = gimp_tile_width (); gimp_drawable_mask_bounds (drawable->drawable_id, &x1, &y1, &x2, &y2); bpp = gimp_drawable_bpp (drawable->drawable_id); has_alpha = gimp_drawable_has_alpha (drawable->drawable_id); colour_bpp = has_alpha ? bpp-1 : bpp; colourspace= pvals.colourspace; if (colour_bpp == 1) { colourspace = CS_GREY; } else { if (colourspace == CS_GREY) colourspace = CS_RGB; } /* Bartlett window matrix optimisation */ w002 = BARTLETT (0, 0) * BARTLETT (0, 0); #if 0 /* It turns out to be slightly slower to cache a pre-computed * bartlett matrix! I put it down to d-cache pollution *shrug* */ wgt = g_new (gint, oversample * oversample); for (y = -oversample / 2; y <= oversample / 2; y++) for (x = -oversample / 2; x<=oversample / 2; x++) WGT (x, y) = BARTLETT (x, y); #endif /* 0 */ #define ASRT(_x) \ do { \ if (!VALID_SPOTFN(_x)) \ { \ printf("newsprint: %d is not a valid spot type\n", _x); \ _x = SPOTFN_DOT; \ } \ } while(0) /* calculate the RGB / CMYK rotations and threshold matrices */ if (colour_bpp == 1 || colourspace == CS_INTENSITY) { rot[0] = DEG2RAD (pvals.gry_ang); thresh[0] = spot2thresh (pvals.gry_spotfn, width); } else { gint rf = pvals.red_spotfn; gint gf = pvals.grn_spotfn; gint bf = pvals.blu_spotfn; rot[0] = DEG2RAD (pvals.red_ang); rot[1] = DEG2RAD (pvals.grn_ang); rot[2] = DEG2RAD (pvals.blu_ang); /* always need at least one threshold matrix */ ASRT (rf); spotfn_list[rf].thresh = spot2thresh (rf, width); thresh[0] = spotfn_list[rf].thresh; /* optimisation: only use separate threshold matrices if the * spot functions are actually different */ ASRT (gf); if (!spotfn_list[gf].thresh) spotfn_list[gf].thresh = spot2thresh (gf, width); thresh[1] = spotfn_list[gf].thresh; ASRT (bf); if (!spotfn_list[bf].thresh) spotfn_list[bf].thresh = spot2thresh (bf, width); thresh[2] = spotfn_list[bf].thresh; if (colourspace == CS_CMYK) { rot[3] = DEG2RAD (pvals.gry_ang); gf = pvals.gry_spotfn; ASRT (gf); if (!spotfn_list[gf].thresh) spotfn_list[gf].thresh = spot2thresh (gf, width); thresh[3] = spotfn_list[gf].thresh; } } /* Initialize progress */ progress = 0; max_progress = (x2 - x1) * (y2 - y1); TM_START (); for (y = y1; y < y2; y += tile_width - (y % tile_width)) { for (x = x1; x < x2; x += tile_width - (x % tile_width)) { /* snap to tile boundary */ x_step = tile_width - (x % tile_width); y_step = tile_width - (y % tile_width); /* don't step off the end of the image */ x_step = MIN (x_step, x2-x); y_step = MIN (y_step, y2-y); /* set up the source and dest regions */ gimp_pixel_rgn_init (&src_rgn, drawable, x, y, x_step, y_step, FALSE/*dirty*/, FALSE/*shadow*/); gimp_pixel_rgn_init (&dest_rgn, drawable, x, y, x_step, y_step, TRUE/*dirty*/, TRUE/*shadow*/); /* page in the image, one tile at a time */ for (pr = gimp_pixel_rgns_register (2, &src_rgn, &dest_rgn); pr != NULL; pr = gimp_pixel_rgns_process (pr)) { src_row = src_rgn.data; dest_row = dest_rgn.data; for (row = 0; row < src_rgn.h; row++) { src = src_row; dest = dest_row; for (col = 0; col < src_rgn.w; col++) { guchar data[4]; rx = (x + col) * oversample; ry = (y + row) * oversample; /* convert rx and ry to polar (r, theta) */ r = sqrt (((double)rx)*rx + ((double)ry)*ry); theta = atan2 (((gdouble)ry), ((gdouble)rx)); for (b = 0; b < colour_bpp; b++) data[b] = src[b]; /* do colour space conversion */ switch (colourspace) { case CS_CMYK: data[3] = 0xff; data[0] = 0xff - data[0]; data[3] = MIN (data[3], data[0]); data[1] = 0xff - data[1]; data[3] = MIN (data[3], data[1]); data[2] = 0xff - data[2]; data[3] = MIN (data[3], data[2]); data[3] = ((gdouble)data[3]) * k_pullout; data[0] -= data[3]; data[1] -= data[3]; data[2] -= data[3]; break; case CS_INTENSITY: data[3] = data[0]; /* save orig for later */ data[0] = INTENSITY (data[0], data[1], data[2]); break; default: break; } for (b = 0; b < cspace_nchans[colourspace]; b++) { rx = RINT (r * cos (theta + rot[b])); ry = RINT (r * sin (theta + rot[b])); /* Make sure rx and ry are positive and within * the range 0 .. width-1 (incl). Can't use % * operator, since its definition on negative * numbers is not helpful. Can't use ABS(), * since that would cause reflection about the * x- and y-axes. Relies on integer division * rounding towards zero. */ rx -= ((rx - ISNEG (rx) * (width-1)) / width) * width; ry -= ((ry - ISNEG (ry) * (width-1)) / width) * width; { guint32 sum = 0; gint sx, sy; gint tx, ty; for (sy = -oversample/2; sy <= oversample/2; sy++) for (sx = -oversample/2; sx <= oversample/2; sx++) { tx = rx+sx; ty = ry+sy; while (tx < 0) tx += width; while (ty < 0) ty += width; while (tx >= width) tx -= width; while (ty >= width) ty -= width; if (data[b] > THRESHn(b, tx, ty)) sum += 0xff * BARTLETT(sx, sy); } sum /= w002; data[b] = sum; } } if (has_alpha) dest[colour_bpp] = src[colour_bpp]; /* re-pack the colours into RGB */ switch (colourspace) { case CS_CMYK: data[0] = CLAMPED_ADD (data[0], data[3]); data[1] = CLAMPED_ADD (data[1], data[3]); data[2] = CLAMPED_ADD (data[2], data[3]); data[0] = 0xff - data[0]; data[1] = 0xff - data[1]; data[2] = 0xff - data[2]; break; case CS_INTENSITY: if (has_alpha) { dest[colour_bpp] = data[0]; data[0] = 0xff; } data[1] = data[1] * data[0] / 0xff; data[2] = data[2] * data[0] / 0xff; data[0] = data[3] * data[0] / 0xff; break; default: /* no other special cases */ break; } for (b = 0; b < colour_bpp; b++) dest[b] = data[b]; src += src_rgn.bpp; dest += dest_rgn.bpp; } src_row += src_rgn.rowstride; dest_row += dest_rgn.rowstride; } /* Update progress */ progress += src_rgn.w * src_rgn.h; gimp_progress_update ((double) progress / (double) max_progress); } } } TM_END (); /* We don't free the threshold matrices, since we're about to * exit, and the OS should clean up after us. */ /* update the affected region */ gimp_drawable_flush (drawable); gimp_drawable_merge_shadow (drawable->drawable_id, TRUE); gimp_drawable_update (drawable->drawable_id, x1, y1, (x2 - x1), (y2 - y1)); }