292
296
private OneShotScheduler reimport_editable_scheduler = null;
293
297
private OneShotScheduler update_editable_attributes_scheduler = null;
294
298
private OneShotScheduler remove_editable_scheduler = null;
300
// The first time we have to run the pipeline on an image, we'll precache
301
// a copy of the unscaled, unmodified version; this allows us to operate
302
// directly on the image data quickly without re-fetching it at the top
303
// of the pipeline, which can cause significant lag with larger images.
305
// This adds a small amount of (automatically garbage-collected) memory
306
// overhead, but greatly simplifies the pipeline, since scaling can now
307
// be blithely ignored, and most of the pixel operations are fast enough
308
// that the app remains responsive, even with 10MP images.
310
// In order to make sure we discard unneeded precaches in a timely fashion,
311
// we spawn a timer when the unmodified pixbuf is first precached; if the
312
// timer elapses and the pixbuf hasn't been needed again since then, we'll
313
// discard it and free up the memory.
314
private Gdk.Pixbuf unmodified_precached = null;
315
private GLib.Timer secs_since_access = null;
296
317
// RAW only: developed backing photos.
297
318
private Gee.HashMap<RawDeveloper, BackingPhotoRow?>? developments = null;
2243
2259
file_exif_updated();
2246
// Returns cropped and rotated dimensions
2247
public override Dimensions get_dimensions() {
2249
if (get_crop(out crop))
2250
return crop.get_dimensions();
2252
return get_original_dimensions();
2263
* @brief Returns the width and height of the Photo after various
2264
* arbitrary stages of the pipeline have been applied in
2265
* the same order they're applied in get_pixbuf_with_options.
2266
* With no argument passed, it works exactly like the
2267
* previous incarnation did.
2269
* @param disallowed_steps Which pipeline steps should NOT
2270
* be taken into account when computing image dimensions
2271
* (matching the convention set by get_pixbuf_with_options()).
2272
* Pipeline steps that do not affect the image geometry are
2275
public override Dimensions get_dimensions(Exception disallowed_steps = Exception.NONE) {
2276
// The raw dimensions of the incoming image prior to the pipeline.
2277
Dimensions returned_dims = get_raw_dimensions();
2279
// Compute how much the image would be resized by after rotating and/or mirroring.
2280
if (disallowed_steps.allows(Exception.ORIENTATION)) {
2281
Orientation ori_tmp = get_orientation();
2283
// Is this image rotated 90 or 270 degrees?
2285
case Orientation.LEFT_TOP:
2286
case Orientation.RIGHT_TOP:
2287
case Orientation.LEFT_BOTTOM:
2288
case Orientation.RIGHT_BOTTOM:
2289
// Yes, swap width and height of raw dimensions.
2290
int width_tmp = returned_dims.width;
2292
returned_dims.width = returned_dims.height;
2293
returned_dims.height = width_tmp;
2297
// No, only mirrored or rotated 180; do nothing.
2302
// Compute how much the image would be resized by after straightening.
2303
if (disallowed_steps.allows(Exception.STRAIGHTEN)) {
2304
double x_size, y_size;
2307
get_straighten(out angle);
2309
compute_arb_rotated_size(returned_dims.width, returned_dims.height, angle, out x_size, out y_size);
2311
returned_dims.width = (int) (x_size);
2312
returned_dims.height = (int) (y_size);
2315
// Compute how much the image would be resized by after cropping.
2316
if (disallowed_steps.allows(Exception.CROP)) {
2318
if (get_crop(out crop)) {
2319
returned_dims = crop.get_dimensions();
2322
return returned_dims;
2255
2325
// This method *must* be called with row locked.
2923
2993
pixbuf = original_orientation.rotate_pixbuf(pixbuf);
2924
2995
#if MEASURE_PIPELINE
2925
2996
orientation_time = timer.elapsed();
2927
2998
debug("MASTER PIPELINE %s (%s): orientation=%lf total=%lf", to_string(), scaling.to_string(),
2928
2999
orientation_time, total_timer.elapsed());
2934
3005
public override Gdk.Pixbuf get_pixbuf(Scaling scaling) throws Error {
2935
3006
return get_pixbuf_with_options(scaling);
3010
* @brief Populates the cached version of the unmodified image.
3012
public void populate_prefetched() throws Error {
3013
lock (unmodified_precached) {
3014
// If we don't have it already, precache the original...
3015
if (unmodified_precached == null) {
3016
unmodified_precached = load_raw_pixbuf(Scaling.for_original(), Exception.ALL, BackingFetchMode.SOURCE);
3017
secs_since_access = new GLib.Timer();
3018
GLib.Timeout.add_seconds(5, (GLib.SourceFunc)discard_prefetched);
3019
debug("spawning new precache timeout for %s", this.to_string());
3025
* @brief Get a copy of what's in the cache.
3027
* @return A Pixbuf with the image data from unmodified_precached.
3029
public Gdk.Pixbuf? get_prefetched_copy() {
3030
lock (unmodified_precached) {
3031
if (unmodified_precached == null) {
3033
populate_prefetched();
3035
warning("raw pixbuf for %s could not be loaded", this.to_string());
3040
return unmodified_precached.copy();
3045
* @brief Discards the cached version of the unmodified image.
3047
* @param immed Whether the cached version should be discarded now, or not.
3049
public bool discard_prefetched(bool immed = false) {
3050
lock (unmodified_precached) {
3051
if (secs_since_access == null)
3055
if ((secs_since_access.elapsed(out tmp) > PRECACHE_TIME_TO_LIVE) || (immed)) {
3056
debug("pipeline not run in over %d seconds or got immediate command, discarding" +
3057
"cached original for %s",
3058
PRECACHE_TIME_TO_LIVE, to_string());
3059
unmodified_precached = null;
3060
secs_since_access = null;
2938
// Returns a fully transformed and scaled pixbuf. Transformations may be excluded via the mask.
2939
// If the image is smaller than the scaling, it will be returned in its actual size. The
2940
// caller is responsible for scaling thereafter.
2942
// Note that an unscaled fetch can be extremely expensive, and it's far better to specify an
2943
// appropriate scale.
3069
* @brief Returns a fully transformed and scaled pixbuf. Transformations may be excluded via
3070
* the mask. If the image is smaller than the scaling, it will be returned in its actual size.
3071
* The caller is responsible for scaling thereafter.
3073
* @param scaling A scaling object that describes the size the output pixbuf should be.
3074
* @param exceptions The parts of the pipeline that should be skipped; defaults to NONE if
3076
* @param fetch_mode The fetch mode; if left unset, defaults to BASELINE so that
3077
* we get the image exactly as it is in the file.
2944
3079
public Gdk.Pixbuf get_pixbuf_with_options(Scaling scaling, Exception exceptions =
2945
3080
Exception.NONE, BackingFetchMode fetch_mode = BackingFetchMode.BASELINE) throws Error {
2946
3082
#if MEASURE_PIPELINE
2947
3083
Timer timer = new Timer();
2948
3084
Timer total_timer = new Timer();
2959
3095
|| fetch_mode == BackingFetchMode.SOURCE) &&
2960
3096
!is_raw_developer_complete(get_raw_developer()))
2961
3097
set_raw_developer(get_raw_developer());
2963
3099
// to minimize holding the row lock, fetch everything needed for the pipeline up-front
2964
3100
bool is_scaled, is_cropped, is_straightened;
2965
Dimensions scaled_image, scaled_to_viewport;
3101
Dimensions scaled_to_viewport;
2966
3102
Dimensions original = Dimensions();
2967
3103
Dimensions scaled = Dimensions();
2968
3104
EditingTools.RedeyeInstance[] redeye_instances = null;
2970
3106
double straightening_angle;
2971
3107
PixelTransformer transformer = null;
2972
3108
Orientation orientation;
2975
// it's possible for get_raw_pixbuf to not return an image scaled to the spec'd scaling,
2976
// particularly when the raw crop is smaller than the viewport
2977
is_scaled = calculate_pixbuf_dimensions(scaling, exceptions, out scaled_image,
2978
out scaled_to_viewport);
2981
original = get_raw_dimensions();
3111
original = get_dimensions(Exception.ALL);
3112
scaled = scaling.get_scaled_dimensions(get_dimensions(exceptions));
3113
scaled_to_viewport = scaled;
3115
is_scaled = !(get_dimensions().equals(scaled));
2983
3117
redeye_instances = get_raw_redeye_instances();
2985
3119
is_cropped = get_raw_crop(out crop);
2987
3121
is_straightened = get_raw_straighten(out straightening_angle);
2989
3123
if (has_color_adjustments())
2990
3124
transformer = get_pixel_transformer();
2992
3126
orientation = get_orientation();
2996
3130
// Image load-and-decode
2999
Gdk.Pixbuf pixbuf = load_raw_pixbuf(scaling, exceptions, fetch_mode);
3002
scaled = Dimensions.for_pixbuf(pixbuf);
3132
populate_prefetched();
3134
Gdk.Pixbuf pixbuf = get_prefetched_copy();
3136
// remember to delete the cached copy if it isn't being used.
3137
secs_since_access.start();
3138
debug("pipeline being run against %s, timer restarted.", this.to_string());
3140
assert(pixbuf != null);
3005
3143
// Image transformation pipeline
3008
3146
// redeye reduction
3009
3147
if (exceptions.allows(Exception.REDEYE)) {
3010
3149
#if MEASURE_PIPELINE
3013
3152
foreach (EditingTools.RedeyeInstance instance in redeye_instances) {
3014
// redeye is stored in raw coordinates; need to scale to scaled image coordinates
3016
instance.center = coord_scaled_in_space(instance.center.x, instance.center.y,
3018
instance.radius = radius_scaled_in_space(instance.radius, original, scaled);
3019
assert(instance.radius != -1);
3022
3153
pixbuf = do_redeye(pixbuf, instance);
3024
3155
#if MEASURE_PIPELINE
3047
3179
if (is_cropped) {
3048
// crop is stored in raw coordinates; need to scale to scaled image coordinates;
3049
// also, no need to do this if the image itself was unscaled (which can happen
3050
// if the crop is smaller than the viewport)
3052
crop = crop.get_scaled_similar(original, scaled);
3181
// ensure the crop region stays inside the scaled image boundaries and is
3182
// at least 1 px by 1 px; this is needed as a work-around for inaccuracies
3183
// which can occur when zooming.
3184
crop.left = crop.left.clamp(0, pixbuf.width - 2);
3185
crop.top = crop.top.clamp(0, pixbuf.height - 2);
3187
crop.right = crop.right.clamp(crop.left + 1, pixbuf.width - 1);
3188
crop.bottom = crop.bottom.clamp(crop.top + 1, pixbuf.height - 1);
3054
3190
pixbuf = new Gdk.Pixbuf.subpixbuf(pixbuf, crop.left, crop.top, crop.get_width(),
3058
3194
#if MEASURE_PIPELINE
3059
3195
crop_time = timer.elapsed();
3199
// orientation (all modifications are stored in unrotated coordinate system)
3200
if (exceptions.allows(Exception.ORIENTATION)) {
3201
#if MEASURE_PIPELINE
3204
pixbuf = orientation.rotate_pixbuf(pixbuf);
3205
#if MEASURE_PIPELINE
3206
orientation_time = timer.elapsed();
3210
#if MEASURE_PIPELINE
3211
debug("PIPELINE %s (%s): redeye=%lf crop=%lf adjustment=%lf orientation=%lf total=%lf",
3212
to_string(), scaling.to_string(), redeye_time, crop_time, adjustment_time,
3213
orientation_time, total_timer.elapsed());
3216
// scale the scratch image, as needed.
3218
pixbuf = pixbuf.scale_simple(scaled_to_viewport.width, scaled_to_viewport.height, Gdk.InterpType.BILINEAR);
3221
// color adjustment; we do this dead last, since, if an image has been scaled down,
3222
// it may allow us to reduce the amount of pixel arithmetic, increasing responsiveness.
3064
3223
if (exceptions.allows(Exception.ADJUST)) {
3065
3224
#if MEASURE_PIPELINE
3070
3229
#if MEASURE_PIPELINE
3071
3230
adjustment_time = timer.elapsed();
3075
// orientation (all modifications are stored in unrotated coordinate system)
3076
if (exceptions.allows(Exception.ORIENTATION)) {
3077
#if MEASURE_PIPELINE
3080
pixbuf = orientation.rotate_pixbuf(pixbuf);
3081
#if MEASURE_PIPELINE
3082
orientation_time = timer.elapsed();
3086
3234
// This is to verify the generated pixbuf matches the scale requirements; crop, straighten
3087
3235
// and orientation are all transformations that change the dimensions or aspect ratio of
3088
3236
// the pixbuf, and must be accounted for the test to be valid.
3089
3237
if ((is_scaled) && (!is_straightened))
3090
3238
assert(scaled_to_viewport.approx_equals(Dimensions.for_pixbuf(pixbuf), SCALING_FUDGE));
3092
#if MEASURE_PIPELINE
3093
debug("PIPELINE %s (%s): redeye=%lf crop=%lf adjustment=%lf orientation=%lf total=%lf",
3094
to_string(), scaling.to_string(), redeye_time, crop_time, adjustment_time,
3095
orientation_time, total_timer.elapsed());
3739
3886
// Sets the crop against the coordinate system of the rotated photo
3740
public void set_crop(Box crop) {
3741
Dimensions dim = get_raw_dimensions();
3887
public void set_crop(Box crop) {
3888
Dimensions dim = get_dimensions(Exception.CROP | Exception.ORIENTATION);
3742
3889
Orientation orientation = get_orientation();
3744
3891
Box derotated = orientation.derotate_box(dim, crop);
3746
assert(derotated.get_width() <= dim.width);
3747
assert(derotated.get_height() <= dim.height);
3893
derotated.left = derotated.left.clamp(0, dim.width - 2);
3894
derotated.right = derotated.right.clamp(derotated.left, dim.width - 1);
3896
derotated.top = derotated.top.clamp(0, dim.height - 2);
3897
derotated.bottom = derotated.bottom.clamp(derotated.top, dim.height - 1);
3749
3899
set_raw_crop(derotated);
3760
3910
set_raw_straighten(theta);
3763
public void add_redeye_instance(EditingTools.RedeyeInstance inst_unscaled) {
3764
Gdk.Rectangle bounds_rect_unscaled = EditingTools.RedeyeInstance.to_bounds_rect(inst_unscaled);
3765
Gdk.Rectangle bounds_rect_raw = unscaled_to_raw_rect(bounds_rect_unscaled);
3766
EditingTools.RedeyeInstance inst = EditingTools.RedeyeInstance.from_bounds_rect(bounds_rect_raw);
3768
add_raw_redeye_instance(inst);
3771
3913
private Gdk.Pixbuf do_redeye(Gdk.Pixbuf pixbuf, EditingTools.RedeyeInstance inst) {
3772
3914
/* we remove redeye within a circular region called the "effect
3773
3915
extent." the effect extent is inscribed within its "bounding