Index: src/ColorTransformation.vala
===================================================================
--- src/ColorTransformation.vala	(revision 2019)
+++ src/ColorTransformation.vala	(working copy)
@@ -211,14 +211,89 @@
     }
 }
 
-public enum CompositionMode {
-    NONE,
-    RGB_MATRIX
+public struct RGBValuesTable {
+    public float[] component_values_red;
+    public float[] component_values_green;
+    public float[] component_values_blue;
+    public const int num_values = 256;
+ 
+    private const float INV_255 = 1.0f / 255.0f;
+
+    public RGBValuesTable() {
+        component_values_red = new float[256];
+        component_values_green = new float[256];
+        component_values_blue = new float[256];
+        
+        for (int i=0; i<256; i++) {
+            float component_value = ((float) i) * INV_255;
+            component_values_red[i] = component_value;
+            component_values_green[i] = component_value;
+            component_values_blue[i] = component_value;
+        }
+    }
+    
+    public void copy_from(RGBValuesTable source_table) {
+        for (int i=0; i<256; i++) {
+            component_values_red[i] = source_table.component_values_red[i];
+            component_values_green[i] = source_table.component_values_green[i];
+            component_values_blue[i] = source_table.component_values_blue[i];
+        }
+    }
 }
 
-public enum PixelFormat {
-    RGB,
-    HSV
+public struct RGBLookupTable {
+    public uchar[] lookup_values_red;
+    public uchar[] lookup_values_green;
+    public uchar[] lookup_values_blue;
+    public const int num_values = 256;
+
+    public RGBLookupTable() {
+        lookup_values_red = new uchar[256];
+        lookup_values_green = new uchar[256];
+        lookup_values_blue = new uchar[256];
+        
+        for (int i=0; i<256; i++) {
+            lookup_values_red[i] = (uchar) i;
+            lookup_values_green[i] = (uchar) i;
+            lookup_values_blue[i] = (uchar) i;
+        }
+    }
+    
+    public void load_from_values(RGBValuesTable source_values) {
+        float value;
+    
+        for (int i=0; i<256; i++) {
+            value = source_values.component_values_red[i];
+            
+            if (value >= 1.0) {
+                lookup_values_red[i] = 0xFF;
+            } else if (value > 0.0) {
+                lookup_values_red[i] = (uchar) (value * 255.0f + 0.5f);
+            } else {
+                lookup_values_red[i] = 0x00;
+            }
+
+            value = source_values.component_values_green[i];
+            
+            if (value >= 1.0) {
+                lookup_values_green[i] = 0xFF;
+            } else if (value > 0.0) {
+                lookup_values_green[i] = (uchar) (value * 255.0f + 0.5f);
+            } else {
+                lookup_values_green[i] = 0x00;
+            }
+
+            value = source_values.component_values_blue[i];
+            
+            if (value >= 1.0) {
+                lookup_values_blue[i] = 0xFF;
+            } else if (value > 0.0) {
+                lookup_values_blue[i] = (uchar) (value * 255.0f + 0.5f);
+            } else {
+                lookup_values_blue[i] = 0x00;
+            }
+        }
+    }
 }
 
 public enum PixelTransformationType {
@@ -230,6 +305,11 @@
     EXPOSURE
 }
 
+public enum PixelTransformationMethod {
+    RGB_MATRIX,
+    RGB_LOOKUP
+}
+
 public class PixelTransformationBundle {
     private Gee.HashMap<int, PixelTransformation> map = new Gee.HashMap<int, PixelTransformation>(
         Gee.Functions.get_hash_func_for(typeof(int)), Gee.Functions.get_equal_func_for(typeof(int)));
@@ -340,38 +420,27 @@
 
 public abstract class PixelTransformation {
     private PixelTransformationType type;
+    private PixelTransformationMethod method;
+    protected bool identity = true;
     
-    public PixelTransformation(PixelTransformationType type) {
+    public PixelTransformation(PixelTransformationType type,
+        PixelTransformationMethod method) {
         this.type = type;
+        this.method = method;
     }
     
     public PixelTransformationType get_transformation_type() {
         return type;
     }
     
-    public abstract PixelFormat get_preferred_format();
-
-    public virtual CompositionMode get_composition_mode() {
-        return CompositionMode.NONE;
+    public PixelTransformationMethod get_transformation_method() {
+        return method;
     }
-
-    public virtual void compose_with(PixelTransformation other) {
-        error("PixelTransformation: compose_with( ): this type of pixel " +
-            "transformation doesn't support composition.");
+    
+    public bool is_identity() {
+        return identity;
     }
 
-    public virtual bool is_identity() {
-        return true;
-    }
-
-    public virtual HSVAnalyticPixel transform_pixel_hsv(HSVAnalyticPixel p) {
-        return p;
-    }
-
-    public virtual RGBAnalyticPixel transform_pixel_rgb(RGBAnalyticPixel p) {
-        return p;
-    }
-
     public virtual string to_string() {
         return "PixelTransformation";
     }
@@ -379,268 +448,120 @@
     public abstract PixelTransformation copy();
 }
 
-public class RGBTransformation : PixelTransformation {
-    /* matrix entries are stored in row-major order; by default, the matrix formed
-       by matrix_entries is the 4x4 identity matrix */
-    protected float[] matrix_entries;
-    
-    protected const int MATRIX_SIZE = 16;
-
-    protected bool identity = true;
-    
-    public RGBTransformation(PixelTransformationType type) {
-        base(type);
-        
-        // Can't initialize these in their member declarations because of a valac bug that
-        // I've been unable to produce a minimal test case for to report (JN).  May be 
-        // related to this bug:
-        // https://bugzilla.gnome.org/show_bug.cgi?id=570821
-        matrix_entries = {
-        1.0f, 0.0f, 0.0f, 0.0f,
-        0.0f, 1.0f, 0.0f, 0.0f,
-        0.0f, 0.0f, 1.0f, 0.0f,
-        0.0f, 0.0f, 0.0f, 1.0f };
+public abstract class RGBLookupTransformation: PixelTransformation {
+    public RGBLookupTransformation(PixelTransformationType type) {
+        base(type, PixelTransformationMethod.RGB_LOOKUP);
     }
 
-    public override PixelFormat get_preferred_format() {
-        return PixelFormat.RGB;
+    public virtual void transform_rgb_values_table(RGBValuesTable table) {
     }
 
-    public override CompositionMode get_composition_mode() {
-        return CompositionMode.RGB_MATRIX;
+    public override string to_string() {
+        return "RGBLookupTransformation";
     }
+}
 
-    public override void compose_with(PixelTransformation other) {
-        if (other.get_composition_mode() != CompositionMode.RGB_MATRIX)
-            error("RGBTransformation: compose_with( ): 'other' transformation " +
-                "does not support RGB_MATRIX composition mode");
+public abstract class RGBMatrixTransformation: PixelTransformation {
+    public float matrix_entries[12];
 
-        RGBTransformation transform = (RGBTransformation) other;
-
-        float[] result_matrix_entries = new float[16];
-
-        /* row 0 */
-        result_matrix_entries[0] =
-            (transform.matrix_entries[0] * matrix_entries[0]) +
-            (transform.matrix_entries[1] * matrix_entries[4]) +
-            (transform.matrix_entries[2] * matrix_entries[8]) +
-            (transform.matrix_entries[3] * matrix_entries[12]);
-
-        result_matrix_entries[1] =
-            (transform.matrix_entries[0] * matrix_entries[1]) +
-            (transform.matrix_entries[1] * matrix_entries[5]) +
-            (transform.matrix_entries[2] * matrix_entries[9]) +
-            (transform.matrix_entries[3] * matrix_entries[13]);
-
-        result_matrix_entries[2] =
-            (transform.matrix_entries[0] * matrix_entries[2]) +
-            (transform.matrix_entries[1] * matrix_entries[6]) +
-            (transform.matrix_entries[2] * matrix_entries[10]) +
-            (transform.matrix_entries[3] * matrix_entries[14]);
-
-        result_matrix_entries[3] =
-            (transform.matrix_entries[0] * matrix_entries[3]) +
-            (transform.matrix_entries[1] * matrix_entries[7]) +
-            (transform.matrix_entries[2] * matrix_entries[11]) +
-            (transform.matrix_entries[3] * matrix_entries[15]);
-
-        /* row 1 */
-        result_matrix_entries[4] =
-            (transform.matrix_entries[4] * matrix_entries[0]) +
-            (transform.matrix_entries[5] * matrix_entries[4]) +
-            (transform.matrix_entries[6] * matrix_entries[8]) +
-            (transform.matrix_entries[7] * matrix_entries[12]);
-
-        result_matrix_entries[5] =
-            (transform.matrix_entries[4] * matrix_entries[1]) +
-            (transform.matrix_entries[5] * matrix_entries[5]) +
-            (transform.matrix_entries[6] * matrix_entries[9]) +
-            (transform.matrix_entries[7] * matrix_entries[13]);
-
-        result_matrix_entries[6] =
-            (transform.matrix_entries[4] * matrix_entries[2]) +
-            (transform.matrix_entries[5] * matrix_entries[6]) +
-            (transform.matrix_entries[6] * matrix_entries[10]) +
-            (transform.matrix_entries[7] * matrix_entries[14]);
-
-        result_matrix_entries[7] =
-            (transform.matrix_entries[4] * matrix_entries[3]) +
-            (transform.matrix_entries[5] * matrix_entries[7]) +
-            (transform.matrix_entries[6] * matrix_entries[11]) +
-            (transform.matrix_entries[7] * matrix_entries[15]);
-
-        /* row 2 */
-        result_matrix_entries[8] =
-            (transform.matrix_entries[8] * matrix_entries[0]) +
-            (transform.matrix_entries[9] * matrix_entries[4]) +
-            (transform.matrix_entries[10] * matrix_entries[8]) +
-            (transform.matrix_entries[11] * matrix_entries[12]);
-
-        result_matrix_entries[9] =
-            (transform.matrix_entries[8] * matrix_entries[1]) +
-            (transform.matrix_entries[9] * matrix_entries[5]) +
-            (transform.matrix_entries[10] * matrix_entries[9]) +
-            (transform.matrix_entries[11] * matrix_entries[13]);
-
-        result_matrix_entries[10] =
-            (transform.matrix_entries[8] * matrix_entries[2]) +
-            (transform.matrix_entries[9] * matrix_entries[6]) +
-            (transform.matrix_entries[10] * matrix_entries[10]) +
-            (transform.matrix_entries[11] * matrix_entries[14]);
-
-        result_matrix_entries[11] =
-            (transform.matrix_entries[8] * matrix_entries[3]) +
-            (transform.matrix_entries[9] * matrix_entries[7]) +
-            (transform.matrix_entries[10] * matrix_entries[11]) +
-            (transform.matrix_entries[11] * matrix_entries[15]);
-
-        /* row 3 */
-        result_matrix_entries[12] =
-            (transform.matrix_entries[12] * matrix_entries[0]) +
-            (transform.matrix_entries[13] * matrix_entries[4]) +
-            (transform.matrix_entries[14] * matrix_entries[8]) +
-            (transform.matrix_entries[15] * matrix_entries[12]);
-
-        result_matrix_entries[13] =
-            (transform.matrix_entries[12] * matrix_entries[1]) +
-            (transform.matrix_entries[13] * matrix_entries[5]) +
-            (transform.matrix_entries[14] * matrix_entries[9]) +
-            (transform.matrix_entries[15] * matrix_entries[13]);
-
-        result_matrix_entries[14] =
-            (transform.matrix_entries[12] * matrix_entries[2]) +
-            (transform.matrix_entries[13] * matrix_entries[6]) +
-            (transform.matrix_entries[14] * matrix_entries[10]) +
-            (transform.matrix_entries[15] * matrix_entries[14]);
-
-        result_matrix_entries[15] =
-            (transform.matrix_entries[12] * matrix_entries[3]) +
-            (transform.matrix_entries[13] * matrix_entries[7]) +
-            (transform.matrix_entries[14] * matrix_entries[11]) +
-            (transform.matrix_entries[15] * matrix_entries[15]);
-
-        for (int i = 0; i < MATRIX_SIZE; i++)
-            matrix_entries[i] = result_matrix_entries[i];
-
-        identity = (identity && transform.identity);
+    public RGBMatrixTransformation(PixelTransformationType type) {
+        base(type, PixelTransformationMethod.RGB_MATRIX);
     }
 
-    public override HSVAnalyticPixel transform_pixel_hsv(HSVAnalyticPixel p) {
-        return (transform_pixel_rgb(p.to_rgb())).to_hsv();
+    public override string to_string() {
+        return "RGBMatrixTransformation";
     }
-
-    public override RGBAnalyticPixel transform_pixel_rgb(RGBAnalyticPixel p) {
-        float red_out = (p.red * matrix_entries[0]) +
-            (p.green * matrix_entries[1]) +
-            (p.blue * matrix_entries[2]) +
-            matrix_entries[3];
-        red_out = red_out.clamp(0.0f, 1.0f);
-
-        float green_out = (p.red * matrix_entries[4]) +
-            (p.green * matrix_entries[5]) +
-            (p.blue * matrix_entries[6]) +
-            matrix_entries[7];
-        green_out = green_out.clamp(0.0f, 1.0f);
-
-        float blue_out = (p.red * matrix_entries[8]) +
-            (p.green * matrix_entries[9]) +
-            (p.blue * matrix_entries[10]) +
-            matrix_entries[11];
-        blue_out = blue_out.clamp(0.0f, 1.0f);
-        
-        return RGBAnalyticPixel.from_components(red_out, green_out, blue_out);
-    }
-
-    public override bool is_identity() {
-        return identity;
-    }
-    
-    public override PixelTransformation copy() {
-        RGBTransformation result = new RGBTransformation(get_transformation_type());
-
-        for (int i = 0; i < MATRIX_SIZE; i++) {
-            result.matrix_entries[i] = matrix_entries[i];
-        }
-
-        return result;
-    }
 }
 
-public abstract class HSVTransformation : PixelTransformation {
-    public HSVTransformation(PixelTransformationType type) {
-        base(type);
-    }
-    
-    public override PixelFormat get_preferred_format() {
-        return PixelFormat.HSV;
-    }
-
-    public override RGBAnalyticPixel transform_pixel_rgb(RGBAnalyticPixel p) {
-        return (transform_pixel_hsv(p.to_hsv())).to_rgb();
-    }
-}
-
-public class TintTransformation : RGBTransformation {
+public class TintTransformation : RGBLookupTransformation {
     private const float INTENSITY_FACTOR = 0.25f;
     public const float MIN_PARAMETER = -16.0f;
     public const float MAX_PARAMETER = 16.0f;
     
     private float parameter;
+    private float adjusted_param;
+    private float half_adjusted_param;
 
     public TintTransformation(float client_param) {
         base(PixelTransformationType.TINT);
         
         parameter = client_param.clamp(MIN_PARAMETER, MAX_PARAMETER);
+        adjusted_param = parameter / MAX_PARAMETER * INTENSITY_FACTOR;
+        half_adjusted_param = adjusted_param / 2.0f;
 
-         if (parameter != 0.0f) {
-             float adjusted_param = parameter / MAX_PARAMETER;
-             adjusted_param *= INTENSITY_FACTOR;
-             
-             matrix_entries[11] -= (adjusted_param / 2);
-             matrix_entries[7] += adjusted_param;
-             matrix_entries[3] -= (adjusted_param / 2);
- 
+         if (parameter != 0.0f)
              identity = false;
+    }
+
+    public override void transform_rgb_values_table(RGBValuesTable table) {
+        float *red_value = table.component_values_red;
+        float *green_value = table.component_values_green;
+        float *blue_value = table.component_values_blue;
+        int table_counter = table.num_values;
+        
+        while (table_counter-- != 0) {
+            *red_value++ = *red_value - half_adjusted_param;
+            *green_value++ = *green_value + adjusted_param;
+            *blue_value++ = *blue_value - half_adjusted_param;
          }
     }
+    
+    public override PixelTransformation copy() {
+        return new TintTransformation(parameter);
+    }
 
     public float get_parameter() {
         return parameter;
     }
 }
 
-public class TemperatureTransformation : RGBTransformation {
+public class TemperatureTransformation : RGBLookupTransformation {
     private const float INTENSITY_FACTOR = 0.33f;
     public const float MIN_PARAMETER = -16.0f;
     public const float MAX_PARAMETER = 16.0f;
     
     private float parameter;
+    private float adjusted_param;
+    private float half_adjusted_param;
 
     public TemperatureTransformation(float client_parameter) {
         base(PixelTransformationType.TEMPERATURE);
         
         parameter = client_parameter.clamp(MIN_PARAMETER, MAX_PARAMETER);
+        adjusted_param = parameter / MAX_PARAMETER * INTENSITY_FACTOR;
+        half_adjusted_param = adjusted_param / 2.0f;
         
-         if (parameter != 0.0f) {
-             float adjusted_param = parameter / MAX_PARAMETER;
-             adjusted_param *= INTENSITY_FACTOR;
-             
-             matrix_entries[11] -= adjusted_param;
-             matrix_entries[7] += (adjusted_param / 2);
-             matrix_entries[3] += (adjusted_param / 2);
+        if (parameter != 0.0f)
+            identity = false;
+    }
 
-             identity = false;
-         }
+    public override void transform_rgb_values_table(RGBValuesTable table) {
+        float *red_value = table.component_values_red;
+        float *green_value = table.component_values_green;
+        float *blue_value = table.component_values_blue;
+        int table_counter = table.num_values;
+        
+        while (table_counter-- != 0) {
+            *red_value++ = *red_value + half_adjusted_param;
+            *green_value++ = *green_value + half_adjusted_param;
+            *blue_value++ = *blue_value - adjusted_param;
+        
+        }
     }
-
+  
+    public override PixelTransformation copy() {
+        return new TemperatureTransformation(parameter);
+    }
+    
     public float get_parameter() {
         return parameter;
     }
 }
 
-public class SaturationTransformation : RGBTransformation {
+public class SaturationTransformation : RGBMatrixTransformation {
     public const float MIN_PARAMETER = -16.0f;
     public const float MAX_PARAMETER = 16.0f;
+    private const float ONE_THIRD = 1.0f / 3.0f;
     
     private float parameter;
 
@@ -650,25 +571,25 @@
         parameter = client_parameter.clamp(MIN_PARAMETER, MAX_PARAMETER);
 
         if (parameter != 0.0f) {
-            float adjusted_param = parameter / MAX_PARAMETER;
-            adjusted_param += 1.0f;
+            float adjusted_param = parameter / MAX_PARAMETER + 1.0f;
 
-            float one_third = 0.3333333f;
-
-            matrix_entries[0] = ((1.0f - adjusted_param) * one_third) +
+            matrix_entries[0] = ((1.0f - adjusted_param) * ONE_THIRD) +
                 adjusted_param;
-            matrix_entries[1] = (1.0f - adjusted_param) * one_third;
-            matrix_entries[2] = (1.0f - adjusted_param) * one_third;
+            matrix_entries[1] = (1.0f - adjusted_param) * ONE_THIRD;
+            matrix_entries[2] = (1.0f - adjusted_param) * ONE_THIRD;
+            matrix_entries[3] = 0.0f;
 
-            matrix_entries[4] = (1.0f - adjusted_param) * one_third;
-            matrix_entries[5] = ((1.0f - adjusted_param) * one_third) +
+            matrix_entries[4] = (1.0f - adjusted_param) * ONE_THIRD;
+            matrix_entries[5] = ((1.0f - adjusted_param) * ONE_THIRD) +
                 adjusted_param;
-            matrix_entries[6] = (1.0f - adjusted_param) * one_third;
+            matrix_entries[6] = (1.0f - adjusted_param) * ONE_THIRD;
+            matrix_entries[7] = 0.0f;
 
-            matrix_entries[8] = (1.0f - adjusted_param) * one_third;
-            matrix_entries[9] = (1.0f - adjusted_param) * one_third;
-            matrix_entries[10] = ((1.0f - adjusted_param) * one_third) +
+            matrix_entries[8] = (1.0f - adjusted_param) * ONE_THIRD;
+            matrix_entries[9] = (1.0f - adjusted_param) * ONE_THIRD;
+            matrix_entries[10] = ((1.0f - adjusted_param) * ONE_THIRD) +
                 adjusted_param;
+            matrix_entries[11] = 0.0f;
 
             identity = false;
         }
@@ -677,176 +598,323 @@
     public float get_parameter() {
         return parameter;
     }
+    
+    public override PixelTransformation copy() {
+        return new SaturationTransformation(parameter);
+    }
 }
 
-public class ExposureTransformation : RGBTransformation {
+public class ExposureTransformation : RGBLookupTransformation {
     public const float MIN_PARAMETER = -16.0f;
     public const float MAX_PARAMETER = 16.0f;
 
     float parameter;
+    float adjusted_param;
 
     public ExposureTransformation(float client_parameter) {
         base(PixelTransformationType.EXPOSURE);
         
         parameter = client_parameter.clamp(MIN_PARAMETER, MAX_PARAMETER);
+        adjusted_param = ((parameter + 16.0f) / 32.0f) + 0.5f;
+        
+        if (parameter != 0.0f)
+            identity = false;
+    }
 
-        if (parameter != 0.0f) {
+    public override void transform_rgb_values_table(RGBValuesTable table) {
+        float *red_value = table.component_values_red;
+        float *green_value = table.component_values_green;
+        float *blue_value = table.component_values_blue;
+        int table_counter = table.num_values;
+        
+        while (table_counter-- != 0) {
+            *red_value++ = *red_value * adjusted_param;
+            *green_value++ = *green_value * adjusted_param;
+            *blue_value++ = *blue_value * adjusted_param;       
+        }
+    }
 
-            float adjusted_param = ((parameter + 16.0f) / 32.0f) + 0.5f;
+    public float get_parameter() {
+        return parameter;
+    }
+    
+    public override PixelTransformation copy() {
+        return new ExposureTransformation(parameter);
+    }
+}
 
-            matrix_entries[0] = adjusted_param;
-            matrix_entries[5] = adjusted_param;
-            matrix_entries[10] = adjusted_param;
+public class ExpansionTransformation : RGBLookupTransformation {
+    private const float LOW_DISCARD_MASS = 0.02f;
+    private const float HIGH_DISCARD_MASS = 0.02f;
+    private const float INV_255 = 1.0f / 255.0f;
 
+    private int black_point;
+    private int white_point;
+
+    private float lower_bound;
+    private float inverse_range;
+
+    public ExpansionTransformation(IntensityHistogram histogram) {
+        float LOW_KINK_MASS = LOW_DISCARD_MASS;        
+        int black_point = 0;
+        while (histogram.get_cumulative_probability(black_point) < LOW_KINK_MASS)
+            black_point++;
+        
+        float HIGH_KINK_MASS = 1.0f - HIGH_DISCARD_MASS;
+        int white_point = 255;
+        while (histogram.get_cumulative_probability(white_point) > HIGH_KINK_MASS)
+            white_point--;
+            
+        this.from_extrema(black_point, white_point);
+    }
+    
+    public ExpansionTransformation.from_extrema(int black_point, int white_point) {
+        base(PixelTransformationType.TONE_EXPANSION);
+
+        white_point = white_point.clamp(0, 255);
+        black_point = black_point.clamp(0, 255);
+
+        if (black_point == white_point) {
+            if (black_point == 0)
+                white_point = 1;
+            else if (white_point == 255)
+                black_point = 254;
+            else
+                black_point = white_point - 1;
+        }
+
+        this.black_point = black_point;
+        this.white_point = white_point;
+
+        lower_bound = ((float) black_point) * INV_255;
+        inverse_range = 255.0f / ((float) (white_point - black_point));
+        
+        if ((black_point != 0) || (white_point != 255))
             identity = false;
+    }
+
+    public ExpansionTransformation.from_string(string encoded_transformation) {
+        encoded_transformation.canon("0123456789. ", ' ');
+        encoded_transformation.chug();
+        encoded_transformation.chomp();
+
+        int black_point = 0, white_point = 255;
+        int num_captured = encoded_transformation.scanf("%d %d", &black_point,
+            &white_point);
+
+        assert(num_captured == 2);
+        
+        this.from_extrema(black_point, white_point);
+    }
+
+    public override void transform_rgb_values_table(RGBValuesTable table) {
+        float *red_value = table.component_values_red;
+        float *green_value = table.component_values_green;
+        float *blue_value = table.component_values_blue;
+        int table_counter = table.num_values;
+        
+        while (table_counter-- != 0) {
+            *red_value++ = (*red_value - lower_bound) * inverse_range;
+            *green_value++ = (*green_value - lower_bound) * inverse_range;
+            *blue_value++ = (*blue_value - lower_bound) * inverse_range;
         }
     }
+  
+    public override string to_string() {
+        return "{ %d, %d }".printf(black_point, white_point);
+    }
 
+    public int get_white_point() {
+        return white_point;
+    }
+
+    public int get_black_point() {
+        return black_point;
+    }
+
+    public override PixelTransformation copy() {
+        return new ExpansionTransformation.from_extrema(black_point, white_point);
+    }
+}
+
+public class ShadowDetailTransformation : RGBLookupTransformation {
+    public const float MIN_PARAMETER = 0.0f;
+    public const float MAX_PARAMETER = 32.0f;
+    private const float MAX_EFFECT_SHIFT = 0.5f;
+    
+    private float intensity = 0.0f;
+    private float effect_shift = 0.0f;
+    
+    public ShadowDetailTransformation(float user_intensity) {
+        base(PixelTransformationType.SHADOWS);
+        
+        intensity = user_intensity;
+        effect_shift = MAX_EFFECT_SHIFT * (intensity / MAX_PARAMETER).clamp(0.0f, 1.0f);
+
+        if (intensity != 0.0f)
+            identity = false;
+    }
+
+    public override void transform_rgb_values_table(RGBValuesTable table) {
+        float *red_value = table.component_values_red;
+        float *green_value = table.component_values_green;
+        float *blue_value = table.component_values_blue;
+        int table_counter = table.num_values;
+        float value, value_squared, weight;
+        
+        while (table_counter-- != 0) {
+            value = *red_value;
+            value_squared = value * value;
+            weight = 6.0f * (value * value_squared - 2.0f * value_squared + value);
+            *red_value++ = value + weight * effect_shift;
+        
+            value = *green_value;
+            value_squared = value * value;
+            weight = 6.0f * (value * value_squared - 2.0f * value_squared + value);
+            *green_value++ = value + weight * effect_shift;
+        
+            value = *blue_value;
+            value_squared = value * value;
+            weight = 6.0f * (value * value_squared - 2.0f * value_squared + value);
+            *blue_value++ = value + weight * effect_shift;
+        }
+    }
+  
+    public override PixelTransformation copy() {
+        return new ShadowDetailTransformation(intensity);
+    }
+    
     public float get_parameter() {
-        return parameter;
+        return intensity;
     }
 }
 
 public class PixelTransformer {
-    private Gee.ArrayList<PixelTransformation> transformations =
-        new Gee.ArrayList<PixelTransformation>();
-    private PixelTransformation[] optimized_transformations = null;
-    private int optimized_slots_used = 0;
+    private Gee.ArrayList<RGBMatrixTransformation> matrix_transformations =
+        new Gee.ArrayList<RGBMatrixTransformation>();
+    private Gee.ArrayList<RGBLookupTransformation> lookup_transformations =
+        new Gee.ArrayList<RGBLookupTransformation>();
+   
+    // matrix transform members
+    private float working_matrix_float[12];
+    private int32 combined_matrix_fixedpt[12];  // 16-bit shifted
+    private bool combined_matrix_is_dirty = true;
+    private bool combined_matrix_is_identity = true;
     
+    // lookup transform members 
+    private RGBValuesTable identity_rgb_values;
+    private RGBValuesTable transformed_rgb_values;
+    private RGBLookupTable transformed_rgb_lookup_table;
+    private bool lookup_table_is_dirty = true;
+    
     public PixelTransformer() {
+        identity_rgb_values = RGBValuesTable();
+        transformed_rgb_values = RGBValuesTable();
+        transformed_rgb_lookup_table = RGBLookupTable();
     }
     
     public PixelTransformer copy() {
         PixelTransformer clone = new PixelTransformer();
         
-        foreach (PixelTransformation transformation in transformations)
-            clone.transformations.add(transformation);
+        foreach (RGBMatrixTransformation transformation in matrix_transformations)
+            clone.matrix_transformations.add(transformation);
         
+        foreach (RGBLookupTransformation transformation in lookup_transformations)
+            clone.lookup_transformations.add(transformation);
+        
         return clone;
     }
     
-    private void build_optimized_transformations() {
-        optimized_transformations = new PixelTransformation[transformations.size];
-
-        PixelTransformation pre_trans = null;
-        optimized_slots_used = 0;
-        for (int i = 0; i < transformations.size; i++) {
-            PixelTransformation trans = transformations.get(i);
-
-            if (trans.is_identity())
-                continue;
-
-            PixelTransformation this_trans = null;
-            if (trans.get_composition_mode() == CompositionMode.NONE)
-                this_trans = trans;
-            else
-                this_trans = trans.copy();
-
-            if ((pre_trans != null) && (this_trans.get_composition_mode() != CompositionMode.NONE)
-                && (this_trans.get_composition_mode() == pre_trans.get_composition_mode())) {
-                    pre_trans.compose_with(this_trans);
-            } else {
-                    optimized_transformations[optimized_slots_used++] = this_trans;
-                    pre_trans = this_trans;
-            }
-        }
-    }
-    
-    private RGBAnalyticPixel apply_transformations(RGBAnalyticPixel p) {
-        PixelFormat current_format = PixelFormat.RGB;
-        RGBAnalyticPixel p_rgb = p;
-        HSVAnalyticPixel p_hsv = HSVAnalyticPixel();
-
-        for (int i = 0; i < optimized_slots_used; i++) {
-            PixelTransformation trans = optimized_transformations[i];
-            if (trans.get_preferred_format() == PixelFormat.RGB) {
-                if (current_format == PixelFormat.HSV) {
-                    p_rgb = p_hsv.to_rgb();
-                    current_format = PixelFormat.RGB;
-                }
-                p_rgb = trans.transform_pixel_rgb(p_rgb);
-            } else {
-                if (current_format == PixelFormat.RGB) {
-                    p_hsv = p_rgb.to_hsv();
-                    current_format = PixelFormat.HSV;
-                }
-                p_hsv = trans.transform_pixel_hsv(p_hsv);
-            }
-        }
-
-        if (current_format == PixelFormat.HSV)
-            p_rgb = p_hsv.to_rgb();
-
-        return p_rgb;
-    }
-
     /* NOTE: this method allows the same transformation to be added multiple
              times. There's nothing wrong with this behavior as of today,
              but it may be a policy that we want to change in the future */
     public void attach_transformation(PixelTransformation trans) {
-        transformations.add(trans);
-        optimized_transformations = null;
+        PixelTransformationMethod transformation_method = 
+            trans.get_transformation_method();
+    
+        if (transformation_method == PixelTransformationMethod.RGB_LOOKUP) {
+            lookup_transformations.add((RGBLookupTransformation) trans);
+            lookup_table_is_dirty = true;
+        } else if (transformation_method == PixelTransformationMethod.RGB_MATRIX) {
+            matrix_transformations.add((RGBMatrixTransformation) trans);
+            handle_matrix_transformations_update();
+        } else {
+            error("PixelTransformer: attach_transformation(): Invalid transformation " + 
+                "method");
+        }
     }
 
     /* NOTE: if a transformation has been added multiple times, only the first
              instance of it will be removed */
     public void detach_transformation(PixelTransformation victim) {
-        transformations.remove(victim);
-        optimized_transformations = null;
+        PixelTransformationMethod transformation_method = 
+            victim.get_transformation_method();
+    
+        if (transformation_method == PixelTransformationMethod.RGB_LOOKUP) {
+            lookup_transformations.remove((RGBLookupTransformation) victim);
+            lookup_table_is_dirty = true;
+        } else if (transformation_method == PixelTransformationMethod.RGB_MATRIX) {
+            matrix_transformations.remove((RGBMatrixTransformation) victim);
+            handle_matrix_transformations_update();
+        } else {
+            error("PixelTransformer: detach_transformation(): Invalid transformation " + 
+                "method");
+        }
     }
 
     /* NOTE: if a transformation has been added multiple times, only the first
              instance of it will be replaced with 'new' */
     public void replace_transformation(PixelTransformation old_trans,
         PixelTransformation new_trans) {
-        for (int i = 0; i < transformations.size; i++) {
-            if (transformations.get(i) == old_trans) {
-                transformations.set(i, new_trans);
+        
+        assert(old_trans.get_transformation_type() == new_trans.get_transformation_type());
 
-                optimized_transformations = null;
-                return;
+        PixelTransformationMethod transformation_method = 
+            old_trans.get_transformation_method();
+        
+        if (transformation_method == PixelTransformationMethod.RGB_LOOKUP) {
+            for (int i = 0; i < lookup_transformations.size; i++) {
+                if (lookup_transformations.get(i) == (RGBLookupTransformation) old_trans) {
+                    lookup_transformations.set(i, (RGBLookupTransformation) new_trans);
+
+                    lookup_table_is_dirty = true;
+                    return;
+                }
             }
+        } else if (transformation_method == PixelTransformationMethod.RGB_MATRIX) {
+            for (int i = 0; i < matrix_transformations.size; i++) {
+                if (matrix_transformations.get(i) == (RGBMatrixTransformation) old_trans) {
+                    matrix_transformations.set(i, (RGBMatrixTransformation) new_trans);
+                    
+                    handle_matrix_transformations_update();
+                    return;
+                }
+            }
+        } else {
+            error("PixelTransformer: replace_transformation(): Invalid transformation " + 
+                "method");
         }
+
         error("PixelTransformer: replace_transformation( ): old_trans is not present in " +
             "transformation collection");
     }
+    
+    private void handle_matrix_transformations_update() {
+        combined_matrix_is_dirty = true;
+        
+        combined_matrix_is_identity = true;
+        foreach (RGBMatrixTransformation transformation in matrix_transformations) {
+            if (!transformation.is_identity()) {
+                combined_matrix_is_identity = false;
+                return;
+            }
+        }
+    }
 
     public void transform_pixbuf(Gdk.Pixbuf pixbuf, Cancellable? cancellable = null) {
         transform_to_other_pixbuf(pixbuf, pixbuf, cancellable);
     }
 
-    public void transform_from_fp(ref float[] fp_pixel_cache, Gdk.Pixbuf dest) {
-        if (optimized_transformations == null)
-            build_optimized_transformations();
-
-        int dest_width = dest.get_width();
-        int dest_height = dest.get_height();
-        int dest_num_channels = dest.get_n_channels();
-        int dest_rowstride = dest.get_rowstride();
-        unowned uchar[] dest_pixels = dest.get_pixels();
-        
-        int cache_pixel_ticker = 0;
-
-        for (int j = 0; j < dest_height; j++) {
-            int row_start_index = j * dest_rowstride;
-            int row_end_index = row_start_index + (dest_width * dest_num_channels);
-            for (int i = row_start_index; i < row_end_index; i += dest_num_channels) {
-                RGBAnalyticPixel pixel = RGBAnalyticPixel.from_components(
-                    fp_pixel_cache[cache_pixel_ticker],
-                    fp_pixel_cache[cache_pixel_ticker + 1],
-                    fp_pixel_cache[cache_pixel_ticker + 2]);
-
-                cache_pixel_ticker += 3;
-
-                pixel = apply_transformations(pixel);
-
-                dest_pixels[i] = (uchar) (pixel.red * 255.0f);
-                dest_pixels[i + 1] = (uchar) (pixel.green * 255.0f);
-                dest_pixels[i + 2] = (uchar) (pixel.blue * 255.0f);
-            }
-        }
-    }
-
     public void transform_to_other_pixbuf(Gdk.Pixbuf source, Gdk.Pixbuf dest,
         Cancellable? cancellable = null) {
         if (source.width != dest.width)
@@ -859,35 +927,256 @@
             error("PixelTransformer: source and destination pixbufs must have the same number " +
                 "of channels");
 
-        if (optimized_transformations == null)
-            build_optimized_transformations();
-
         int n_channels = source.get_n_channels();
         int rowstride = source.get_rowstride();
         int width = source.get_width();
         int height = source.get_height();
-        int rowbytes = n_channels * width;
         unowned uchar[] source_pixels = source.get_pixels();
         unowned uchar[] dest_pixels = dest.get_pixels();
-        for (int j = 0; j < height; j++) {
-            int row_start_index = j * rowstride;
-            int row_end_index = row_start_index + rowbytes;
-            for (int i = row_start_index; i < row_end_index; i += n_channels) {
-                RGBAnalyticPixel current_pixel = RGBAnalyticPixel.from_quantized_components(
-                    source_pixels[i], source_pixels[i + 1], source_pixels[i + 2]);
+        int row_counter, column_counter;
+        uchar* source_row, source_pixel, dest_row, dest_pixel;
+        uchar* lookup_table_red = transformed_rgb_lookup_table.lookup_values_red;
+        uchar* lookup_table_green = transformed_rgb_lookup_table.lookup_values_green;
+        uchar* lookup_table_blue = transformed_rgb_lookup_table.lookup_values_blue;
+        
+        if (lookup_table_is_dirty)
+            build_rgb_lookup_table();
+            
+        source_row = source_pixels;
+        dest_row = dest_pixels;
+        
+        row_counter = height;
+        
+        if (combined_matrix_is_identity) {
+            while (row_counter-- != 0) {
+                source_pixel = source_row;
+                dest_pixel = dest_row;
+                
+                column_counter = width;
+                
+                while (column_counter-- != 0) {
+                    dest_pixel[0] = lookup_table_red[source_pixel[0]];
+                    dest_pixel[1] = lookup_table_green[source_pixel[1]];
+                    dest_pixel[2] = lookup_table_blue[source_pixel[2]];
+                    
+                    dest_pixel += n_channels;
+                    source_pixel += n_channels;
+                }
+                
+                if ((cancellable != null) && (cancellable.is_cancelled())) {
+                    return;
+                }
 
-                current_pixel = apply_transformations(current_pixel);
-
-                dest_pixels[i] = current_pixel.quantized_red();
-                dest_pixels[i + 1] = current_pixel.quantized_green();
-                dest_pixels[i + 2] = current_pixel.quantized_blue();
+                source_row += rowstride;
+                dest_row += rowstride;
             }
+        } else {    // has matrix transforms
+            if (combined_matrix_is_dirty)
+                build_combined_matrix();
+        
+            while (row_counter-- != 0) {
+                source_pixel = source_row;
+                dest_pixel = dest_row;
+                
+                column_counter = width;
+                
+                while (column_counter-- != 0) {
+                    int32 transformed_channel_value = 
+                        ((int32) source_pixel[0] * combined_matrix_fixedpt[0]) +
+                        ((int32) source_pixel[1] * combined_matrix_fixedpt[1]) +
+                        ((int32) source_pixel[2] * combined_matrix_fixedpt[2]) +
+                        combined_matrix_fixedpt[3];
+                        
+                    if (transformed_channel_value >= ((int32) 0xFF << 16)) {
+                        dest_pixel[0] = lookup_table_red[255];
+                    } else if (transformed_channel_value > 0) {
+                        dest_pixel[0] = lookup_table_red[transformed_channel_value >> 16];
+                    } else {
+                        dest_pixel[0] = lookup_table_red[0];
+                    }
+                    
+                    transformed_channel_value =
+                        ((int32) source_pixel[0] * combined_matrix_fixedpt[4]) +
+                        ((int32) source_pixel[1] * combined_matrix_fixedpt[5]) +
+                        ((int32) source_pixel[2] * combined_matrix_fixedpt[6]) +
+                        combined_matrix_fixedpt[7];
+                        
+                    if (transformed_channel_value >= ((int32) 0xFF << 16)) {
+                        dest_pixel[1] = lookup_table_green[255];
+                    } else if (transformed_channel_value > 0) {
+                        dest_pixel[1] = lookup_table_green[transformed_channel_value >> 16];
+                    } else {
+                        dest_pixel[1] = lookup_table_green[0];
+                    }
+                    
+                    transformed_channel_value = 
+                        ((int32) source_pixel[0] * combined_matrix_fixedpt[8]) +
+                        ((int32) source_pixel[1] * combined_matrix_fixedpt[9]) +
+                        ((int32) source_pixel[2] * combined_matrix_fixedpt[10]) +
+                        combined_matrix_fixedpt[11];
+                        
+                    if (transformed_channel_value >= ((int32) 0xFF << 16)) {
+                        dest_pixel[2] = lookup_table_blue[255];
+                    } else if (transformed_channel_value > 0) {
+                        dest_pixel[2] = lookup_table_blue[transformed_channel_value >> 16];
+                    } else {
+                        dest_pixel[2] = lookup_table_blue[0];
+                    }
+                    
+                    dest_pixel += n_channels;
+                    source_pixel += n_channels;
+                }
+                
+                if ((cancellable != null) && (cancellable.is_cancelled())) {
+                    return;
+                }
 
-            if ((cancellable != null) && (cancellable.is_cancelled())) {
-                return;
+                source_row += rowstride;
+                dest_row += rowstride;
             }
         }
     }
+    
+    void build_rgb_lookup_table() {
+        transformed_rgb_values.copy_from(identity_rgb_values);
+ 
+        foreach (RGBLookupTransformation transformation in lookup_transformations) {
+            if (!transformation.is_identity()) {
+                transformation.transform_rgb_values_table(transformed_rgb_values);
+            }
+        }
+        
+        transformed_rgb_lookup_table.load_from_values(transformed_rgb_values);
+        
+        lookup_table_is_dirty = false;
+    }
+    
+    private void build_combined_matrix() {
+        combined_matrix_is_identity = true;
+        
+        foreach (RGBMatrixTransformation transformation in matrix_transformations) {
+            if (!transformation.is_identity()) {
+                if (combined_matrix_is_identity) {
+                    copy_transformation_to_working_matrix(transformation);
+                    combined_matrix_is_identity = false;
+                 } else {
+                    combine_transformation_with_working_matrix(transformation);
+                }
+            }
+        }
+        
+        if (!combined_matrix_is_identity)
+            convert_working_matrix_to_combined_matrix();
+            
+        combined_matrix_is_dirty = false;
+    }
+    
+    private void copy_transformation_to_working_matrix(
+        RGBMatrixTransformation transform) {
+        
+        for (int i=0; i<12; i++) {
+            working_matrix_float[i] = transform.matrix_entries[i];
+        }
+    }
+    
+    private void combine_transformation_with_working_matrix(
+        RGBMatrixTransformation transform) {
+        /* row 0 */
+        working_matrix_float[0] =
+            (transform.matrix_entries[0] * working_matrix_float[0]) +
+            (transform.matrix_entries[1] * working_matrix_float[4]) +
+            (transform.matrix_entries[2] * working_matrix_float[8]);
+
+        working_matrix_float[1] =
+            (transform.matrix_entries[0] * working_matrix_float[1]) +
+            (transform.matrix_entries[1] * working_matrix_float[5]) +
+            (transform.matrix_entries[2] * working_matrix_float[9]);
+
+        working_matrix_float[2] =
+            (transform.matrix_entries[0] * working_matrix_float[2]) +
+            (transform.matrix_entries[1] * working_matrix_float[6]) +
+            (transform.matrix_entries[2] * working_matrix_float[10]);
+
+        working_matrix_float[3] =
+            (transform.matrix_entries[0] * working_matrix_float[3]) +
+            (transform.matrix_entries[1] * working_matrix_float[7]) +
+            (transform.matrix_entries[2] * working_matrix_float[11]);
+
+        /* row 1 */
+        working_matrix_float[4] =
+            (transform.matrix_entries[4] * working_matrix_float[0]) +
+            (transform.matrix_entries[5] * working_matrix_float[4]) +
+            (transform.matrix_entries[6] * working_matrix_float[8]);
+
+        working_matrix_float[5] =
+            (transform.matrix_entries[4] * working_matrix_float[1]) +
+            (transform.matrix_entries[5] * working_matrix_float[5]) +
+            (transform.matrix_entries[6] * working_matrix_float[9]);
+
+        working_matrix_float[6] =
+            (transform.matrix_entries[4] * working_matrix_float[2]) +
+            (transform.matrix_entries[5] * working_matrix_float[6]) +
+            (transform.matrix_entries[6] * working_matrix_float[10]);
+
+        working_matrix_float[7] =
+            (transform.matrix_entries[4] * working_matrix_float[3]) +
+            (transform.matrix_entries[5] * working_matrix_float[7]) +
+            (transform.matrix_entries[6] * working_matrix_float[11]);
+
+        /* row 2 */
+        working_matrix_float[8] =
+            (transform.matrix_entries[8] * working_matrix_float[0]) +
+            (transform.matrix_entries[9] * working_matrix_float[4]) +
+            (transform.matrix_entries[10] * working_matrix_float[8]);
+
+        working_matrix_float[9] =
+            (transform.matrix_entries[8] * working_matrix_float[1]) +
+            (transform.matrix_entries[9] * working_matrix_float[5]) +
+            (transform.matrix_entries[10] * working_matrix_float[9]) +
+            (transform.matrix_entries[11] * working_matrix_float[13]);
+
+        working_matrix_float[10] =
+            (transform.matrix_entries[8] * working_matrix_float[2]) +
+            (transform.matrix_entries[9] * working_matrix_float[6]) +
+            (transform.matrix_entries[10] * working_matrix_float[10]);
+
+        working_matrix_float[11] =
+            (transform.matrix_entries[8] * working_matrix_float[3]) +
+            (transform.matrix_entries[9] * working_matrix_float[7]) +
+            (transform.matrix_entries[10] * working_matrix_float[11]);        
+    }
+    
+    private void convert_working_matrix_to_combined_matrix() {
+        float conversion_factor_coefficient = (float) (1 << 16);
+        float conversion_factor_offset = (float) (1 << (16 + 8));
+        
+        combined_matrix_fixedpt[0] =
+            (int32) (working_matrix_float[0] * conversion_factor_coefficient);
+        combined_matrix_fixedpt[1] = 
+            (int32) (working_matrix_float[1] * conversion_factor_coefficient);
+        combined_matrix_fixedpt[2] =
+            (int32) (working_matrix_float[2] * conversion_factor_coefficient);
+        combined_matrix_fixedpt[3] =
+            (int32) (working_matrix_float[3] * conversion_factor_offset);
+        
+        combined_matrix_fixedpt[4] =
+            (int32) (working_matrix_float[4] * conversion_factor_coefficient);
+        combined_matrix_fixedpt[5] =
+            (int32) (working_matrix_float[5] * conversion_factor_coefficient);
+        combined_matrix_fixedpt[6] =
+            (int32) (working_matrix_float[6] * conversion_factor_coefficient);
+        combined_matrix_fixedpt[7] =
+            (int32) (working_matrix_float[7] * conversion_factor_offset);
+        
+        combined_matrix_fixedpt[8] =
+            (int32) (working_matrix_float[8] * conversion_factor_coefficient);
+        combined_matrix_fixedpt[9] =
+            (int32) (working_matrix_float[9] * conversion_factor_coefficient);
+        combined_matrix_fixedpt[10] =
+            (int32) (working_matrix_float[10] * conversion_factor_coefficient);
+        combined_matrix_fixedpt[11] =
+            (int32) (working_matrix_float[11] * conversion_factor_offset);
+    }
 }
 
 class RGBHistogram {
@@ -1186,195 +1475,6 @@
     }
 }
 
-public class ExpansionTransformation : HSVTransformation {
-    private float[] remap_table = null;
-    private const float LOW_DISCARD_MASS = 0.02f;
-    private const float HIGH_DISCARD_MASS = 0.02f;
-
-    private int low_kink;
-    private int high_kink;
-
-    public ExpansionTransformation(IntensityHistogram histogram) {
-        base(PixelTransformationType.TONE_EXPANSION);
-        
-        remap_table = new float[256];
-
-        float LOW_KINK_MASS = LOW_DISCARD_MASS;
-        low_kink = 0;
-        while (histogram.get_cumulative_probability(low_kink) < LOW_KINK_MASS)
-            low_kink++;
-        
-        float HIGH_KINK_MASS = 1.0f - HIGH_DISCARD_MASS;
-        high_kink = 255;
-        while (histogram.get_cumulative_probability(high_kink) > HIGH_KINK_MASS)
-            high_kink--;
-
-        build_remap_table();
-    }
-    
-    public ExpansionTransformation.from_extrema(int black_point, int white_point) {
-        base(PixelTransformationType.TONE_EXPANSION);
-
-        white_point = white_point.clamp(0, 255);
-        black_point = black_point.clamp(0, 255);
-
-        if (black_point == white_point) {
-            if (black_point == 0)
-                white_point = 1;
-            else if (white_point == 255)
-                black_point = 254;
-            else
-                black_point = white_point - 1;
-        }
-
-        low_kink = black_point;
-        high_kink = white_point;
-
-        build_remap_table();        
-    }
-
-    public ExpansionTransformation.from_string(string encoded_transformation) {
-        base(PixelTransformationType.TONE_EXPANSION);
-        
-        encoded_transformation.canon("0123456789. ", ' ');
-        encoded_transformation.chug();
-        encoded_transformation.chomp();
-
-        int num_captured = encoded_transformation.scanf("%d %d", &low_kink,
-            &high_kink);
-
-        assert(num_captured == 2);
-        
-        build_remap_table();
-    }
-
-    private void build_remap_table() {
-        if (remap_table == null)
-            remap_table = new float[256];
-
-        float low_kink_f = ((float) low_kink) / 255.0f;
-        float high_kink_f = ((float) high_kink) / 255.0f;
-
-        float slope = 1.0f / (high_kink_f - low_kink_f);
-        float intercept = -(low_kink_f / (high_kink_f - low_kink_f));
-        
-        int i = 0;
-        for ( ; i <= low_kink; i++)
-            remap_table[i] = 0.0f;
-        
-        for ( ; i < high_kink; i++)
-            remap_table[i] = slope * (((float) i) / 255.0f) + intercept;
-        
-        for ( ; i < 256; i++)
-            remap_table[i] = 1.0f;
-    }
-
-    public override HSVAnalyticPixel transform_pixel_hsv(HSVAnalyticPixel pixel) {
-        int remap_index = (int)(pixel.light_value * 255.0f);
-
-        HSVAnalyticPixel result = pixel;
-        result.light_value = remap_table[remap_index];
-
-        result.light_value = result.light_value.clamp(0.0f, 1.0f);
-
-        return result;
-    }
-
-    public override string to_string() {
-        return "{ %d, %d }".printf(low_kink, high_kink);
-    }
-
-    public int get_white_point() {
-        return high_kink;
-    }
-
-    public int get_black_point() {
-        return low_kink;
-    }
-
-    public override bool is_identity() {
-        return ((low_kink == 0) && (high_kink == 255));
-    }
-
-    public override PixelTransformation copy() {
-        return new ExpansionTransformation.from_extrema(low_kink, high_kink);
-    }
-}
-
-public class ShadowDetailTransformation : HSVTransformation {
-    private const float MAX_EFFECT_SHIFT = 0.5f;
-    private const float MIN_TONAL_WIDTH = 0.1f;
-    private const float MAX_TONAL_WIDTH = 1.0f;
-    private const float TONAL_WIDTH = 1.0f;
-
-    private float intensity = 0.0f;
-    private float[] remap_table = null;
-    
-    public const float MIN_PARAMETER = 0.0f;
-    public const float MAX_PARAMETER = 32.0f;
-
-    public ShadowDetailTransformation(float user_intensity) {
-        base(PixelTransformationType.SHADOWS);
-        
-        intensity = user_intensity;
-        float intensity_adj = (intensity / MAX_PARAMETER).clamp(0.0f, 1.0f);
-
-        float effect_shift = MAX_EFFECT_SHIFT * intensity_adj;
-        HermiteGammaApproximationFunction func =
-            new HermiteGammaApproximationFunction(TONAL_WIDTH);
-        
-        remap_table = new float[256];
-        for (int i = 0; i < 256; i++) {
-            float x = ((float) i) / 255.0f;
-            float weight = func.evaluate(x);
-            remap_table[i] = (weight * (x + effect_shift)) + ((1.0f - weight) * x);
-        }
-    }
-
-    public override HSVAnalyticPixel transform_pixel_hsv(HSVAnalyticPixel pixel) {
-        HSVAnalyticPixel result = pixel;
-        result.light_value = (remap_table[(int)(pixel.light_value * 255.0f)]).clamp(0.0f, 1.0f);
-        return result;
-    }
-
-    public override PixelTransformation copy() {
-        return new ShadowDetailTransformation(intensity);
-    }
-    
-    public override bool is_identity() {
-        return (intensity == 0.0f);
-    }
-
-    public float get_parameter() {
-        return intensity;
-    }
-}
-
-public class HermiteGammaApproximationFunction {
-    private float x_scale = 1.0f;
-    private float nonzero_interval_upper = 1.0f;
-    
-    public HermiteGammaApproximationFunction(float user_interval_upper) {
-        nonzero_interval_upper = user_interval_upper.clamp(0.1f, 1.0f);
-        x_scale = 1.0f / nonzero_interval_upper;
-    }
-    
-    public float evaluate(float x) {
-        if (x < 0.0f)
-            return 0.0f;
-        else if (x > nonzero_interval_upper)
-            return 0.0f;
-        else {
-            float indep_var = x_scale * x;
-            
-            float dep_var =  6.0f * ((indep_var * indep_var * indep_var) -
-                (2.0f * (indep_var * indep_var)) + (indep_var));
-            
-            return dep_var.clamp(0.0f, 1.0f);
-        }
-    }
-}
-
 namespace AutoEnhance {
     const int SHADOW_DETECT_MIN_INTENSITY = 4;
     const int SHADOW_DETECT_MAX_INTENSITY = 128;
Index: src/EditingTools.vala
===================================================================
--- src/EditingTools.vala	(revision 2019)
+++ src/EditingTools.vala	(working copy)
@@ -2032,7 +2032,7 @@
             slider_organizer.attach_defaults(exposure_slider, 1, 2, 0, 1);
             exposure_slider.set_size_request(SLIDER_WIDTH, -1);
             exposure_slider.set_draw_value(false);
-            exposure_slider.set_update_policy(Gtk.UpdateType.DELAYED);
+            exposure_slider.set_update_policy(Gtk.UpdateType.CONTINUOUS);
 
             Gtk.Label saturation_label = new Gtk.Label.with_mnemonic(_("Saturation:"));
             saturation_label.set_alignment(0.0f, 0.5f);
@@ -2040,7 +2040,7 @@
             slider_organizer.attach_defaults(saturation_slider, 1, 2, 1, 2);
             saturation_slider.set_size_request(SLIDER_WIDTH, -1);
             saturation_slider.set_draw_value(false);
-            saturation_slider.set_update_policy(Gtk.UpdateType.DELAYED);
+            saturation_slider.set_update_policy(Gtk.UpdateType.CONTINUOUS);
 
             Gtk.Label tint_label = new Gtk.Label.with_mnemonic(_("Tint:"));
             tint_label.set_alignment(0.0f, 0.5f);
@@ -2048,7 +2048,7 @@
             slider_organizer.attach_defaults(tint_slider, 1, 2, 2, 3);
             tint_slider.set_size_request(SLIDER_WIDTH, -1);
             tint_slider.set_draw_value(false);
-            tint_slider.set_update_policy(Gtk.UpdateType.DELAYED);
+            tint_slider.set_update_policy(Gtk.UpdateType.CONTINUOUS);
 
             Gtk.Label temperature_label =
                 new Gtk.Label.with_mnemonic(_("Temperature:"));
@@ -2057,7 +2057,7 @@
             slider_organizer.attach_defaults(temperature_slider, 1, 2, 3, 4);
             temperature_slider.set_size_request(SLIDER_WIDTH, -1);
             temperature_slider.set_draw_value(false);
-            temperature_slider.set_update_policy(Gtk.UpdateType.DELAYED);
+            temperature_slider.set_update_policy(Gtk.UpdateType.CONTINUOUS);
 
             Gtk.Label shadows_label = new Gtk.Label.with_mnemonic(_("Shadows:"));
             shadows_label.set_alignment(0.0f, 0.5f);
@@ -2065,7 +2065,7 @@
             slider_organizer.attach_defaults(shadows_slider, 1, 2, 4, 5);
             shadows_slider.set_size_request(SLIDER_WIDTH, -1);
             shadows_slider.set_draw_value(false);
-            shadows_slider.set_update_policy(Gtk.UpdateType.DELAYED);
+            shadows_slider.set_update_policy(Gtk.UpdateType.CONTINUOUS);
 
             Gtk.HBox button_layouter = new Gtk.HBox(false, 8);
             button_layouter.set_homogeneous(true);
@@ -2253,13 +2253,13 @@
     
     private AdjustToolWindow adjust_tool_window = null;
     private bool suppress_effect_redraw = false;
+    private Gdk.Pixbuf scaled_original_pixbuf = null;
     private Gdk.Pixbuf draw_to_pixbuf = null;
     private Gdk.Pixbuf histogram_pixbuf = null;
     private Gdk.Pixbuf virgin_histogram_pixbuf = null;
     private PixelTransformer transformer = null;
     private PixelTransformer histogram_transformer = null;
     private PixelTransformationBundle transformations = null;
-    private float[] fp_pixel_cache = null;
     private bool disable_histogram_refresh = false;
     
     private AdjustTool() {
@@ -2323,8 +2323,8 @@
         bind_canvas_handlers(canvas);
         bind_window_handlers();
 
-        draw_to_pixbuf = canvas.get_scaled_pixbuf().copy();
-        init_fp_pixel_cache(canvas.get_scaled_pixbuf());
+        scaled_original_pixbuf = canvas.get_scaled_pixbuf().copy();
+        draw_to_pixbuf = scaled_original_pixbuf.copy();
 
         /* if we have an 1x1 pixel image, then there's no need to deal with recomputing the
            histogram, because a histogram for a 1x1 image is meaningless. The histogram shows the
@@ -2367,15 +2367,15 @@
             adjust_tool_window = null;
         }
 
+        scaled_original_pixbuf = null;
         draw_to_pixbuf = null;
-        fp_pixel_cache = null;
 
         base.deactivate();
     }
 
     public override void paint(Gdk.GC gc, Gdk.Drawable drawable) {
         if (!suppress_effect_redraw) {
-            transformer.transform_from_fp(ref fp_pixel_cache, draw_to_pixbuf);
+            transformer.transform_to_other_pixbuf(scaled_original_pixbuf, draw_to_pixbuf);
             histogram_transformer.transform_to_other_pixbuf(virgin_histogram_pixbuf,
                 histogram_pixbuf);
             if (!disable_histogram_refresh)
@@ -2475,8 +2475,8 @@
     }
 
     private void on_canvas_resize() {
-        draw_to_pixbuf = canvas.get_scaled_pixbuf().copy();
-        init_fp_pixel_cache(canvas.get_scaled_pixbuf());
+        scaled_original_pixbuf = canvas.get_scaled_pixbuf().copy();
+        draw_to_pixbuf = scaled_original_pixbuf.copy();
     }
     
     private bool on_hscale_reset(Gtk.Widget widget, Gdk.EventButton event) {
@@ -2606,27 +2606,5 @@
             break;
         }
     }
-    
-    private void init_fp_pixel_cache(Gdk.Pixbuf source) {
-        int source_width = source.get_width();
-        int source_height = source.get_height();
-        int source_num_channels = source.get_n_channels();
-        int source_rowstride = source.get_rowstride();
-        unowned uchar[] source_pixels = source.get_pixels();
-
-        fp_pixel_cache = new float[3 * source_width * source_height];
-        int cache_pixel_index = 0;
-        float INV_255 = 1.0f / 255.0f;
-
-        for (int j = 0; j < source_height; j++) {
-            int row_start_index = j * source_rowstride;
-            int row_end_index = row_start_index + (source_width * source_num_channels);
-            for (int i = row_start_index; i < row_end_index; i += source_num_channels) {
-                fp_pixel_cache[cache_pixel_index++] = ((float) source_pixels[i]) * INV_255;
-                fp_pixel_cache[cache_pixel_index++] = ((float) source_pixels[i + 1]) * INV_255;
-                fp_pixel_cache[cache_pixel_index++] = ((float) source_pixels[i + 2]) * INV_255;
-            }
-        }
-    }
 }
 
