implement fountain fills similar to most paint programs
authorTony Cook <tony@develop=help.com>
Wed, 29 Aug 2001 09:27:35 +0000 (09:27 +0000)
committerTony Cook <tony@develop=help.com>
Wed, 29 Aug 2001 09:27:35 +0000 (09:27 +0000)
minor bug fixes

12 files changed:
Changes
Imager.pm
Imager.xs
MANIFEST
Makefile.PL
color.c [new file with mode: 0644]
filters.c
image.c
image.h
lib/Imager/Fountain.pm [new file with mode: 0644]
t/t61filters.t
testimg/gimpgrad [new file with mode: 0644]

diff --git a/Changes b/Changes
index caa23e7..1ed0604 100644 (file)
--- 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
 
 =================================================================
 
index a091901..46b636f 100644 (file)
--- 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<coef>.  I<coef> 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<repeat> option controls how the fill is repeated for some
+I<ftype>s 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<combine> 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<stddev> 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<size>.
+
 =item noise
 
 adds noise of the given I<amount> to the image.  If I<subtype> is
index bfb3953..0217966 100644 (file)
--- 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<count; i++) {
+          SV **sv1 = av_fetch(asegs, i, 0);
+          if (!sv1 || !*sv1 || !SvROK(*sv1) 
+              || SvTYPE(SvRV(*sv1)) != SVt_PVAV) {
+            myfree(segs);
+            croak("i_fountain: segs must be an arrayref of arrayrefs");
+          }
+          aseg = (AV *)SvRV(*sv1);
+          if (av_len(aseg) != 7-1) {
+            myfree(segs);
+            croak("i_fountain: a segment must have 7 members");
+          }
+          for (j = 0; j < 3; ++j) {
+            SV **sv2 = av_fetch(aseg, j, 0);
+            if (!sv2 || !*sv2) {
+              myfree(segs);
+              croak("i_fountain: XS error");
+            }
+            work[j] = SvNV(*sv2);
+          }
+          segs[i].start  = work[0];
+          segs[i].middle = work[1];
+          segs[i].end    = work[2];
+          for (j = 0; j < 2; ++j) {
+            SV **sv3 = av_fetch(aseg, 3+j, 0);
+            if (!sv3 || !*sv3 || !SvROK(*sv3) ||
+                (!sv_derived_from(*sv3, "Imager::Color")
+                 && !sv_derived_from(*sv3, "Imager::Color::Float"))) {
+              myfree(segs);
+              croak("i_fountain: segs must contain colors in elements 3 and 4");
+            }
+            if (sv_derived_from(*sv3, "Imager::Color::Float")) {
+              segs[i].c[j] = *(i_fcolor *)SvIV((SV *)SvRV(*sv3));
+            }
+            else {
+              i_color c = *(i_color *)SvIV((SV *)SvRV(*sv3));
+              int ch;
+              for (ch = 0; ch < MAXCHANNELS; ++ch) {
+                segs[i].c[j].channel[ch] = c.channel[ch] / 255.0;
+              }
+            }
+          }
+          for (j = 0; j < 2; ++j) {
+            SV **sv2 = av_fetch(aseg, j+5, 0);
+            if (!sv2 || !*sv2) {
+              myfree(segs);
+              croak("i_fountain: XS error");
+            }
+            worki[j] = SvIV(*sv2);
+          }
+          segs[i].type = worki[0];
+          segs[i].color = worki[1];
+       }
+        i_fountain(im, xa, ya, xb, yb, type, repeat, combine, super_sample, 
+                   ssample_param, count, segs);
+        myfree(segs);
 
 void
 i_errors()
index b2ccc6b..1710a37 100644 (file)
--- a/MANIFEST
+++ b/MANIFEST
@@ -5,6 +5,7 @@ MANIFEST
 README
 Makefile.PL
 bmp.c
+color.c         Color translation and handling
 conv.c
 convert.c
 draw.c
@@ -57,6 +58,7 @@ lib/Imager/Font.pm
 lib/Imager/Font/Type1.pm
 lib/Imager/Font/Truetype.pm
 lib/Imager/Font/FreeType2.pm
+lib/Imager/Fountain.pm
 lib/Imager/interface.pod
 lib/Imager/Matrix2d.pm
 lib/Imager/regmach.pod
@@ -96,9 +98,10 @@ t/t70newgif.t
 t/t75polyaa.t
 t/t90cc.t
 testimg/bandw.gif
-testimg/comp4.bmp
-testimg/comp8.bmp
+testimg/comp4.bmp       Compressed 4-bit/pixel BMP
+testimg/comp8.bmp       Compressed 8-bit/pixel BMP
 testimg/expected.gif
+testimg/gimpgrad        A GIMP gradient file
 testimg/junk.ppm
 testimg/loccmap.gif
 testimg/nocmap.gif
@@ -110,7 +113,7 @@ testimg/scale.ppm
 testimg/scalei.gif
 testimg/screen2.gif
 testimg/screen3.gif
-testimg/test_gimp_pal
+testimg/test_gimp_pal   A simple GIMP palette file
 testimg/trimgdesc.gif
 testimg/trmiddesc.gif
 typemap
index 994fd33..8b61d14 100644 (file)
@@ -63,7 +63,7 @@ if (defined $Config{'d_dlsymun'}) { $OSDEF  .= ' -DDLSYMUN'; }
           filters.o dynaload.o stackmach.o datatypes.o
           regmach.o trans2.o quant.o error.o convert.o
           map.o tags.o palimg.o maskimg.o img16.o rotate.o
-           bmp.o);
+           bmp.o color.o);
 
 %opts=(
        'NAME'         => 'Imager',
diff --git a/color.c b/color.c
new file mode 100644 (file)
index 0000000..6097cc9
--- /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 <tony@develop-help.com>
+
+=head1 SEE ALSO
+
+Imager
+
+=cut
+*/
index 5747ab4..4ee3987 100644 (file)
--- 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<type> 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<repeat> 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<super_sample> 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<ssample_param> is intended to be roughly the number of points
+sampled within the pixel.
+
+I<count> and I<segs> 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<middle> 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 <addi@umich.edu>
+
+Tony Cook <tony@develop-help.com> (i_fountain())
+
+=head1 SEE ALSO
+
+Imager(3)
+
+=cut
+*/
diff --git a/image.c b/image.c
index 8486a21..8b5bf76 100644 (file)
--- 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 45f5040..1bb7cc8 100644 (file)
--- 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 (file)
index 0000000..f9f2732
--- /dev/null
@@ -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<start> and I<end>.
+
+=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<start>.  Default: opaque black.
+
+=item c1
+
+The color of the fountain fill where the fill parameter is equal to
+I<end>.  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<c0> and I<c1>.
+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 <tony@develop-help.com>
+
+=head1 SEE ALSO
+
+Imager(3)
+
+=cut
index 17cdbc8..6e6e350 100644 (file)
@@ -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 (file)
index 0000000..563cd77
--- /dev/null
@@ -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