gimp/plug-ins/common/newsprint.c

2076 lines
64 KiB
C

/* GIMP - The GNU Image Manipulation Program
* Copyright (C) 1995 Spencer Kimball and Peter Mattis
*
* newsprint plug-in
* Copyright (C) 1997-1998 Austin Donnelly <austin@gimp.org>
*
* 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 3 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, see <http://www.gnu.org/licenses/>.
*/
/*
* version 0.60
*
* 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 <austin@greenend.org.uk>
* http://www.cl.cam.ac.uk/~and1000/newsprint/
*
* Richard Mortier <rmm1002@cam.ac.uk> did the spot_round() function
* with correct tonal balance.
*
* Tim Harris <tim.harris@acm.org> provided valuable feedback on
* pre-press issues.
*/
#include "config.h"
#include <stdlib.h>
#include <string.h>
#include <libgimp/gimp.h>
#include <libgimp/gimpui.h>
#include "libgimp/stdplugins-intl.h"
#define VERSION "v0.60"
/* Some useful macros */
#ifdef DEBUG
#define DEBUG_PRINT(x) printf x
#else
#define DEBUG_PRINT(x)
#endif
/*#define TIMINGS*/
#define PLUG_IN_PROC "plug-in-newsprint"
#define PLUG_IN_BINARY "newsprint"
#define PLUG_IN_ROLE "gimp-newsprint"
#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_SIZE (2 * SPOT_PREVIEW_SZ + 1)
#define PREVIEW_OVERSAMPLE 3
#define ISNEG(x) (((x) < 0)? 1 : 0)
#define DEG2RAD(d) ((d) * G_PI / 180)
#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]
/* colorspaces we can separate to: */
#define CS_GREY 0
#define CS_RGB 1
#define CS_CMYK 2
#define CS_LUMINANCE 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 referred 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 (G_N_ELEMENTS (spotfn_list))
#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 colorspace; /* 0: RGB, 1: CMYK, 2: Luminance */
gint k_pullout; /* percentage of black to pull out */
/* grey screen (only used if grayscale 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
{
gdouble input_spi; /* input samples per inch */
gdouble output_lpi; /* desired output lines per inch */
gboolean lock_channels; /* changes to one channel affect all */
} NewsprintUIValues;
/* 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 *combo; /* 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 *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 colorspace */
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 Luminance */
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;
/* 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_("R_ed"),
&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_("C_yan"),
&pvals.red_ang,
&pvals.red_spotfn,
&factory_defaults.red_ang,
&factory_defaults.red_spotfn
},
{
N_("Magen_ta"),
&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 luminance_tmpl[] =
{
{
N_("Luminance"),
&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 colorspace, and gives an array of
* channel templates for that colorspace */
static const chan_tmpl *cspace_chan_tmpl[] =
{
grey_tmpl,
rgb_tmpl,
cmyk_tmpl,
luminance_tmpl
};
#define NCHANS(x) ((sizeof(x) / sizeof(chan_tmpl)) - 1)
/* cspace_nchans gives a quick way of finding the number of channels
* in a colorspace. 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 (luminance_tmpl)
};
/* Declare local functions. */
static void query (void);
static void run (const gchar *name,
gint nparams,
const GimpParam *param,
gint *nreturn_vals,
GimpParam **return_vals);
static gboolean newsprint_dialog (GimpDrawable *drawable);
static void newsprint_cspace_update (GtkWidget *widget,
gpointer data);
static void newsprint_menu_callback (GtkWidget *widget,
gpointer data);
static void angle_callback (GtkAdjustment *adjustment,
gpointer data);
static void lpi_callback (GtkAdjustment *adjustment,
gpointer data);
static void spi_callback (GtkAdjustment *adjustment,
gpointer data);
static void cellsize_callback (GtkAdjustment *adjustment,
gpointer data);
static void newsprint_defaults_callback (GtkWidget *widget,
gpointer data);
static void newsprint (GimpDrawable *drawable,
GimpPreview *preview);
static guchar * spot2thresh (gint type,
gint width);
static void preview_update (channel_st *st);
const GimpPlugInInfo PLUG_IN_INFO =
{
NULL, /* init_proc */
NULL, /* quit_proc */
query, /* query_proc */
run /* run_proc */
};
/***** Functions *****/
MAIN ()
static void
query (void)
{
static const GimpParamDef args[]=
{
{ GIMP_PDB_INT32, "run-mode", "The run mode { RUN-INTERACTIVE (0), RUN-NONINTERACTIVE (1) }" },
{ 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 { GRAYSCALE (0), RGB (1), CMYK (2), LUMINANCE (3) }" },
{ 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 { DOTS (0), LINES (1), DIAMONDS (2), EUCLIDIAN-DOT (3), PS-DIAMONDS (4) }" },
{ 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" }
};
gimp_install_procedure (PLUG_IN_PROC,
N_("Halftone the image to give 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_("Newsprin_t..."),
"RGB*, GRAY*",
GIMP_PLUGIN,
G_N_ELEMENTS (args), 0,
args, NULL);
gimp_plugin_menu_register (PLUG_IN_PROC, "<Image>/Filters/Distorts");
}
static void
run (const gchar *name,
gint nparams,
const GimpParam *param,
gint *nreturn_vals,
GimpParam **return_vals)
{
static GimpParam values[1];
GimpDrawable *drawable;
GimpRunMode run_mode;
GimpPDBStatusType status = GIMP_PDB_SUCCESS;
run_mode = param[0].data.d_int32;
INIT_I18N ();
*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:
/* Possibly retrieve data */
gimp_get_data (PLUG_IN_PROC, &pvals);
gimp_get_data (PLUG_IN_PROC "-ui", &pvals_ui);
/* First acquire information with a dialog */
if (! newsprint_dialog (drawable))
{
gimp_drawable_detach (drawable);
return;
}
break;
case GIMP_RUN_NONINTERACTIVE:
/* 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.colorspace = 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.colorspace) ||
pvals.k_pullout < 0 || pvals.k_pullout > 100)
{
status = GIMP_PDB_CALLING_ERROR;
}
break;
case GIMP_RUN_WITH_LAST_VALS:
/* Possibly retrieve data */
gimp_get_data (PLUG_IN_PROC, &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 (_("Newsprint"));
/* set the tile cache size */
gimp_tile_cache_ntiles (TILE_CACHE_SIZE);
/* run the newsprint effect */
newsprint (drawable, NULL);
if (run_mode != GIMP_RUN_NONINTERACTIVE)
gimp_displays_flush ();
/* Store data */
if (run_mode == GIMP_RUN_INTERACTIVE)
{
gimp_set_data (PLUG_IN_PROC,
&pvals, sizeof (NewsprintValues));
gimp_set_data (PLUG_IN_PROC "-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 = gimp_preview_area_new ();
gtk_widget_set_size_request (preview,
PREVIEW_SIZE, PREVIEW_SIZE);
gtk_widget_show (preview);
g_signal_connect_swapped (preview, "size-allocate",
G_CALLBACK (preview_update), st);
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 rgb[3 * PREVIEW_SIZE * PREVIEW_SIZE ];
/* 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 < PREVIEW_SIZE ; y++)
{
for (x = 0; x < PREVIEW_SIZE ; 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)
{
rgb[(y*PREVIEW_SIZE+x)*3+0] = 0;
rgb[(y*PREVIEW_SIZE+x)*3+1] = 0;
rgb[(y*PREVIEW_SIZE+x)*3+2] = 0xff;
}
else
{
rgb[(y*PREVIEW_SIZE+x)*3+0] = sum;
rgb[(y*PREVIEW_SIZE+x)*3+1] = sum;
rgb[(y*PREVIEW_SIZE+x)*3+2] = sum;
}
}
}
/* redraw preview widget */
gimp_preview_area_draw (GIMP_PREVIEW_AREA (prev->widget),
0, 0, PREVIEW_SIZE, PREVIEW_SIZE,
GIMP_RGB_IMAGE,
rgb,
PREVIEW_SIZE * 3);
g_snprintf (pct, sizeof (pct), "%2d%%",
(int) RINT (spotfn_list[sfn].prev_lvl[i] * 100));
gtk_label_set_text (GTK_LABEL(prev->label), pct);
}
}
static void
newsprint_menu_callback (GtkWidget *widget,
gpointer data)
{
channel_st *st = data;
gint value;
static gboolean in_progress = FALSE;
/* We shouldn't need recursion protection, but if lock_channels is
* set, and gimp_int_combo_box_set_active ever generates a "changed"
* signal, then we'll get back here.
*/
if (in_progress)
{
printf ("newsprint_menu_callback: unexpected recursion: can't happen\n");
return;
}
in_progress = TRUE;
gimp_int_combo_box_get_active (GIMP_INT_COMBO_BOX (widget), &value);
*(st->spotfn_num) = value;
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 old_value;
while (c != st)
{
gimp_int_combo_box_set_active (GIMP_INT_COMBO_BOX (c->combo), value);
old_value = *(c->spotfn_num);
*(c->spotfn_num) = value;
if (old_value != value)
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 = g_object_get_data (G_OBJECT (adjustment), "angle");
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
lpi_callback (GtkAdjustment *adjustment,
gpointer data)
{
NewsprintDialog_st *st = data;
gimp_double_adjustment_update (adjustment, &pvals_ui.output_lpi);
g_signal_handlers_block_by_func (st->cellsize,
cellsize_callback,
data);
gtk_adjustment_set_value (GTK_ADJUSTMENT (st->cellsize),
pvals_ui.input_spi / pvals_ui.output_lpi);
g_signal_handlers_unblock_by_func (st->cellsize,
cellsize_callback,
data);
}
static void
spi_callback (GtkAdjustment *adjustment,
gpointer data)
{
NewsprintDialog_st *st = data;
gimp_double_adjustment_update (adjustment, &pvals_ui.input_spi);
g_signal_handlers_block_by_func (st->output_lpi,
lpi_callback,
data);
gtk_adjustment_set_value (GTK_ADJUSTMENT (st->output_lpi),
pvals_ui.input_spi / pvals.cell_width);
g_signal_handlers_unblock_by_func (st->output_lpi,
lpi_callback,
data);
}
static void
cellsize_callback (GtkAdjustment *adjustment,
gpointer data)
{
NewsprintDialog_st *st = data;
gimp_int_adjustment_update (adjustment, &pvals.cell_width);
g_signal_handlers_block_by_func (st->output_lpi,
lpi_callback,
data);
gtk_adjustment_set_value (GTK_ADJUSTMENT (st->output_lpi),
pvals_ui.input_spi / pvals.cell_width);
g_signal_handlers_unblock_by_func (st->output_lpi,
lpi_callback,
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 colorspace, reset its channel info */
for (cspace = 0; cspace < NUM_CS; cspace++)
{
chst = st->chst[cspace];
ct = cspace_chan_tmpl[cspace];
/* skip this colorspace 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);
gimp_int_combo_box_set_active (GIMP_INT_COMBO_BOX (chst[c]->combo),
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 *preview)
{
GtkSizeGroup *group;
GtkWidget *table;
GtkWidget *hbox;
GtkWidget *hbox2;
GtkWidget *abox;
GtkWidget *label;
spot_info_t *sf;
channel_st *chst;
gint i;
/* create the channel state record */
chst = new_preview (ct->spotfn);
chst->vbox = gtk_box_new (GTK_ORIENTATION_VERTICAL, 6);
gtk_container_set_border_width (GTK_CONTAINER (chst->vbox), 12);
table = gtk_table_new (1, 3, FALSE);
gtk_table_set_col_spacings (GTK_TABLE (table), 6);
gtk_box_pack_start (GTK_BOX (chst->vbox), table, FALSE, FALSE, 0);
gtk_widget_show (table);
group = gtk_size_group_new (GTK_SIZE_GROUP_HORIZONTAL);
/* 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);
g_object_set_data (G_OBJECT (chst->angle_adj), "angle", ct->angle);
gtk_size_group_add_widget (group, GIMP_SCALE_ENTRY_LABEL (chst->angle_adj));
g_object_unref (group);
g_signal_connect (chst->angle_adj, "value-changed",
G_CALLBACK (angle_callback),
chst);
g_signal_connect_swapped (chst->angle_adj, "value-changed",
G_CALLBACK (gimp_preview_invalidate),
preview);
/* spot function popup */
hbox = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 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_box_new (GTK_ORIENTATION_HORIZONTAL, 6);
gtk_container_add (GTK_CONTAINER (abox), hbox2);
gtk_widget_show (hbox2);
label = gtk_label_new_with_mnemonic (_("_Spot function:"));
gtk_misc_set_alignment (GTK_MISC (label), 0.0, 0.5);
gtk_box_pack_start (GTK_BOX (hbox2), label, FALSE, FALSE, 0);
gtk_widget_show (label);
gtk_size_group_add_widget (group, label);
chst->combo = g_object_new (GIMP_TYPE_INT_COMBO_BOX, NULL);
for (sf = spotfn_list, i = 0; sf->name; sf++, i++)
gimp_int_combo_box_append (GIMP_INT_COMBO_BOX (chst->combo),
GIMP_INT_STORE_VALUE, i,
GIMP_INT_STORE_LABEL, gettext (sf->name),
-1);
gimp_int_combo_box_set_active (GIMP_INT_COMBO_BOX (chst->combo),
*ct->spotfn);
g_signal_connect (chst->combo, "changed",
G_CALLBACK (newsprint_menu_callback),
chst);
g_signal_connect_swapped (chst->combo, "changed",
G_CALLBACK (gimp_preview_invalidate),
preview);
gtk_box_pack_start (GTK_BOX (hbox2), chst->combo, FALSE, FALSE, 0);
gtk_widget_show (chst->combo);
/* 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));
/* 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 "colorspace", and fill in
* the respective channel state fields in "st". */
static void
gen_channels (NewsprintDialog_st *st,
gint colorspace,
GtkWidget *preview)
{
const chan_tmpl *ct;
channel_st **chst;
channel_st *base = NULL;
gint i;
chst = st->chst[colorspace];
ct = cspace_chan_tmpl[colorspace];
i = 0;
st->channel_notebook[colorspace] = gtk_notebook_new ();
gtk_box_pack_start (GTK_BOX (st->vbox), st->channel_notebook[colorspace],
FALSE, FALSE, 0);
gtk_box_reorder_child (GTK_BOX (st->vbox),
st->channel_notebook[colorspace], 3);
gtk_widget_show (st->channel_notebook[colorspace]);
while (ct->name)
{
chst[i] = new_channel (ct, preview);
if (i)
chst[i-1]->next = chst[i];
else
base = chst[i];
gtk_notebook_append_page (GTK_NOTEBOOK (st->channel_notebook[colorspace]),
chst[i]->vbox,
gtk_label_new_with_mnemonic (gettext (ct->name)));
gtk_widget_show (chst[i]->vbox);
i++;
ct++;
}
/* make the list circular */
chst[i-1]->next = base;
}
static gboolean
newsprint_dialog (GimpDrawable *drawable)
{
/* widgets we need from callbacks stored here */
NewsprintDialog_st st;
GtkWidget *dialog;
GtkWidget *paned;
GtkWidget *vbox;
GtkWidget *hbox;
GtkWidget *preview;
GtkWidget *frame;
GtkWidget *table;
GtkObject *adj;
GSList *group = NULL;
gint bpp;
gint i;
gdouble xres, yres;
gboolean run;
gimp_ui_init (PLUG_IN_BINARY, TRUE);
/* flag values to say we haven't filled these channel
* states in yet */
for(i=0; i<NUM_CS; i++)
st.chst[i][0] = NULL;
/* need to know the bpp, so we can tell if we're doing
* RGB/CMYK or grey style of dialog box */
bpp = gimp_drawable_bpp (drawable->drawable_id);
if (gimp_drawable_has_alpha (drawable->drawable_id))
bpp--;
/* force grayscale if it's the only thing we can do */
if (bpp == 1)
{
pvals.colorspace = CS_GREY;
}
else
{
if (pvals.colorspace == CS_GREY)
pvals.colorspace = CS_RGB;
}
dialog = gimp_dialog_new (_("Newsprint"), PLUG_IN_ROLE,
NULL, 0,
gimp_standard_help_func, PLUG_IN_PROC,
GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL,
GTK_STOCK_OK, GTK_RESPONSE_OK,
NULL);
gtk_dialog_set_alternative_button_order (GTK_DIALOG (dialog),
GTK_RESPONSE_OK,
GTK_RESPONSE_CANCEL,
-1);
gimp_window_set_transient (GTK_WINDOW (dialog));
paned = gtk_paned_new (GTK_ORIENTATION_HORIZONTAL);
gtk_container_set_border_width (GTK_CONTAINER (paned), 12);
gtk_box_pack_start (GTK_BOX (gtk_dialog_get_content_area (GTK_DIALOG (dialog))),
paned, TRUE, TRUE, 0);
gtk_widget_show (paned);
hbox = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 0);
gtk_paned_pack1 (GTK_PANED (paned), hbox, TRUE, FALSE);
gtk_widget_show (hbox);
vbox = gtk_box_new (GTK_ORIENTATION_VERTICAL, 0);
gtk_container_set_border_width (GTK_CONTAINER (vbox), 6);
gtk_box_pack_end (GTK_BOX (hbox), vbox, FALSE, FALSE, 0);
gtk_widget_show (vbox);
preview = gimp_drawable_preview_new (drawable, NULL);
gtk_box_pack_start (GTK_BOX (hbox), preview, TRUE, TRUE, 0);
gtk_widget_show (preview);
g_signal_connect_swapped (preview, "invalidated",
G_CALLBACK (newsprint),
drawable);
hbox = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 0);
gtk_paned_pack2 (GTK_PANED (paned), hbox, FALSE, FALSE);
gtk_widget_show (hbox);
vbox = gtk_box_new (GTK_ORIENTATION_VERTICAL, 0);
gtk_container_set_border_width (GTK_CONTAINER (vbox), 6);
gtk_box_pack_start (GTK_BOX (hbox), vbox, FALSE, FALSE, 0);
gtk_widget_show (vbox);
vbox = gtk_box_new (GTK_ORIENTATION_VERTICAL, 12);
gtk_box_pack_start (GTK_BOX (hbox), vbox, TRUE, TRUE, 0);
gtk_widget_show (vbox);
/* resolution settings */
frame = gimp_frame_new (_("Resolution"));
gtk_box_pack_start (GTK_BOX (vbox), frame, FALSE, FALSE, 0);
gtk_widget_show (frame);
table = gtk_table_new (3, 3, FALSE);
gtk_table_set_col_spacings (GTK_TABLE (table), 6);
gtk_table_set_row_spacings (GTK_TABLE (table), 6);
gtk_container_add (GTK_CONTAINER (frame), table);
gtk_widget_show (table);
gimp_image_get_resolution (gimp_item_get_image (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, 7,
pvals_ui.input_spi,
1.0, 1200.0, 1.0, 10.0, 0,
FALSE, GIMP_MIN_RESOLUTION, GIMP_MAX_RESOLUTION,
NULL, NULL);
g_signal_connect (st.input_spi, "value-changed",
G_CALLBACK (spi_callback),
&st);
g_signal_connect_swapped (st.input_spi, "value-changed",
G_CALLBACK (gimp_preview_invalidate),
preview);
st.output_lpi =
gimp_scale_entry_new (GTK_TABLE (table), 0, 1,
_("O_utput LPI:"), SCALE_WIDTH, 7,
pvals_ui.output_lpi,
1.0, 1200.0, 1.0, 10.0, 1,
FALSE, GIMP_MIN_RESOLUTION, GIMP_MAX_RESOLUTION,
NULL, NULL);
g_signal_connect (st.output_lpi, "value-changed",
G_CALLBACK (lpi_callback),
&st);
g_signal_connect_swapped (st.output_lpi, "value-changed",
G_CALLBACK (gimp_preview_invalidate),
preview);
st.cellsize = gimp_scale_entry_new (GTK_TABLE (table), 0, 2,
_("C_ell size:"), SCALE_WIDTH, 7,
pvals.cell_width,
3.0, 100.0, 1.0, 5.0, 0,
FALSE, 3.0, GIMP_MAX_IMAGE_SIZE,
NULL, NULL);
g_signal_connect (st.cellsize, "value-changed",
G_CALLBACK (cellsize_callback),
&st);
g_signal_connect_swapped (st.cellsize, "value-changed",
G_CALLBACK (gimp_preview_invalidate),
preview);
/* screen settings */
frame = gimp_frame_new (_("Screen"));
gtk_box_pack_start (GTK_BOX (vbox), frame, FALSE, FALSE, 0);
st.vbox = gtk_box_new (GTK_ORIENTATION_VERTICAL, 12);
gtk_container_add (GTK_CONTAINER (frame), st.vbox);
/* optional portion begins */
if (bpp != 1)
{
GtkWidget *hbox;
GtkWidget *label;
GtkWidget *button;
GtkWidget *toggle;
st.pull_table = gtk_table_new (1, 3, FALSE);
gtk_table_set_col_spacings (GTK_TABLE (st.pull_table), 6);
/* black pullout */
st.pull = gimp_scale_entry_new (GTK_TABLE (st.pull_table), 0, 0,
_("B_lack pullout (%):"), SCALE_WIDTH, 0,
pvals.k_pullout,
0, 100, 1, 10, 0,
TRUE, 0, 0,
NULL, NULL);
gtk_widget_set_sensitive (st.pull_table, (pvals.colorspace == CS_CMYK));
gtk_widget_show (st.pull_table);
g_signal_connect (st.pull, "value-changed",
G_CALLBACK (gimp_int_adjustment_update),
&pvals.k_pullout);
g_signal_connect_swapped (st.pull, "value-changed",
G_CALLBACK (gimp_preview_invalidate),
preview);
/* RGB / CMYK / Luminance select */
hbox = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 6);
gtk_box_pack_start (GTK_BOX (st.vbox), hbox, FALSE, FALSE, 0);
/* 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_mnemonic(group, _("_RGB"));
group = gtk_radio_button_get_group (GTK_RADIO_BUTTON (toggle));
gtk_box_pack_start (GTK_BOX (hbox), toggle, TRUE, TRUE, 0);
gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (toggle),
(pvals.colorspace == CS_RGB));
gtk_widget_show (toggle);
g_object_set_data (G_OBJECT (toggle), "dialog", &st);
g_object_set_data (G_OBJECT (toggle), "preview", preview);
g_signal_connect (toggle, "toggled",
G_CALLBACK (newsprint_cspace_update),
GINT_TO_POINTER (CS_RGB));
g_signal_connect_swapped (toggle, "toggled",
G_CALLBACK (gimp_preview_invalidate),
preview);
toggle = gtk_radio_button_new_with_mnemonic (group, _("C_MYK"));
group = gtk_radio_button_get_group (GTK_RADIO_BUTTON (toggle));
gtk_box_pack_start (GTK_BOX (hbox), toggle, TRUE, TRUE, 0);
gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (toggle),
(pvals.colorspace == CS_CMYK));
gtk_widget_show (toggle);
g_object_set_data (G_OBJECT (toggle), "dialog", &st);
g_object_set_data (G_OBJECT (toggle), "preview", preview);
g_signal_connect (toggle, "toggled",
G_CALLBACK (newsprint_cspace_update),
GINT_TO_POINTER (CS_CMYK));
g_signal_connect_swapped (toggle, "toggled",
G_CALLBACK (gimp_preview_invalidate),
preview);
toggle = gtk_radio_button_new_with_mnemonic (group, _("I_ntensity"));
group = gtk_radio_button_get_group (GTK_RADIO_BUTTON (toggle));
gtk_box_pack_start (GTK_BOX (hbox), toggle, TRUE, TRUE, 0);
gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (toggle),
(pvals.colorspace == CS_LUMINANCE));
gtk_widget_show (toggle);
g_object_set_data (G_OBJECT (toggle), "dialog", &st);
g_object_set_data (G_OBJECT (toggle), "preview", preview);
g_signal_connect (toggle, "toggled",
G_CALLBACK (newsprint_cspace_update),
GINT_TO_POINTER (CS_LUMINANCE));
g_signal_connect_swapped (toggle, "toggled",
G_CALLBACK (gimp_preview_invalidate),
preview);
gtk_widget_show (hbox);
/* channel lock & factory defaults button */
hbox = gtk_button_box_new (GTK_ORIENTATION_HORIZONTAL);
gtk_box_set_spacing (GTK_BOX (hbox), 6);
gtk_box_pack_start (GTK_BOX (st.vbox), hbox, FALSE, FALSE, 0);
gtk_widget_show (hbox);
toggle = gtk_check_button_new_with_mnemonic (_("_Lock channels"));
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);
g_signal_connect (toggle, "toggled",
G_CALLBACK (gimp_toggle_button_update),
&pvals_ui.lock_channels);
g_signal_connect_swapped (toggle, "toggled",
G_CALLBACK (gimp_preview_invalidate),
preview);
button = gtk_button_new_with_mnemonic (_("_Factory Defaults"));
gtk_box_pack_end (GTK_BOX (hbox), button, FALSE, FALSE, 0);
gtk_widget_show (button);
g_signal_connect (button, "clicked",
G_CALLBACK (newsprint_defaults_callback),
&st);
g_signal_connect_swapped (button, "clicked",
G_CALLBACK (gimp_preview_invalidate),
preview);
}
/* Make the channels appropriate for this colorspace 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.colorspace][0])
{
gen_channels (&st, pvals.colorspace, preview);
}
gtk_widget_show (st.vbox);
gtk_widget_show (frame);
/* anti-alias control */
frame = gimp_frame_new (_("Antialiasing"));
gtk_box_pack_start (GTK_BOX (vbox), frame, FALSE, FALSE, 0);
table = gtk_table_new (1, 3, FALSE);
gtk_table_set_col_spacings (GTK_TABLE (table), 6);
gtk_container_add (GTK_CONTAINER (frame), table);
adj = gimp_scale_entry_new (GTK_TABLE (table), 0, 0,
_("O_versample:"), SCALE_WIDTH, 0,
pvals.oversample,
1.0, 15.0, 1.0, 5.0, 0,
TRUE, 0, 0,
NULL, NULL);
g_signal_connect (adj, "value-changed",
G_CALLBACK (gimp_int_adjustment_update),
&pvals.oversample);
g_signal_connect_swapped (adj, "value-changed",
G_CALLBACK (gimp_preview_invalidate),
preview);
gtk_widget_show (table);
gtk_widget_show (frame);
gtk_widget_show (dialog);
preview_update(st.chst[pvals.colorspace][0]);
run = (gimp_dialog_run (GIMP_DIALOG (dialog)) == GTK_RESPONSE_OK);
gtk_widget_destroy (dialog);
return run;
}
/* Newsprint interface functions */
static void
newsprint_cspace_update (GtkWidget *widget,
gpointer data)
{
NewsprintDialog_st *st;
gint new_cs = GPOINTER_TO_INT (data);
gint old_cs = pvals.colorspace;
GtkWidget *preview;
st = g_object_get_data (G_OBJECT (widget), "dialog");
preview = g_object_get_data (G_OBJECT (widget), "preview");
if (!st)
printf ("newsprint: cspace_update: no state, can't happen!\n");
if (st)
{
gboolean active = gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (widget));
/* the CMYK widget looks after the black pullout widget */
if (new_cs == CS_CMYK)
{
gtk_widget_set_sensitive (st->pull_table, active);
}
/* if we're not activate, then there's nothing more to do */
if (! active)
return;
pvals.colorspace = new_cs;
/* make sure we have the necessary channels for the new
* colorspace */
if (!st->chst[new_cs][0])
gen_channels (st, new_cs, preview);
/* 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);
preview_update (st->chst[new_cs][0]);
}
}
/*
* Newsprint Effect
*/
/*************************************************************/
/* Spot functions */
/* Spot functions define the order in which pixels should be whitened
* as a cell lightened in color. 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 two functions are implementations of algorithms
* described in "Postscript Screening: Adobe Accurate Screens"
* (Adobe Press, 1992) by Peter Fink.
*/
/* 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 */
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 */
}
/*************************************************************/
/* 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 noticeable 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 color 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,
GimpPreview *preview)
{
GimpPixelRgn src_rgn, dest_rgn;
guchar *src_row, *dest_row;
guchar *src, *dest;
guchar *thresh[4] = { NULL, NULL, NULL, NULL };
gdouble r;
gdouble theta;
gdouble rot[4];
gint bpp, color_bpp;
gint has_alpha;
gint b;
gint tile_width;
gint tile_height;
gint width;
gint row, col;
gint x, y, x_step, y_step;
gint x1, y1, x2, y2;
gint preview_width, preview_height;
gint rx, ry;
gint progress, max_progress;
gint oversample;
gint colorspace;
gpointer pr;
gint w002;
guchar *preview_buffer = NULL;
#ifdef TIMINGS
GTimer *timer = g_timer_new ();
#endif
width = pvals.cell_width;
if (width < 0)
width = -width;
if (width < 1)
width = 1;
oversample = pvals.oversample;
width *= oversample;
tile_width = gimp_tile_width ();
tile_height = gimp_tile_height ();
bpp = gimp_drawable_bpp (drawable->drawable_id);
if (preview)
{
gimp_preview_get_position (preview, &x1, &y1);
gimp_preview_get_size (preview, &preview_width, &preview_height);
x2 = x1 + preview_width;
y2 = y1 + preview_height;
preview_buffer = g_new (guchar, preview_width * preview_height * bpp);
}
else
{
gimp_drawable_mask_bounds (drawable->drawable_id, &x1, &y1, &x2, &y2);
}
has_alpha = gimp_drawable_has_alpha (drawable->drawable_id);
color_bpp = has_alpha ? bpp-1 : bpp;
colorspace= pvals.colorspace;
if (color_bpp == 1)
{
colorspace = CS_GREY;
}
else
{
if (colorspace == CS_GREY)
colorspace = 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 (color_bpp == 1 || colorspace == CS_LUMINANCE)
{
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;
ASRT (gf);
spotfn_list[gf].thresh = spot2thresh (gf, width);
thresh[1] = spotfn_list[gf].thresh;
ASRT (bf);
spotfn_list[bf].thresh = spot2thresh (bf, width);
thresh[2] = spotfn_list[bf].thresh;
if (colorspace == CS_CMYK)
{
rot[3] = DEG2RAD (pvals.gry_ang);
gf = pvals.gry_spotfn;
ASRT (gf);
spotfn_list[gf].thresh = spot2thresh (gf, width);
thresh[3] = spotfn_list[gf].thresh;
}
}
/* Initialize progress */
progress = 0;
max_progress = (x2 - x1) * (y2 - y1);
for (y = y1; y < y2; y += tile_height - (y % tile_height))
{
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_height - (y % tile_height);
/* 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;
if (preview)
dest_row = preview_buffer +
((src_rgn.y - y1) * preview_width + src_rgn.x - x1) * bpp;
else
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 < color_bpp; b++)
data[b] = src[b];
/* do color space conversion */
switch (colorspace)
{
case CS_CMYK:
{
gint r,g,b,k;
r = data[0];
g = data[1];
b = data[2];
k = pvals.k_pullout;
gimp_rgb_to_cmyk_int (&r, &g, &b, &k);
data[0] = r;
data[1] = g;
data[2] = b;
data[3] = k;
}
break;
case CS_LUMINANCE:
data[3] = data[0]; /* save orig for later */
data[0] = GIMP_RGB_LUMINANCE (data[0],
data[1],
data[2]) + 0.5;
break;
default:
break;
}
for (b = 0; b < cspace_nchans[colorspace]; 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[color_bpp] = src[color_bpp];
/* re-pack the colors into RGB */
switch (colorspace)
{
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_LUMINANCE:
if (has_alpha)
{
dest[color_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 < color_bpp; b++)
dest[b] = data[b];
src += src_rgn.bpp;
dest += dest_rgn.bpp;
}
src_row += src_rgn.rowstride;
if (preview)
dest_row += preview_width * bpp;
else
dest_row += dest_rgn.rowstride;
}
/* Update progress */
progress += src_rgn.w * src_rgn.h;
if (!preview)
gimp_progress_update ((double) progress / (double) max_progress);
}
}
}
#ifdef TIMINGS
g_printerr ("%f seconds\n", g_timer_elapsed (timer));
g_timer_destroy (timer);
#endif
/*
* Note: the tresh array should *NOT* be freed.
* Its values will be reused anyway so this is NOT a memory leak.
* Well it is, but only the first time, so it doesn't matter.
*/
if (preview)
{
gimp_preview_draw_buffer (preview, preview_buffer, preview_width * bpp);
g_free (preview_buffer);
}
else
{
gimp_progress_update (1.0);
/* 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));
}
}