From 6607600c04f3dd999b08ce15f4681e5153afeba9 Mon Sep 17 00:00:00 2001 From: Tony Cook Date: Wed, 29 Aug 2001 09:27:35 +0000 Subject: [PATCH] implement fountain fills similar to most paint programs minor bug fixes --- Changes | 11 + Imager.pm | 223 +++++++++++ Imager.xs | 96 ++++- MANIFEST | 9 +- Makefile.PL | 2 +- color.c | 124 ++++++ filters.c | 833 ++++++++++++++++++++++++++++++++++++++++- image.c | 8 +- image.h | 49 +++ lib/Imager/Fountain.pm | 393 +++++++++++++++++++ t/t61filters.t | 33 +- testimg/gimpgrad | 4 + 12 files changed, 1763 insertions(+), 22 deletions(-) create mode 100644 color.c create mode 100644 lib/Imager/Fountain.pm create mode 100644 testimg/gimpgrad diff --git a/Changes b/Changes index caa23e7b..1ed06043 100644 --- a/Changes +++ b/Changes @@ -480,6 +480,17 @@ Revision history for Perl extension Imager. - fixed some problems in jpeg handling from the exp_represent merge - fixed buffer flushing for wiol jpeg code - added some tests that will hopefully catch it in the future + - added the OO interfaces to the mosaic, bumpmap, postlevels and + watermark filters, and documented them + - fixed a sample size conversion problem in i_gpixf_d() etc. + - added simple color representation conversion functions (used + in i_fountain().) + - added the fountain filter: + - creates gradients similar to paint software + - 90% support for GIMP gradient files + - OO interface and documentation + - Imager::Fountain for building/loading fill definitions + - named value translation for filters ================================================================= diff --git a/Imager.pm b/Imager.pm index a091901b..46b636f2 100644 --- a/Imager.pm +++ b/Imager.pm @@ -168,6 +168,16 @@ BEGIN { $DEBUG=0; + # the members of the subhashes under %filters are: + # callseq - a list of the parameters to the underlying filter in the + # order they are passed + # callsub - a code ref that takes a named parameter list and calls the + # underlying filter + # defaults - a hash of default values + # names - defines names for value of given parameters so if the names + # field is foo=> { bar=>1 }, and the user supplies "bar" as the + # foo parameter, the filter will receive 1 for the foo + # parameter $filters{contrast}={ callseq => ['image','intensity'], callsub => sub { my %hsh=@_; i_contrast($hsh{image},$hsh{intensity}); } @@ -258,6 +268,47 @@ BEGIN { $hsh{pixdiff}); }, }; + $filters{fountain} = + { + callseq => [ qw(image xa ya xb yb ftype repeat combine super_sample ssample_param segments) ], + names => { + ftype => { linear => 0, + bilinear => 1, + radial => 2, + radial_square => 3, + revolution => 4, + conical => 5 }, + repeat => { none => 0, + sawtooth => 1, + triangle => 2, + saw_both => 3, + tri_both => 4, + }, + super_sample => { + none => 0, + grid => 1, + random => 2, + circle => 3, + }, + }, + defaults => { ftype => 0, repeat => 0, combine => 0, + super_sample => 0, ssample_param => 4, + segments=>[ + [ 0, 0.5, 1, + Imager::Color->new(0,0,0), + Imager::Color->new(255, 255, 255), + 0, 0, + ], + ], + }, + callsub => + sub { + my %hsh = @_; + i_fountain($hsh{image}, $hsh{xa}, $hsh{ya}, $hsh{xb}, $hsh{yb}, + $hsh{ftype}, $hsh{repeat}, $hsh{combine}, $hsh{super_sample}, + $hsh{ssample_param}, $hsh{segments}); + }, + }; $FORMATGUESS=\&def_guess_type; } @@ -1171,6 +1222,14 @@ sub filter { $self->{ERRSTR}='type parameter not matching any filter'; return undef; } + if ($filters{$input{type}}{names}) { + my $names = $filters{$input{type}}{names}; + for my $name (keys %$names) { + if (defined $input{$name} && exists $names->{$name}{$input{$name}}) { + $input{$name} = $names->{$name}{$input{$name}}; + } + } + } if (defined($filters{$input{type}}{defaults})) { %hsh=('image',$self->{IMG},%{$filters{$input{type}}{defaults}},%input); } else { @@ -2824,9 +2883,12 @@ source. bumpmap bump elevation(0) lightx lighty st(2) contrast intensity conv coef + fountain xa ya xb yb ftype(linear) repeat(none) combine(0) + super_sample(none) ssample_param(4) segments(see below) gaussian stddev gradgen xo yo colors dist hardinvert + mosaic size(20) noise amount(3) subtype(0) postlevels levels(10) radnoise xo(100) yo(100) ascale(17.0) rscale(0.02) @@ -2864,6 +2926,163 @@ will reduce the contrast. performs 2 1-dimensional convolutions on the image using the values from I. I should be have an odd length. +=item fountain + +renders a fountain fill, similar to the gradient tool in most paint +software. The default fill is a linear fill from opaque black to +opaque white. The points A(xa, ya) and B(xb, yb) control the way the +fill is performed, depending on the ftype parameter: + +=over + +=item linear + +the fill ramps from A through to B. + +=item bilinear + +the fill ramps in both directions from A, where AB defines the length +of the gradient. + +=item radial + +A is the center of a circle, and B is a point on it's circumference. +The fill ramps from the center out to the circumference. + +=item radial_square + +A is the center of a square and B is the center of one of it's sides. +This can be used to rotate the square. The fill ramps out to the +edges of the square. + +=item revolution + +A is the centre of a circle and B is a point on it's circumference. B +marks the 0 and 360 point on the circle, with the fill ramping +clockwise. + +=item conical + +A is the center of a circle and B is a point on it's circumference. B +marks the 0 and point on the circle, with the fill ramping in both +directions to meet opposite. + +=back + +The I option controls how the fill is repeated for some +Is after it leaves the AB range: + +=over + +=item none + +no repeats, points outside of each range are treated as if they were +on the extreme end of that range. + +=item sawtooth + +the fill simply repeats in the positive direction + +=item triangle + +the fill repeats in reverse and then forward and so on, in the +positive direction + +=item saw_both + +the fill repeats in both the positive and negative directions (only +meaningful for a linear fill). + +=item tri_both + +as for triangle, but in the negative direction too (only meaningful +for a linear fill). + +=back + +By default the fill simply overwrites the whole image (unless you have +parts of the range 0 through 1 that aren't covered by a segment), if +any segments of your fill have any transparency, you can set the +I option to 1 to have the fill combined with the existing pixels. + +If your fill has sharp edges, for example between steps if you use +repeat set to 'triangle', you may see some aliased or ragged edges. +You can enable super-sampling which will take extra samples within the +pixel in an attempt anti-alias the fill. + +The possible values for the super_sample option are: + +=over + +=item none + +no super-sampling is done + +=item grid + +a square grid of points are sampled. The number of points sampled is +the square of ceil(0.5 + sqrt(ssample_param)). + +=item random + +a random set of points within the pixel are sampled. This looks +pretty bad for low ssample_param values. + +=item circle + +the points on the radius of a circle within the pixel are sampled. +This seems to produce the best results, but is fairly slow (for now). + +=back + +You can control the level of sampling by setting the ssample_param +option. This is roughly the number of points sampled, but depends on +the type of sampling. + +The segments option is an arrayref of segments. You really should use +the Imager::Fountain class to build your fountain fill. Each segment +is an array ref containing: + +=over + +=item start + +a floating point number between 0 and 1, the start of the range of fill parameters covered by this segment. + +=item middle + +a floating point number between start and end which can be used to +push the color range towards one end of the segment. + +=item end + +a floating point number between 0 and 1, the end of the range of fill +parameters covered by this segment. This should be greater than +start. + +=item c0 + +=item c1 + +The colors at each end of the segment. These can be either +Imager::Color or Imager::Color::Float objects. + +=item segment type + +The type of segment, this controls the way the fill parameter varies +over the segment. 0 for linear, 1 for curved (unimplemented), 2 for +sine, 3 for sphere increasing, 4 for sphere decreasing. + +=item color type + +The way the color varies within the segment, 0 for simple RGB, 1 for +hue increasing and 2 for hue decreasing. + +=back + +Don't forgot to use Imager::Fountain instead of building your own. +Really. It even loads GIMP gradient files. + =item gaussian performs a gaussian blur of the image, using I as the standard @@ -2884,6 +3103,10 @@ for Euclidean squared, and 2 for Manhattan distance. inverts the image, black to white, white to black. All channels are inverted, including the alpha channel if any. +=item mosaic + +produces averaged tiles of the given I. + =item noise adds noise of the given I to the image. If I is diff --git a/Imager.xs b/Imager.xs index bfb39536..0217966f 100644 --- a/Imager.xs +++ b/Imager.xs @@ -1883,9 +1883,99 @@ i_gradgen(im, ...) } i_gradgen(im, num, xo, yo, ival, dmeasure); - - - +void +i_fountain(im, xa, ya, xb, yb, type, repeat, combine, super_sample, ssample_param, segs) + Imager::ImgRaw im + double xa + double ya + double xb + double yb + int type + int repeat + int combine + int super_sample + double ssample_param + PREINIT: + int i, j; + AV *asegs; + AV *aseg; + SV *sv; + int count; + i_fountain_seg *segs; + double work[3]; + int worki[2]; + CODE: + /* Each element of segs must contain: + [ start, middle, end, c0, c1, segtype, colortrans ] + start, middle, end are doubles from 0 to 1 + c0, c1 are Imager::Color::Float or Imager::Color objects + segtype, colortrans are ints + */ + if (!SvROK(ST(10)) || ! SvTYPE(SvRV(ST(10)))) + croak("i_fountain: argument 11 must be an array ref"); + + asegs = (AV *)SvRV(ST(10)); + + count = av_len(asegs)+1; + if (count < 1) + croak("i_fountain must have at least one segment"); + segs = mymalloc(sizeof(i_fountain_seg) * count); + for(i = 0; i 'Imager', diff --git a/color.c b/color.c new file mode 100644 index 00000000..6097cc95 --- /dev/null +++ b/color.c @@ -0,0 +1,124 @@ +#include "image.h" + +/* +=head1 NAME + +color.c - color manipulation functions + +=head1 SYNOPSIS + + i_fcolor color; + i_rgb_to_hsvf(&color); + i_hsv_to_rgbf(&color); + +=head1 DESCRIPTION + +A collection of utility functions for converting between color spaces. + +*/ + +#define EPSILON (1e-8) + +/* +=item i_rgb2hsvf(&color) + +Converts the first 3 channels of color into hue, saturation and value. + +Each value is scaled into the range 0 to 1.0. + +=cut +*/ +void i_rgb_to_hsvf(i_fcolor *color) { + double h, s, v; + double temp; + double Cr, Cg, Cb; + + v = max(max(color->rgb.r, color->rgb.g), color->rgb.b); + temp = min(min(color->rgb.r, color->rgb.g), color->rgb.b); + if (v < EPSILON) + s = 0; + else + s = (v-temp)/v; + if (s == 0) + h = 0; + else { + Cr = (v - color->rgb.r)/(v-temp); + Cg = (v - color->rgb.g)/(v-temp); + Cb = (v - color->rgb.b)/(v-temp); + if (color->rgb.r == v) + h = Cb - Cg; + else if (color->rgb.g == v) + h = 2 + Cr - Cb; + else if (color->rgb.b == v) + h = 4 + Cg - Cr; + h = 60 * h; + if (h < 0) + h += 360; + } + color->channel[0] = h / 360.0; + color->channel[1] = s; + color->channel[2] = v; +} + +/* +=item i_hsv_to_rgbf(&color) + +Convert a HSV value to an RGB value, each value ranges from 0 to 1. + +=cut +*/ + +void i_hsv_to_rgbf(i_fcolor *color) { + double h = color->channel[0]; + double s = color->channel[1]; + double v = color->channel[2]; + + if (color->channel[1] < EPSILON) { + /* ignore h in this case */ + color->rgb.r = color->rgb.g = color->rgb.b = v; + } + else { + int i; + double f, m, n, k; + h = fmod(h, 1.0) * 6; + i = h; + f = h - i; + m = v * (1 - s); + n = v * (1 - s * f); + k = v * (1 - s * (1 - f)); + switch (i) { + case 0: + color->rgb.r = v; color->rgb.g = k; color->rgb.b = m; + break; + case 1: + color->rgb.r = n; color->rgb.g = v; color->rgb.b = m; + break; + case 2: + color->rgb.r = m; color->rgb.g = v; color->rgb.b = k; + break; + case 3: + color->rgb.r = m; color->rgb.g = n; color->rgb.b = v; + break; + case 4: + color->rgb.r = k; color->rgb.g = m; color->rgb.b = v; + break; + case 5: + color->rgb.r = v; color->rgb.g = m; color->rgb.b = n; + break; + } + } +} + +/* +=back + +=head1 AUTHOR + +Tony Cook + +=head1 SEE ALSO + +Imager + +=cut +*/ diff --git a/filters.c b/filters.c index 5747ab4c..4ee39879 100644 --- a/filters.c +++ b/filters.c @@ -835,16 +835,6 @@ i_nearest_color_foo(i_img *im, int num, int *xo, int *yo, i_color *ival, int dme } } - - - - - - - - - - void i_nearest_color(i_img *im, int num, int *xo, int *yo, i_color *oval, int dmeasure) { i_color *ival; @@ -927,3 +917,826 @@ i_nearest_color(i_img *im, int num, int *xo, int *yo, i_color *oval, int dmeasur i_nearest_color_foo(im, num, xo, yo, ival, dmeasure); } + +/* + Keep state information used by each type of fountain fill +*/ +struct fount_state { + /* precalculated for the equation of the line perpendicular to the line AB */ + double lA, lB, lC; + double AB; + double sqrtA2B2; + double mult; + double cos; + double sin; + double theta; + int xa, ya; + void *ssample_data; +}; + +static double linear_fount_f(double x, double y, struct fount_state *state); +static double bilinear_fount_f(double x, double y, struct fount_state *state); +static double radial_fount_f(double x, double y, struct fount_state *state); +static double square_fount_f(double x, double y, struct fount_state *state); +static double revolution_fount_f(double x, double y, + struct fount_state *state); +static double conical_fount_f(double x, double y, struct fount_state *state); + +typedef double (*fount_func)(double, double, struct fount_state *); +static fount_func fount_funcs[] = +{ + linear_fount_f, + bilinear_fount_f, + radial_fount_f, + square_fount_f, + revolution_fount_f, + conical_fount_f, +}; + +static double linear_interp(double pos, i_fountain_seg *seg); +static double sine_interp(double pos, i_fountain_seg *seg); +static double sphereup_interp(double pos, i_fountain_seg *seg); +static double spheredown_interp(double pos, i_fountain_seg *seg); +typedef double (*fount_interp)(double pos, i_fountain_seg *seg); +static fount_interp fount_interps[] = +{ + linear_interp, + linear_interp, + sine_interp, + sphereup_interp, + spheredown_interp, +}; + +static void direct_cinterp(i_fcolor *out, double pos, i_fountain_seg *seg); +static void hue_up_cinterp(i_fcolor *out, double pos, i_fountain_seg *seg); +static void hue_down_cinterp(i_fcolor *out, double pos, i_fountain_seg *seg); +typedef void (*fount_cinterp)(i_fcolor *out, double pos, i_fountain_seg *seg); +static fount_cinterp fount_cinterps[] = +{ + direct_cinterp, + hue_up_cinterp, + hue_down_cinterp, +}; + +typedef double (*fount_repeat)(double v); +static double fount_r_none(double v); +static double fount_r_sawtooth(double v); +static double fount_r_triangle(double v); +static double fount_r_saw_both(double v); +static double fount_r_tri_both(double v); +static fount_repeat fount_repeats[] = +{ + fount_r_none, + fount_r_sawtooth, + fount_r_triangle, + fount_r_saw_both, + fount_r_tri_both, +}; + +static int simple_ssample(i_fcolor *out, double parm, double x, double y, + struct fount_state *state, + fount_func ffunc, fount_repeat rpfunc, + i_fountain_seg *segs, int count); +static int random_ssample(i_fcolor *out, double parm, double x, double y, + struct fount_state *state, + fount_func ffunc, fount_repeat rpfunc, + i_fountain_seg *segs, int count); +static int circle_ssample(i_fcolor *out, double parm, double x, double y, + struct fount_state *state, + fount_func ffunc, fount_repeat rpfunc, + i_fountain_seg *segs, int count); +typedef int (*fount_ssample)(i_fcolor *out, double parm, double x, double y, + struct fount_state *state, + fount_func ffunc, fount_repeat rpfunc, + i_fountain_seg *segs, int count); +static fount_ssample fount_ssamples[] = +{ + NULL, + simple_ssample, + random_ssample, + circle_ssample, +}; + +static int +fount_getat(i_fcolor *out, double x, double y, fount_func ffunc, + fount_repeat rpfunc, struct fount_state *state, + i_fountain_seg *segs, int count); + +#define EPSILON (1e-6) + +/* +=item i_fountain(im, xa, ya, xb, yb, type, repeat, combine, super_sample, ssample_param, count, segs) + +Draws a fountain fill using A(xa, ya) and B(xb, yb) as reference points. + +I controls how the reference points are used: + +=over + +=item i_ft_linear + +linear, where A is 0 and B is 1. + +=item i_ft_bilinear + +linear in both directions from A. + +=item i_ft_radial + +circular, where A is the centre of the fill, and B is a point +on the radius. + +=item i_ft_radial_square + +where A is the centre of the fill and B is the centre of +one side of the square. + +=item i_ft_revolution + +where A is the centre of the fill and B defines the 0/1.0 +angle of the fill. + +=item i_ft_conical + +similar to i_ft_revolution, except that the revolution goes in both +directions + +=back + +I can be one of: + +=over + +=item i_fr_none + +values < 0 are treated as zero, values > 1 are treated as 1. + +=item i_fr_sawtooth + +negative values are treated as 0, positive values are modulo 1.0 + +=item i_fr_triangle + +negative values are treated as zero, if (int)value is odd then the value is treated as 1-(value +mod 1.0), otherwise the same as for sawtooth. + +=item i_fr_saw_both + +like i_fr_sawtooth, except that the sawtooth pattern repeats into +negative values. + +=item i_fr_tri_both + +Like i_fr_triangle, except that negative values are handled as their +absolute values. + +=back + +If combine is non-zero then non-opaque values are combined with the +underlying color. + +I controls super sampling, if any. At some point I'll +probably add a adaptive super-sampler. Current possible values are: + +=over + +=item i_fts_none + +No super-sampling is done. + +=item i_fts_grid + +A square grid of points withing the pixel are sampled. + +=item i_fts_random + +Random points within the pixel are sampled. + +=item i_fts_circle + +Points on the radius of a circle are sampled. This produces fairly +good results, but is fairly slow since sin() and cos() are evaluated +for each point. + +=back + +I is intended to be roughly the number of points +sampled within the pixel. + +I and I define the segments of the fill. + +=cut + +*/ + +void +i_fountain(i_img *im, double xa, double ya, double xb, double yb, + i_fountain_type type, i_fountain_repeat repeat, + int combine, int super_sample, double ssample_param, + int count, i_fountain_seg *segs) { + struct fount_state state; + fount_func ffunc; + fount_ssample ssfunc; + fount_repeat rpfunc; + int x, y; + i_fcolor *line = mymalloc(sizeof(i_fcolor) * im->xsize); + int i, j; + i_fountain_seg *my_segs = mymalloc(sizeof(i_fountain_seg) * count); + int have_alpha = im->channels == 2 || im->channels == 4; + int ch; + + /* we keep a local copy that we can adjust for speed */ + for (i = 0; i < count; ++i) { + i_fountain_seg *seg = my_segs + i; + + *seg = segs[i]; + if (seg->type < 0 || type >= i_ft_end) + seg->type = i_ft_linear; + if (seg->color < 0 || seg->color >= i_fc_end) + seg->color = i_fc_direct; + if (seg->color == i_fc_hue_up || seg->color == i_fc_hue_down) { + /* so we don't have to translate to HSV on each request, do it here */ + for (j = 0; j < 2; ++j) { + i_rgb_to_hsvf(seg->c+j); + } + if (seg->color == i_fc_hue_up) { + if (seg->c[1].channel[0] <= seg->c[0].channel[0]) + seg->c[1].channel[0] += 1.0; + } + else { + if (seg->c[0].channel[0] <= seg->c[0].channel[1]) + seg->c[0].channel[0] += 1.0; + } + } + /*printf("start %g mid %g end %g c0(%g,%g,%g,%g) c1(%g,%g,%g,%g) type %d color %d\n", + seg->start, seg->middle, seg->end, seg->c[0].channel[0], + seg->c[0].channel[1], seg->c[0].channel[2], seg->c[0].channel[3], + seg->c[1].channel[0], seg->c[1].channel[1], seg->c[1].channel[2], + seg->c[1].channel[3], seg->type, seg->color);*/ + + } + + /* initialize each engine */ + /* these are so common ... */ + state.lA = xb - xa; + state.lB = yb - ya; + state.AB = sqrt(state.lA * state.lA + state.lB * state.lB); + state.xa = xa; + state.ya = ya; + switch (type) { + default: + type = i_ft_linear; /* make the invalid value valid */ + case i_ft_linear: + case i_ft_bilinear: + state.lC = ya * ya - ya * yb + xa * xa - xa * xb; + state.mult = 1; + state.mult = 1/linear_fount_f(xb, yb, &state); + break; + + case i_ft_radial: + state.mult = 1.0 / sqrt((double)(xb-xa)*(xb-xa) + + (double)(yb-ya)*(yb-ya)); + break; + + case i_ft_radial_square: + state.cos = state.lA / state.AB; + state.sin = state.lB / state.AB; + state.mult = 1.0 / state.AB; + break; + + case i_ft_revolution: + state.theta = atan2(yb-ya, xb-xa); + state.mult = 1.0 / (PI * 2); + break; + + case i_ft_conical: + state.theta = atan2(yb-ya, xb-xa); + state.mult = 1.0 / PI; + break; + } + ffunc = fount_funcs[type]; + if (super_sample < 0 + || super_sample >= (sizeof(fount_ssamples)/sizeof(*fount_ssamples))) { + super_sample = 0; + } + state.ssample_data = NULL; + switch (super_sample) { + case i_fts_grid: + ssample_param = floor(0.5 + sqrt(ssample_param)); + state.ssample_data = mymalloc(sizeof(i_fcolor) * ssample_param * ssample_param); + break; + + case i_fts_random: + case i_fts_circle: + ssample_param = floor(0.5+ssample_param); + state.ssample_data = mymalloc(sizeof(i_fcolor) * ssample_param); + break; + } + ssfunc = fount_ssamples[super_sample]; + if (repeat < 0 || repeat >= (sizeof(fount_repeats)/sizeof(*fount_repeats))) + repeat = 0; + rpfunc = fount_repeats[repeat]; + + for (y = 0; y < im->ysize; ++y) { + i_glinf(im, 0, im->xsize, y, line); + for (x = 0; x < im->xsize; ++x) { + i_fcolor c; + int got_one; + double v; + if (super_sample == i_fts_none) + got_one = fount_getat(&c, x, y, ffunc, rpfunc, &state, my_segs, count); + else + got_one = ssfunc(&c, ssample_param, x, y, &state, ffunc, rpfunc, + my_segs, count); + if (got_one) { + i_fountain_seg *seg = my_segs + i; + if (combine) { + for (ch = 0; ch < im->channels; ++ch) { + line[x].channel[ch] = line[x].channel[ch] * (1.0 - c.channel[3]) + + c.channel[ch] * c.channel[3]; + } + } + else + line[x] = c; + } + } + i_plinf(im, 0, im->xsize, y, line); + } + myfree(line); + myfree(my_segs); + if (state.ssample_data) + myfree(state.ssample_data); +} + +/* +=back + +=head1 INTERNAL FUNCTIONS + +=over + +=item fount_getat(out, x, y, ffunc, rpfunc, state, segs, count) + +Evaluates the fountain fill at the given point. + +This is called by both the non-super-sampling and super-sampling code. + +You might think that it would make sense to sample the fill parameter +instead, and combine those, but this breaks badly. + +=cut +*/ + +static int +fount_getat(i_fcolor *out, double x, double y, fount_func ffunc, + fount_repeat rpfunc, struct fount_state *state, + i_fountain_seg *segs, int count) { + double v = rpfunc(ffunc(x, y, state)); + int i; + + i = 0; + while (i < count && (v < segs[i].start || v > segs[i].end)) { + ++i; + } + if (i < count) { + v = (fount_interps[segs[i].type])(v, segs+i); + (fount_cinterps[segs[i].color])(out, v, segs+i); + return 1; + } + else + return 0; +} + +/* +=item linear_fount_f(x, y, state) + +Calculate the fill parameter for a linear fountain fill. + +Uses the point to line distance function, with some precalculation +done in i_fountain(). + +=cut +*/ +static double +linear_fount_f(double x, double y, struct fount_state *state) { + return (state->lA * x + state->lB * y + state->lC) / state->AB * state->mult; +} + +/* +=item bilinear_fount_f(x, y, state) + +Calculate the fill parameter for a bi-linear fountain fill. + +=cut +*/ +static double +bilinear_fount_f(double x, double y, struct fount_state *state) { + return fabs((state->lA * x + state->lB * y + state->lC) / state->AB * state->mult); +} + +/* +=item radial_fount_f(x, y, state) + +Calculate the fill parameter for a radial fountain fill. + +Simply uses the distance function. + +=cut + */ +static double +radial_fount_f(double x, double y, struct fount_state *state) { + return sqrt((double)(state->xa-x)*(state->xa-x) + + (double)(state->ya-y)*(state->ya-y)) * state->mult; +} + +/* +=item square_fount_f(x, y, state) + +Calculate the fill parameter for a square fountain fill. + +Works by rotating the reference co-ordinate around the centre of the +square. + +=cut +*/ +static double +square_fount_f(double x, double y, struct fount_state *state) { + int xc, yc; /* centred on A */ + double xt, yt; /* rotated by theta */ + xc = x - state->xa; + yc = y - state->ya; + xt = fabs(xc * state->cos + yc * state->sin); + yt = fabs(-xc * state->sin + yc * state->cos); + return (xt > yt ? xt : yt) * state->mult; +} + +/* +=item revolution_fount_f(x, y, state) + +Calculates the fill parameter for the revolution fountain fill. + +=cut +*/ +static double +revolution_fount_f(double x, double y, struct fount_state *state) { + double angle = atan2(y - state->ya, x - state->xa); + + angle -= state->theta; + if (angle < 0) { + angle = fmod(angle+ PI * 4, PI*2); + } + + return angle * state->mult; +} + +/* +=item conical_fount_f(x, y, state) + +Calculates the fill parameter for the conical fountain fill. + +=cut +*/ +static double +conical_fount_f(double x, double y, struct fount_state *state) { + double angle = atan2(y - state->ya, x - state->xa); + + angle -= state->theta; + if (angle < -PI) + angle += PI * 2; + else if (angle > PI) + angle -= PI * 2; + + return fabs(angle) * state->mult; +} + +/* +=item linear_interp(pos, seg) + +Calculates linear interpolation on the fill parameter. Breaks the +segment into 2 regions based in the I value. + +=cut +*/ +static double +linear_interp(double pos, i_fountain_seg *seg) { + if (pos < seg->middle) { + double len = seg->middle - seg->start; + if (len < EPSILON) + return 0.0; + else + return (pos - seg->start) / len / 2; + } + else { + double len = seg->end - seg->middle; + if (len < EPSILON) + return 1.0; + else + return 0.5 + (pos - seg->middle) / len / 2; + } +} + +/* +=item sine_interp(pos, seg) + +Calculates sine function interpolation on the fill parameter. + +=cut +*/ +static double +sine_interp(double pos, i_fountain_seg *seg) { + /* I wonder if there's a simple way to smooth the transition for this */ + double work = linear_interp(pos, seg); + + return (1-cos(work * PI))/2; +} + +/* +=item sphereup_interp(pos, seg) + +Calculates spherical interpolation on the fill parameter, with the cusp +at the low-end. + +=cut +*/ +static double +sphereup_interp(double pos, i_fountain_seg *seg) { + double work = linear_interp(pos, seg); + + return sqrt(1.0 - (1-work) * (1-work)); +} + +/* +=item spheredown_interp(pos, seg) + +Calculates spherical interpolation on the fill parameter, with the cusp +at the high-end. + +=cut +*/ +static double +spheredown_interp(double pos, i_fountain_seg *seg) { + double work = linear_interp(pos, seg); + + return 1-sqrt(1.0 - work * work); +} + +/* +=item direct_cinterp(out, pos, seg) + +Calculates the fountain color based on direct scaling of the channels +of the color channels. + +=cut +*/ +static void +direct_cinterp(i_fcolor *out, double pos, i_fountain_seg *seg) { + int ch; + for (ch = 0; ch < MAXCHANNELS; ++ch) { + out->channel[ch] = seg->c[0].channel[ch] * (1 - pos) + + seg->c[1].channel[ch] * pos; + } +} + +/* +=item hue_up_cinterp(put, pos, seg) + +Calculates the fountain color based on scaling a HSV value. The hue +increases as the fill parameter increases. + +=cut +*/ +static void +hue_up_cinterp(i_fcolor *out, double pos, i_fountain_seg *seg) { + int ch; + for (ch = 0; ch < MAXCHANNELS; ++ch) { + out->channel[ch] = seg->c[0].channel[ch] * (1 - pos) + + seg->c[1].channel[ch] * pos; + } + i_hsv_to_rgbf(out); +} + +/* +=item hue_down_cinterp(put, pos, seg) + +Calculates the fountain color based on scaling a HSV value. The hue +decreases as the fill parameter increases. + +=cut +*/ +static void +hue_down_cinterp(i_fcolor *out, double pos, i_fountain_seg *seg) { + int ch; + for (ch = 0; ch < MAXCHANNELS; ++ch) { + out->channel[ch] = seg->c[0].channel[ch] * (1 - pos) + + seg->c[1].channel[ch] * pos; + } + i_hsv_to_rgbf(out); +} + +/* +=item simple_ssample(out, parm, x, y, state, ffunc, rpfunc, segs, count) + +Simple grid-based super-sampling. + +=cut +*/ +static int +simple_ssample(i_fcolor *out, double parm, double x, double y, + struct fount_state *state, + fount_func ffunc, fount_repeat rpfunc, i_fountain_seg *segs, + int count) { + i_fcolor *work = state->ssample_data; + int dx, dy; + int grid = parm; + double base = -0.5 + 0.5 / grid; + double step = 1.0 / grid; + int ch, i; + int samp_count = 0; + + for (dx = 0; dx < grid; ++dx) { + for (dy = 0; dy < grid; ++dy) { + if (fount_getat(work+samp_count, x + base + step * dx, + y + base + step * dy, ffunc, rpfunc, state, + segs, count)) { + ++samp_count; + } + } + } + for (ch = 0; ch < MAXCHANNELS; ++ch) { + out->channel[ch] = 0; + for (i = 0; i < samp_count; ++i) { + out->channel[ch] += work[i].channel[ch]; + } + /* we divide by 4 rather than samp_count since if there's only one valid + sample it should be mostly transparent */ + out->channel[ch] /= grid * grid; + } + return samp_count; +} + +/* +=item random_ssample(out, parm, x, y, state, ffunc, rpfunc, segs, count) + +Random super-sampling. + +=cut +*/ +static int +random_ssample(i_fcolor *out, double parm, double x, double y, + struct fount_state *state, + fount_func ffunc, fount_repeat rpfunc, i_fountain_seg *segs, + int count) { + i_fcolor *work = state->ssample_data; + int i, ch; + int maxsamples = parm; + double rand_scale = 1.0 / RAND_MAX; + int samp_count = 0; + for (i = 0; i < maxsamples; ++i) { + if (fount_getat(work+samp_count, x - 0.5 + rand() * rand_scale, + y - 0.5 + rand() * rand_scale, ffunc, rpfunc, state, + segs, count)) { + ++samp_count; + } + } + for (ch = 0; ch < MAXCHANNELS; ++ch) { + out->channel[ch] = 0; + for (i = 0; i < samp_count; ++i) { + out->channel[ch] += work[i].channel[ch]; + } + /* we divide by maxsamples rather than samp_count since if there's + only one valid sample it should be mostly transparent */ + out->channel[ch] /= maxsamples; + } + return samp_count; +} + +/* +=item circle_ssample(out, parm, x, y, state, ffunc, rpfunc, segs, count) + +Super-sampling around the circumference of a circle. + +I considered saving the sin()/cos() values and transforming step-size +around the circle, but that's inaccurate, though it may not matter +much. + +=cut + */ +static int +circle_ssample(i_fcolor *out, double parm, double x, double y, + struct fount_state *state, + fount_func ffunc, fount_repeat rpfunc, i_fountain_seg *segs, + int count) { + i_fcolor *work = state->ssample_data; + int i, ch; + int maxsamples = parm; + double angle = 2 * PI / maxsamples; + double radius = 0.3; /* semi-random */ + int samp_count = 0; + for (i = 0; i < maxsamples; ++i) { + if (fount_getat(work+samp_count, x + radius * cos(angle * i), + y + radius * sin(angle * i), ffunc, rpfunc, state, + segs, count)) { + ++samp_count; + } + } + for (ch = 0; ch < MAXCHANNELS; ++ch) { + out->channel[ch] = 0; + for (i = 0; i < samp_count; ++i) { + out->channel[ch] += work[i].channel[ch]; + } + /* we divide by maxsamples rather than samp_count since if there's + only one valid sample it should be mostly transparent */ + out->channel[ch] /= maxsamples; + } + return samp_count; +} + +/* +=item fount_r_none(v) + +Implements no repeats. Simply clamps the fill value. + +=cut +*/ +static double +fount_r_none(double v) { + return v < 0 ? 0 : v > 1 ? 1 : v; +} + +/* +=item fount_r_sawtooth(v) + +Implements sawtooth repeats. Clamps negative values and uses fmod() +on others. + +=cut +*/ +static double +fount_r_sawtooth(double v) { + return v < 0 ? 0 : fmod(v, 1.0); +} + +/* +=item fount_r_triangle(v) + +Implements triangle repeats. Clamps negative values, uses fmod to get +a range 0 through 2 and then adjusts values > 1. + +=cut +*/ +static double +fount_r_triangle(double v) { + if (v < 0) + return 0; + else { + v = fmod(v, 2.0); + return v > 1.0 ? 2.0 - v : v; + } +} + +/* +=item fount_r_saw_both(v) + +Implements sawtooth repeats in the both postive and negative directions. + +Adjusts the value to be postive and then just uses fmod(). + +=cut +*/ +static double +fount_r_saw_both(double v) { + if (v < 0) + v += 1+(int)(-v); + return fmod(v, 1.0); +} + +/* +=item fount_r_tri_both(v) + +Implements triangle repeats in the both postive and negative directions. + +Uses fmod on the absolute value, and then adjusts values > 1. + +=cut +*/ +static double +fount_r_tri_both(double v) { + v = fmod(fabs(v), 2.0); + return v > 1.0 ? 2.0 - v : v; +} + +/* +=back + +=head1 AUTHOR + +Arnar M. Hrafnkelsson + +Tony Cook (i_fountain()) + +=head1 SEE ALSO + +Imager(3) + +=cut +*/ diff --git a/image.c b/image.c index 8486a218..8b5bf760 100644 --- a/image.c +++ b/image.c @@ -1458,7 +1458,7 @@ i_glinf_d(i_img *im, int l, int r, int y, i_fcolor *vals) { count = r - l; for (i = 0; i < count; ++i) { for (ch = 0; ch < im->channels; ++ch) - vals[i].channel[ch] = SampleFTo8(*data++); + vals[i].channel[ch] = Sample8ToF(*data++); } return count; } @@ -1495,7 +1495,7 @@ i_plinf_d(i_img *im, int l, int r, int y, i_fcolor *vals) { for (i = 0; i < count; ++i) { for (ch = 0; ch < im->channels; ++ch) { if (im->ch_mask & (1 << ch)) - *data = Sample8ToF(vals[i].channel[ch]); + *data = SampleFTo8(vals[i].channel[ch]); ++data; } } @@ -1599,7 +1599,7 @@ int i_gsampf_d(i_img *im, int l, int r, int y, i_fsample_t *samps, } for (i = 0; i < w; ++i) { for (ch = 0; ch < chan_count; ++ch) { - *samps++ = data[chans[ch]]; + *samps++ = Sample8ToF(data[chans[ch]]); ++count; } data += im->channels; @@ -1608,7 +1608,7 @@ int i_gsampf_d(i_img *im, int l, int r, int y, i_fsample_t *samps, else { for (i = 0; i < w; ++i) { for (ch = 0; ch < chan_count; ++ch) { - *samps++ = data[ch]; + *samps++ = Sample8ToF(data[ch]); ++count; } data += im->channels; diff --git a/image.h b/image.h index 45f50408..1bb7cc84 100644 --- a/image.h +++ b/image.h @@ -42,6 +42,9 @@ void ICL_add (i_color *dst, i_color *src, int ch); extern i_fcolor *i_fcolor_new(double r, double g, double b, double a); extern void i_fcolor_destroy(i_fcolor *cl); +extern void i_rgb_to_hsvf(i_fcolor *color); +extern void i_hsv_to_rgbf(i_fcolor *color); + i_img *IIM_new(int x,int y,int ch); void IIM_DESTROY(i_img *im); i_img *i_img_new( void ); @@ -509,6 +512,52 @@ void i_radnoise(i_img *im,int xo,int yo,float rscale,float ascale); void i_turbnoise(i_img *im,float xo,float yo,float scale); void i_gradgen(i_img *im, int num, int *xo, int *yo, i_color *ival, int dmeasure); void i_nearest_color(i_img *im, int num, int *xo, int *yo, i_color *ival, int dmeasure); +typedef enum { + i_fst_linear, + i_fst_curved, + i_fst_sine, + i_fst_sphere_up, + i_fst_sphere_down, + i_fst_end +} i_fountain_seg_type; +typedef enum { + i_fc_direct, + i_fc_hue_up, + i_fc_hue_down, + i_fc_end +} i_fountain_color; +typedef struct { + double start, middle, end; + i_fcolor c[2]; + i_fountain_seg_type type; + i_fountain_color color; +} i_fountain_seg; +typedef enum { + i_fr_none, + i_fr_sawtooth, + i_fr_triangle, + i_fr_saw_both, + i_fr_tri_both +} i_fountain_repeat; +typedef enum { + i_ft_linear, + i_ft_bilinear, + i_ft_radial, + i_ft_radial_square, + i_ft_revolution, + i_ft_conical, + i_ft_end +} i_fountain_type; +typedef enum { + i_fts_none, + i_fts_grid, + i_fts_random, + i_fts_circle +} i_ft_supersample; +void i_fountain(i_img *im, double xa, double ya, double xb, double yb, + i_fountain_type type, i_fountain_repeat repeat, + int combine, int super_sample, double ssample_param, + int count, i_fountain_seg *segs); /* Debug only functions */ diff --git a/lib/Imager/Fountain.pm b/lib/Imager/Fountain.pm new file mode 100644 index 00000000..f9f27321 --- /dev/null +++ b/lib/Imager/Fountain.pm @@ -0,0 +1,393 @@ +package Imager::Fountain; +use strict; +use Imager::Color::Float; + +=head1 NAME + + Imager::Fountain - a class for building fountain fills suitable for use by + the fountain filter. + +=head1 SYNOPSIS + + use Imager::Fountain; + my $f1 = Imager::Fountain->read(gimp=>$filename); + $f->write(gimp=>$filename); + my $f1 = Imager::Fountain->new; + $f1->add(start=>0, middle=>0.5, end=>1.0, + c0=>Imager::Color->new(...), + c1=>Imager::Color->new(...), + type=>$trans_type, color=>$color_trans_type); + +=head1 DESCRIPTION + +Provide an interface to build arrays suitable for use by the Imager +fountain filter. These can be loaded from or saved to a GIMP gradient +file or you can build them from scratch. + +=over + +=item read(gimp=>$filename) + + Loads a gradient from the given GIMP gradient file, and returns a + new Imager::Fountain object. + +=cut + +sub read { + my ($class, %opts) = @_; + + if ($opts{gimp}) { + my $fh; + $fh = ref($opts{gimp}) ? $opts{gimp} : IO::File->new($opts{gimp}); + unless ($fh) { + $Imager::ERRSTR = "Cannot open $opts{gimp}: $!"; + return; + } + + return $class->_load_gimp_gradient($fh, $opts{gimp}); + } + else { + warn "$class::read: Nothing to do!"; + return; + } +} + +=item write(gimp=>$filename) + +Save the gradient to a GIMP gradient file. + +=cut + +sub write { + my ($self, %opts) = @_; + + if ($opts{gimp}) { + my $fh; + $fh = ref($opts{gimp}) ? $opts{gimp} : IO::File->new("> ".$opts{gimp}); + unless ($fh) { + $Imager::ERRSTR = "Cannot open $opts{gimp}: $!"; + return; + } + + return $self->_save_gimp_gradient($fh, $opts{gimp}); + } + else { + warn "Nothing to do\n"; + return; + } +} + +=item new + +Create an empty fountain fill description. + +=cut + +sub new { + my ($class) = @_; + + return bless [], $class; +} + +sub _first { + for (@_) { + return $_ if defined; + } + return undef; +} + +=item add(start=>$start, middle=>$middle, end=>1.0, c0=>$start_color, c1=>$end_color, type=>$trans_type, color=>$color_trans_type) + +Adds a new segment to the fountain fill, the possible options are: + +=over + +=item start + +The start position in the gradient where this segment +takes effect between 0 and 1. Default: 0. + +=item middle + +The mid-point of the transition between the 2 +colors, between 0 and 1. Default: average of I and I. + +=item end + +The end of the gradient, from 0 to 1. Default: 1. + +=item c0 + +The color of the fountain fill where the fill parameter is equal +to I. Default: opaque black. + +=item c1 + +The color of the fountain fill where the fill parameter is equal to +I. Default: opaque black. + +=item type + +The type of segment, controls the way in which the fill parameter +moves from 0 to 1. Default: linear. + +This can take any of the following values: + +=over + +=item linear + +=item curved + +Unimplemented so far. + +=item sine + +=item sphereup + +=item spheredown + +=back + +=item color + +The way in which the color transitions between I and I. +Default: direct. + +This can take any of the following values: + +=over + +=item direct + +Each channel is simple scaled between c0 and c1. + +=item hsvup + +The color is converted to a HSV value and the scaling is done such +that the hue increases as the fill parameter increases. + +=item hsvdown + +The color is converted to a HSV value and the scaling is done such +that the hue decreases as the fill parameter increases. + +=back + +=back + +In most cases you can ignore some of the arguments, eg. + + # assuming $f is a new Imager::Fountain in each case here + use Imager ':handy'; + # simple transition from red to blue + $f->add(c0=>NC('#FF0000), c1=>NC('#0000FF')); + # simple 2 stages from red to green to blue + $f->add(end=>0.5, c0=>NC('#FF0000'), c1=>NC('#00FF00')) + $f->add(start=>0.5, c0=>NC('#00FF00'), c1->NC('#0000FF')); + +=cut + +# used to translate segment types and color transition types to numbers +my %type_names = + ( + linear => 0, + curved => 1, + sine => 2, + sphereup=> 3, + spheredown => 4, + ); + +my %color_names = + ( + direct => 0, + hueup => 1, + huedown => 2 + ); + +sub add { + my ($self, %opts) = @_; + + my $start = _first($opts{start}, 0); + my $end = _first($opts{end}, 1); + my $middle = _first($opts{middle}, ($start+$end)/2); + my @row = + ( + $start, $middle, $end, + _first($opts{c0}, Imager::Color::Float->new(0,0,0,1)), + _first($opts{c1}, Imager::Color::Float->new(1,1,1,0)), + _first($opts{type} && $type_names{$opts{type}}, $opts{type}, 0), + _first($opts{color} && $color_names{$opts{color}}, $opts{color}, 0) + ); + push(@$self, \@row); + + $self; +} + +=item simple(positions=>[ ... ], colors=>[...]) + +Creates a simple fountain fill object consisting of linear segments. + +The arrayrefs passed as positions and colors must have the same number +of elements. They must have at least 2 elements each. + +colors must contain Imager::Color or Imager::Color::Float objects. + +eg. + + my $f = Imager::Fountain->simple(positions=>[0, 0.2, 1.0], + colors=>[ NC(255,0,0), NC(0,255,0), + NC(0,0,255) ]); + +=cut + +sub simple { + my ($class, %opts) = @_; + + if ($opts{positions} && $opts{colors}) { + my $positions = $opts{positions}; + my $colors = $opts{colors}; + unless (@$positions == @$colors) { + $Imager::ERRSTR = "positions and colors must be the same size"; + return; + } + unless (@$positions >= 2) { + $Imager::ERRSTR = "not enough segments"; + return; + } + my $f = $class->new; + for my $i (0.. $#$colors-1) { + $f->add(start=>$positions->[$i], end=>$positions->[$i+1], + c0 => $colors->[$i], c1=>$colors->[$i+1]); + } + } + else { + warn "Nothing to do"; + return; + } +} + +=back + +=head2 Implementation Functions + +Documented for internal use. + +=over + +=item _load_gimp_gradient($class, $fh, $name) + +Does the work of loading a GIMP gradient file. + +=cut + +sub _load_gimp_gradient { + my ($class, $fh, $name) = @_; + + my $head = <$fh>; + chomp $head; + unless ($head eq 'GIMP Gradient') { + $Imager::ERRSTR = "$name is not a GIMP gradient file"; + return; + } + my $count = <$fh>; + chomp $count; + unless ($count =~ /^\d$/) { + $Imager::ERRSTR = "$name is missing the segment count"; + return; + } + my @result; + for my $i (1..$count) { + my $row = <$fh>; + chomp $row; + my @row = split ' ', $row; + unless (@row == 13) { + $Imager::ERRSTR = "Bad segment definition"; + return; + } + my ($start, $middle, $end) = splice(@row, 0, 3); + my $c0 = Imager::Color::Float->new(splice(@row, 0, 4)); + my $c1 = Imager::Color::Float->new(splice(@row, 0, 4)); + my ($type, $color) = @row; + push(@result, [ $start, $middle, $end, $c0, $c1, $type, $color ]); + } + return bless \@result, +} + +=item _save_gimp_gradient($self, $fh, $name) + +Does the work of saving to a GIMP gradient file. + +=cut + +sub _save_gimp_gradient { + my ($self, $fh, $name) = @_; + + print $fh "GIMP Gradient\n"; + print $fh scalar(@$self),"\n"; + for my $row (@$self) { + printf $fh "%.6f %.6f %.6f ",@{$row}[0..2]; + for my $i (0, 1) { + for ($row->[3+$i]->rgba) { + printf $fh, "%.6f ", $_; + } + } + print $fh @{$row}[5,6]; + unless (print $fh "\n") { + $Imager::ERRSTR = "write error: $!"; + return; + } + } + + return 1; +} + +=back + +=head1 FILL PARAMETER + +The add() documentation mentions a fill parameter in a few places, +this is as good a place as any to discuss it. + +The process of deciding the color produced by the gradient works +through the following steps: + +=over + +=item 1. + +calculate the base value, which is typically a distance or an angle of +some sort. This can be positive or occasinally negative, depending on +the type of fill being performed (linear, radial, etc). + +=item 2. + +clamp or convert the base value to the range 0 through 1, how this is +done depends on the repeat parameter. I'm calling this result the +fill parameter. + +=item 3. + +the appropriate segment is found. This is currently done with a +linear search, and the first matching segment is used. If there is no +matching segment the pixel is not touched. + +=item 4 + +the fill parameter is scaled from 0 to 1 depending on the segment type. + +=item 5 + +the color produced, depending on the segment color type. + +=back + +=head1 AUTHOR + +Tony Cook + +=head1 SEE ALSO + +Imager(3) + +=cut diff --git a/t/t61filters.t b/t/t61filters.t index 17cdbc8d..6e6e3503 100644 --- a/t/t61filters.t +++ b/t/t61filters.t @@ -8,7 +8,7 @@ $imbase->open(file=>'testout/t104.ppm') or die; my $im_other = Imager->new(xsize=>150, ysize=>150); $im_other->box(xmin=>30, ymin=>60, xmax=>120, ymax=>90, filled=>1); -print "1..26\n"; +print "1..35\n"; test($imbase, 1, {type=>'autolevels'}, 'testout/t61_autolev.ppm'); @@ -46,6 +46,37 @@ test($imbase, 23, {type=>'postlevels', levels=>3}, 'testout/t61_postlevels.ppm') test($imbase, 25, {type=>'watermark', wmark=>$im_other }, 'testout/t61_watermark.ppm'); +test($imbase, 27, {type=>'fountain', xa=>75, ya=>75, xb=>85, yb=>30, + repeat=>'triangle', #ftype=>'radial', + super_sample=>'circle', ssample_param => 16, + }, + 'testout/t61_fountain.ppm'); +use Imager::Fountain; + +my $f1 = Imager::Fountain->new; +$f1->add(end=>0.2, c0=>NC(255, 0,0), c1=>NC(255, 255,0)); +$f1->add(start=>0.2, c0=>NC(255,255,0), c1=>NC(0,0,255,0)); +test($imbase, 29, { type=>'fountain', xa=>20, ya=>130, xb=>130, yb=>20, + #repeat=>'triangle', + segments=>$f1 + }, + 'testout/t61_fountain2.ppm'); +my $f2 = Imager::Fountain->new + ->add(end=>0.5, c0=>NC(255,0,0), c1=>NC(255,0,0), color=>'hueup') + ->add(start=>0.5, c0=>NC(255,0,0), c1=>NC(255,0,0), color=>'huedown'); +#use Data::Dumper; +#print Dumper($f2); +test($imbase, 31, { type=>'fountain', xa=>20, ya=>130, xb=>130, yb=>20, + segments=>$f2 }, + 'testout/t61_fount_hsv.ppm'); +my $f3 = Imager::Fountain->read(gimp=>'testimg/gimpgrad') + or print "not "; +print "ok 33\n"; +test($imbase, 34, { type=>'fountain', xa=>75, ya=>75, xb=>90, yb=>10, + segments=>$f3, super_sample=>'grid', + ftype=>'radial_square', combine=>1 }, + 'testout/t61_fount_gimp.ppm'); + sub test { my ($in, $num, $params, $out) = @_; diff --git a/testimg/gimpgrad b/testimg/gimpgrad new file mode 100644 index 00000000..563cd779 --- /dev/null +++ b/testimg/gimpgrad @@ -0,0 +1,4 @@ +GIMP Gradient +2 +0.000000 0.130000 0.546377 0.000000 0.000000 1.000000 1.000000 0.000000 1.000000 0.000000 1.000000 4 1 +0.546377 0.773188 1.000000 0.000000 1.000000 0.000000 1.000000 0.000000 0.000000 0.000000 0.000000 0 0 -- 2.39.2