Photo.vala 207 KB
Newer Older
1
/* Copyright 2016 Software Freedom Conservancy Inc.
2 3
 *
 * This software is licensed under the GNU LGPL (version 2.1 or later).
4
 * See the COPYING file in this distribution.
5 6
 */

7 8
// Specifies how pixel data is fetched from the backing file on disk.  MASTER is the original
// backing photo of any supported photo file format; SOURCE is either the master or the editable
9
// file, that is, the appropriate reference file for user display; BASELINE is an appropriate
10 11
// file with the proviso that it may be a suitable substitute for the master and/or the editable.
// UNMODIFIED represents the photo with no edits, i.e. the head of the pipeline.
12 13
//
// In general, callers want to use the BASELINE unless requirements are specific.
14 15
public enum BackingFetchMode {
    SOURCE,
16
    BASELINE,
17 18
    MASTER,
    UNMODIFIED
19 20
}

21 22 23
public class PhotoImportParams {
    // IN:
    public File file;
24
    public File final_associated_file = null;
25 26
    public ImportID import_id;
    public PhotoFileSniffer.Options sniffer_options;
27 28 29
    public string? exif_md5;
    public string? thumbnail_md5;
    public string? full_md5;
30 31 32 33 34
    
    // IN/OUT:
    public Thumbnails? thumbnails;
    
    // OUT:
35
    public PhotoRow row = new PhotoRow();
36 37
    public Gee.Collection<string>? keywords = null;
    
38 39 40
    public PhotoImportParams(File file, File? final_associated_file, ImportID import_id, 
        PhotoFileSniffer.Options sniffer_options, string? exif_md5, string? thumbnail_md5, string? full_md5, 
        Thumbnails? thumbnails = null) {
41
        this.file = file;
42
        this.final_associated_file = final_associated_file;
43 44
        this.import_id = import_id;
        this.sniffer_options = sniffer_options;
45 46 47
        this.exif_md5 = exif_md5;
        this.thumbnail_md5 = thumbnail_md5;
        this.full_md5 = full_md5;
48 49
        this.thumbnails = thumbnails;
    }
50 51 52 53 54 55 56 57 58 59 60
    
    // Creates a placeholder import.
    public PhotoImportParams.create_placeholder(File file, ImportID import_id) {
        this.file = file;
        this.import_id = import_id;
        this.sniffer_options = PhotoFileSniffer.Options.NO_MD5;
        this.exif_md5 = null;
        this.thumbnail_md5 = null;
        this.full_md5 = null;
        this.thumbnails = null;
    }
61 62
}

63 64 65 66 67 68 69 70 71 72 73 74
public abstract class PhotoTransformationState : Object {
    private bool is_broke = false;
    
    // This signal is fired when the Photo object can no longer accept it and reliably return to
    // this state.
    public virtual signal void broken() {
        is_broke = true;
    }
    
    public bool is_broken() {
        return is_broke;
    }
75 76
}

77 78 79 80 81 82 83 84 85 86 87 88 89 90
public enum Rating {
    REJECTED = -1,
    UNRATED = 0,
    ONE = 1,
    TWO = 2,
    THREE = 3,
    FOUR = 4,
    FIVE = 5;

    public bool can_increase() {
        return this < FIVE;
    }

    public bool can_decrease() {
91
        return this > REJECTED;
92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127
    }

    public bool is_valid() {
        return this >= REJECTED && this <= FIVE;
    }

    public Rating increase() {
        return can_increase() ? this + 1 : this;
    }

    public Rating decrease() {
        return can_decrease() ? this - 1 : this;
    }
    
    public int serialize() {
        switch (this) {
            case REJECTED:
                return -1;
            case UNRATED:
                return 0;
            case ONE:
                return 1;
            case TWO:
                return 2;
            case THREE:
                return 3;
            case FOUR:
                return 4;
            case FIVE:
                return 5;
            default:
                return 0;
        }
    }

    public static Rating unserialize(int value) {
128 129 130 131 132
        if (value > FIVE)
            return FIVE;
        else if (value < REJECTED)
            return REJECTED;
        
133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153
        switch (value) {
            case -1:
                return REJECTED;
            case 0:
                return UNRATED;
            case 1:
                return ONE;
            case 2:
                return TWO;
            case 3:
                return THREE;
            case 4:
                return FOUR;
            case 5:
                return FIVE;
            default:
                return UNRATED;
        }
    }
}

Jim Nelson's avatar
Jim Nelson committed
154
// Photo is an abstract class that allows for applying transformations on-the-fly to a
155
// particular photo without modifying the backing image file.  The interface allows for
156
// transformations to be stored persistently elsewhere or in memory until they're committed en
157
// masse to an image file.
158
public abstract class Photo : PhotoSource, Dateable, Positionable {
159 160 161 162
    // Need to use "thumb" rather than "photo" for historical reasons -- this name is used
    // directly to load thumbnails from disk by already-existing filenames
    public const string TYPENAME = "thumb";

163 164 165 166 167 168 169 170
    private const string[] IMAGE_EXTENSIONS = {
        // raster formats
        "jpg", "jpeg", "jpe",
        "tiff", "tif",
        "png",
        "gif",
        "bmp",
        "ppm", "pgm", "pbm", "pnm",
171 172 173 174 175 176
        
        // THM are JPEG thumbnails produced by some RAW cameras ... want to support the RAW
        // image but not import their thumbnails
        "thm",
        
        // less common
177
        "tga", "ilbm", "pcx", "ecw", "img", "sid", "cd5", "fits", "pgf",
178
        
179 180
        // vector
        "cgm", "svg", "odg", "eps", "pdf", "swf", "wmf", "emf", "xps",
181
        
182 183
        // 3D
        "pns", "jps", "mpo",
184
        
185 186 187
        // RAW extensions
        "3fr", "arw", "srf", "sr2", "bay", "crw", "cr2", "cap", "iiq", "eip", "dcs", "dcr", "drf",
        "k25", "kdc", "dng", "erf", "fff", "mef", "mos", "mrw", "nef", "nrw", "orf", "ptx", "pef",
188
        "pxn", "r3d", "raf", "raw", "rw2", "rwl", "rwz", "x3f", "srw"
189
    };
190
    
191 192 193 194
    // There are assertions in the photo pipeline to verify that the generated (or loaded) pixbuf
    // is scaled properly.  We have to allow for some wobble here because of rounding errors and
    // precision limitations of various subsystems.  Pixel-accuracy would be best, but barring that,
    // need to just make sure the pixbuf is in the ballpark.
195
    private const int SCALING_FUDGE = 64;
196 197 198

    // The number of seconds we should hold onto a precached copy of the original image; if
    // it hasn't been accessed in this many seconds, discard it to conserve memory.
199 200
    private const int SOURCE_PIXBUF_TIME_TO_LIVE_SEC = 10;
    
201 202 203
    // min and max size of source pixbuf cache LRU
    private const int SOURCE_PIXBUF_MIN_LRU_COUNT = 1;
    private const int SOURCE_PIXBUF_MAX_LRU_COUNT = 3;
204
    
205 206 207 208 209
    // Minimum raw embedded preview size we're willing to accept; any smaller than this, and 
    // it's probably intended primarily for use only as a thumbnail and won't look good on the
    // PhotoPage.
    private const int MIN_EMBEDDED_SIZE = 1024;
    
210 211 212 213 214
    // Here, we cache the exposure time to avoid paying to access the row every time we
    // need to know it. This is initially set in the constructor, and updated whenever
    // the exposure time is set (please see set_exposure_time() for details).
    private time_t cached_exposure_time;
    
215 216 217 218 219 220
    public enum Exception {
        NONE            = 0,
        ORIENTATION     = 1 << 0,
        CROP            = 1 << 1,
        REDEYE          = 1 << 2,
        ADJUST          = 1 << 3,
221
        STRAIGHTEN      = 1 << 4,
222 223
        ALL             = 0xFFFFFFFF;
        
224
        public bool prohibits(Exception exception) {
225 226 227
            return ((this & exception) != 0);
        }
        
228
        public bool allows(Exception exception) {
229 230
            return ((this & exception) == 0);
        }
231
    }
232
    
233
    // NOTE: This class should only be instantiated when row is locked.
234
    private class PhotoTransformationStateImpl : PhotoTransformationState {
Jim Nelson's avatar
Jim Nelson committed
235
        private Photo photo;
236 237
        private Orientation orientation;
        private Gee.HashMap<string, KeyValueMap>? transformations;
238 239
        private PixelTransformer? transformer;
        private PixelTransformationBundle? adjustments;
240
        
Jim Nelson's avatar
Jim Nelson committed
241
        public PhotoTransformationStateImpl(Photo photo, Orientation orientation,
242 243 244 245 246 247 248 249
            Gee.HashMap<string, KeyValueMap>? transformations, PixelTransformer? transformer,
            PixelTransformationBundle? adjustments) {
            this.photo = photo;
            this.orientation = orientation;
            this.transformations = copy_transformations(transformations);
            this.transformer = transformer;
            this.adjustments = adjustments;
            
250
            photo.baseline_replaced.connect(on_photo_baseline_replaced);
251 252 253
        }
        
        ~PhotoTransformationStateImpl() {
254
            photo.baseline_replaced.disconnect(on_photo_baseline_replaced);
255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282
        }
        
        public Orientation get_orientation() {
            return orientation;
        }
        
        public Gee.HashMap<string, KeyValueMap>? get_transformations() {
            return copy_transformations(transformations);
        }
        
        public PixelTransformer? get_transformer() {
            return (transformer != null) ? transformer.copy() : null;
        }
        
        public PixelTransformationBundle? get_color_adjustments() {
            return (adjustments != null) ? adjustments.copy() : null;
        }
        
        private static Gee.HashMap<string, KeyValueMap>? copy_transformations(
            Gee.HashMap<string, KeyValueMap>? original) {
            if (original == null)
                return null;
            
            Gee.HashMap<string, KeyValueMap>? clone = new Gee.HashMap<string, KeyValueMap>();
            foreach (string object in original.keys)
                clone.set(object, original.get(object).copy());
            
            return clone;
283
        }
284 285 286 287 288 289 290
        
        private void on_photo_baseline_replaced() {
            if (!is_broken())
                broken();
        }
    }
    
291
    private class BackingReaders {
292
        public PhotoFileReader master;
293
        public PhotoFileReader developer;
294
        public PhotoFileReader editable;
295
    }
296
    
297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325
    private class CachedPixbuf {
        public Photo photo;
        public Gdk.Pixbuf pixbuf;
        public Timer last_touched = new Timer();
        
        public CachedPixbuf(Photo photo, Gdk.Pixbuf pixbuf) {
            this.photo = photo;
            this.pixbuf = pixbuf;
        }
    }
    
    // The first time we have to run the pipeline on an image, we'll precache
    // a copy of the unscaled, unmodified version; this allows us to operate
    // directly on the image data quickly without re-fetching it at the top
    // of the pipeline, which can cause significant lag with larger images.
    //
    // This adds a small amount of (automatically garbage-collected) memory
    // overhead, but greatly simplifies the pipeline, since scaling can now
    // be blithely ignored, and most of the pixel operations are fast enough
    // that the app remains responsive, even with 10MP images.
    //
    // In order to make sure we discard unneeded precaches in a timely fashion,
    // we spawn a timer when the unmodified pixbuf is first precached; if the
    // timer elapses and the pixbuf hasn't been needed again since then, we'll
    // discard it and free up the memory.  The cache also has an LRU to prevent
    // runaway amounts of memory from being stored (see SOURCE_PIXBUF_LRU_COUNT)
    private static Gee.LinkedList<CachedPixbuf>? source_pixbuf_cache = null;
    private static uint discard_source_id = 0;
    
326 327
    // because fetching individual items from the database is high-overhead, store all of
    // the photo row in memory
328
    protected PhotoRow row;
329
    private BackingPhotoRow editable = new BackingPhotoRow();
330
    private BackingReaders readers = new BackingReaders();
331
    private PixelTransformer transformer = null;
332
    private PixelTransformationBundle adjustments = null;
333
    // because file_title is determined by data in row, it should only be accessed when row is locked
334
    private string file_title = null;
335 336 337 338
    private FileMonitor editable_monitor = null;
    private OneShotScheduler reimport_editable_scheduler = null;
    private OneShotScheduler update_editable_attributes_scheduler = null;
    private OneShotScheduler remove_editable_scheduler = null;
339 340
    
    protected bool can_rotate_now = true;
341
    
342 343 344
    // RAW only: developed backing photos.
    private Gee.HashMap<RawDeveloper, BackingPhotoRow?>? developments = null;
    
345 346 347
    // Set to true if we want to develop RAW photos into new files.
    public static bool develop_raw_photos_to_files { get; set; default = false; }
    
348
    // This pointer is used to determine which BackingPhotoRow in the PhotoRow to be using at
349
    // any time.  It should only be accessed -- read or write -- when row is locked.
350
    protected BackingPhotoRow? backing_photo_row = null;
351
    
352 353 354 355 356 357
    // This is fired when the photo's editable file is replaced.  The image it generates may or
    // may not be the same; the altered signal is best for that.  null is passed if the editable
    // is being added, replaced, or removed (in the appropriate places)
    public virtual signal void editable_replaced(File? old_file, File? new_file) {
    }
    
358 359 360 361 362
    // Fired when one or more of the photo's RAW developments has been changed.  This will only
    // be fired on RAW photos, and only when a development has been added or removed.
    public virtual signal void raw_development_modified() {
    }
    
363 364 365 366 367 368
    // This is fired when the photo's baseline file (the file that generates images at the head
    // of the pipeline) is replaced.  Photo will make every sane effort to only fire this signal
    // if the new baseline is the same image-wise (i.e. the pixbufs it generates are essentially
    // the same).
    public virtual signal void baseline_replaced() {
    }
369
    
370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393
    // This is fired when the photo's master is reimported in place.  It's fired after all changes
    // to the Photo's state have been incorporated into the object and the "altered" signal has
    // been fired notifying of the various details that have changed.
    public virtual signal void master_reimported(PhotoMetadata? metadata) {
    }
    
    // Like "master-reimported", but when a photo's editable has been reimported.
    public virtual signal void editable_reimported(PhotoMetadata? metadata) {
    }
    
    // Like "master-reimported" but when the baseline file has been reimported.  Note that this
    // could be the master file OR the editable file.
    //
    // See BackingFetchMode for more details.
    public virtual signal void baseline_reimported(PhotoMetadata? metadata) {
    }
    
    // Like "master-reimported" but when the source file has been reimported.  Note that this could
    // be the master file OR the editable file.
    //
    // See BackingFetchMode for more details.
    public virtual signal void source_reimported(PhotoMetadata? metadata) {
    }
    
Jim Nelson's avatar
Jim Nelson committed
394
    // The key to this implementation is that multiple instances of Photo with the
395
    // same PhotoID cannot exist; it is up to the subclasses to ensure this.
Jim Nelson's avatar
Jim Nelson committed
396
    protected Photo(PhotoRow row) {
397
        this.row = row;
398
        
399 400
        // normalize user text
        this.row.title = prep_title(this.row.title);
401
        this.row.comment = prep_comment(this.row.comment);
402
        
403 404 405
        // don't need to lock the struct in the constructor (and to do so would hurt startup
        // time)
        readers.master = row.master.file_format.create_reader(row.master.filepath);
406
        
407
        // get the file title of the Photo without using a File object, skipping the separator itself
408
        string? basename = String.sliced_at_last_char(row.master.filepath, Path.DIR_SEPARATOR);
409
        if (basename != null)
410
            file_title = String.sliced_at(basename, 1);
411
        
412 413 414 415
        if (is_string_empty(file_title))
            file_title = row.master.filepath;
        
        if (row.editable_id.id != BackingPhotoID.INVALID) {
416 417 418
            BackingPhotoRow? e = get_backing_row(row.editable_id);
            if (e != null) {
                editable = e;
419 420 421
                readers.editable = editable.file_format.create_reader(editable.filepath);
            } else {
                try {
422
                    PhotoTable.get_instance().detach_editable(this.row);
423 424 425 426 427 428
                } catch (DatabaseError err) {
                    // ignored
                }
                
                // need to remove all transformations as they're keyed to the editable's
                // coordinate system
429
                remove_all_transformations(false);
430 431 432
            }
        }
        
433 434 435 436 437 438 439 440 441 442 443 444 445
        if (row.master.file_format == PhotoFileFormat.RAW) {
            // Fetch development backing photos for RAW.
            developments = new Gee.HashMap<RawDeveloper, BackingPhotoRow?>();
            foreach (RawDeveloper d in RawDeveloper.as_array()) {
                BackingPhotoID id = row.development_ids[d];
                if (id.id != BackingPhotoID.INVALID) {
                    BackingPhotoRow? bpr = get_backing_row(id);
                    if (bpr != null)
                        developments.set(d, bpr);
                }
            }
        }
        
446 447 448 449 450 451
        // Set up reader for developer.
        if (row.master.file_format == PhotoFileFormat.RAW && developments.has_key(row.developer)) {
            BackingPhotoRow r = developments.get(row.developer);
            readers.developer = r.file_format.create_reader(r.filepath);
        }
        
452 453 454 455 456 457 458 459 460
        // Set the backing photo state appropriately.
        if (readers.editable != null) {
            backing_photo_row = this.editable; 
        } else if (row.master.file_format != PhotoFileFormat.RAW) {
            backing_photo_row = this.row.master;
        } else {
            // For RAW photos, the backing photo is either the editable (above) or
            // the selected raw development.
            if (developments.has_key(row.developer)) {
461
                backing_photo_row = developments.get(row.developer);
462
            } else {
463
                // Use row's backing photo.
464 465 466
                backing_photo_row = this.row.master;
            }
        }
467 468

        cached_exposure_time = this.row.exposure_time;
469 470
    }
    
471 472 473 474 475 476 477 478 479 480 481 482 483
    protected static void init_photo() {
        source_pixbuf_cache = new Gee.LinkedList<CachedPixbuf>();
    }
    
    protected static void terminate_photo() {
        source_pixbuf_cache = null;
        
        if (discard_source_id != 0) {
            Source.remove(discard_source_id);
            discard_source_id = 0;
        }
    }
    
484 485 486 487
    protected virtual void notify_editable_replaced(File? old_file, File? new_file) {
        editable_replaced(old_file, new_file);
    }
    
488 489 490 491
    protected virtual void notify_raw_development_modified() {
        raw_development_modified();
    }
    
492
    protected virtual void notify_baseline_replaced() {
493 494 495
        baseline_replaced();
    }
    
496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511
    protected virtual void notify_master_reimported(PhotoMetadata? metadata) {
        master_reimported(metadata);
    }
    
    protected virtual void notify_editable_reimported(PhotoMetadata? metadata) {
        editable_reimported(metadata);
    }
    
    protected virtual void notify_source_reimported(PhotoMetadata? metadata) {
        source_reimported(metadata);
    }
    
    protected virtual void notify_baseline_reimported(PhotoMetadata? metadata) {
        baseline_reimported(metadata);
    }
    
512
    public override bool internal_delete_backing() throws Error {
513
        bool ret = true;
514 515 516 517 518 519 520 521
        File file = null;
        lock (readers) {
            if (readers.editable != null)
                file = readers.editable.get_file();
        }
        
        detach_editable(true, false);
        
522 523 524 525 526 527
        if (get_master_file_format() == PhotoFileFormat.RAW) {
            foreach (RawDeveloper d in RawDeveloper.as_array()) {
                delete_raw_development(d);
            }
        }
        
528 529
        if (file != null) {
            try {
530
                ret = file.trash(null);
531
            } catch (Error err) {
532 533 534
                ret = false;
                message("Unable to move editable %s for %s to trash: %s", file.get_path(), 
                    to_string(), err.message);
535 536 537
            }
        }
        
538 539
        // Return false if parent method failed.
        return base.internal_delete_backing() && ret;
540 541
    }
    
542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561
    // Fetches the backing state.  If it can't be read, the ID is flushed from the database
    // for safety.  If the ID is invalid or any error occurs, null is returned.
    private BackingPhotoRow? get_backing_row(BackingPhotoID id) {
        if (id.id == BackingPhotoID.INVALID)
            return null;
        
        BackingPhotoRow? backing_row = null;
        try {
            backing_row = BackingPhotoTable.get_instance().fetch(id);
        } catch (DatabaseError err) {
            warning("Unable to fetch backing state for %s: %s", to_string(), err.message);
        }
        
        if (backing_row == null) {
            try {
                BackingPhotoTable.get_instance().remove(id);
            } catch (DatabaseError err) {
                // ignored
            }
            return null;
562 563
        }
        
564 565 566
        return backing_row;
    }
    
567 568
    // Returns true if the given raw development was already made and the developed image 
    // exists on disk.
569 570
    public bool is_raw_developer_complete(RawDeveloper d) {
        lock (developments) {
571 572
            return developments.has_key(d) &&
                FileUtils.test(developments.get(d).filepath, FileTest.EXISTS);
573 574 575
        }
    }
    
576 577
    // Determines whether a given RAW developer is available for this photo.
    public bool is_raw_developer_available(RawDeveloper d) {
578 579 580 581
        lock (developments) {
            if (developments.has_key(d))
                return true;
        }
582 583 584 585 586 587 588 589 590 591 592
        
        switch (d) {
            case RawDeveloper.SHOTWELL:
                return true;
                
            case RawDeveloper.CAMERA:
                return false;
            
            case RawDeveloper.EMBEDDED:
                try {
                    PhotoMetadata meta = get_master_metadata();
593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609
                    uint num_previews = meta.get_preview_count();
                    
                    if (num_previews > 0) {
                        PhotoPreview? prev = meta.get_preview(num_previews - 1);

                        // Embedded preview could not be fetched?
                        if (prev == null)
                            return false;
                        
                        Dimensions dims = prev.get_pixel_dimensions();
                        
                        // Largest embedded preview was an unacceptable size?
                        int preview_major_axis = (dims.width > dims.height) ? dims.width : dims.height;
                        if (preview_major_axis < MIN_EMBEDDED_SIZE)
                            return false;
                        
                        // Preview was a supported size, use it.
610
                        return true;
611 612 613 614
                    }
                    
                    // Image has no embedded preview at all.
                    return false;
615 616 617 618 619 620 621 622 623 624 625
                } catch (Error e) {
                    debug("Error accessing embedded preview. Message: %s", e.message);
                }
                return false;
            
            default:
                assert_not_reached();
        }
    }
    
    // Reads info on a backing photo and adds it.
626 627
    // Note: this function was created for importing new photos.  It will not
    // notify of changes to the developments.
628
    public void add_backing_photo_for_development(RawDeveloper d, BackingPhotoRow bpr, bool notify = true) throws Error {
629
        import_developed_backing_photo(row, d, bpr);
630 631 632
        lock (developments) {
            developments.set(d, bpr);
        }
633 634 635

        if (notify)
            notify_altered(new Alteration("image", "developer"));
636 637
    }
    
638
    public static void import_developed_backing_photo(PhotoRow row, RawDeveloper d, 
639 640 641 642
        BackingPhotoRow bpr) throws Error {
        File file = File.new_for_path(bpr.filepath);
        FileInfo info = file.query_info(DirectoryMonitor.SUPPLIED_ATTRIBUTES,
            FileQueryInfoFlags.NOFOLLOW_SYMLINKS, null);
643
        TimeVal timestamp = info.get_modification_time();
644 645 646 647 648 649
        
        PhotoFileInterrogator interrogator = new PhotoFileInterrogator(
            file, PhotoFileSniffer.Options.GET_ALL);
        interrogator.interrogate();
        
        DetectedPhotoInformation? detected = interrogator.get_detected_photo_information();
650 651 652 653 654 655
        if (detected == null || interrogator.get_is_photo_corrupted()) {
            // TODO: Probably should remove from database, but simply exiting for now (prior code
            // didn't even do this check)
            return;
        }
        
656 657 658 659 660 661 662 663
        bpr.dim = detected.image_dim;
        bpr.filesize = info.get_size();
        bpr.timestamp = timestamp.tv_sec;
        bpr.original_orientation = detected.metadata != null ? detected.metadata.get_orientation() : 
            Orientation.TOP_LEFT;
        
        // Add to DB.
        BackingPhotoTable.get_instance().add(bpr);
664
        PhotoTable.get_instance().update_raw_development(row, d, bpr.id);
665 666 667
    }
    
    // "Develops" a raw photo
668
    // Not thread-safe.
669
    private void develop_photo(RawDeveloper d, bool notify) {
670 671 672
        bool wrote_img_to_disk = false;
        BackingPhotoRow bps = null;
        
673 674 675 676
        switch (d) {
            case RawDeveloper.SHOTWELL:
                try {
                    // Create file and prep.
677
                    bps = d.create_backing_row_for_development(row.master.filepath);
678 679
                    Gdk.Pixbuf? pix = null;
                    lock (readers) {
680 681 682 683 684
                        // Don't rotate this pixbuf before writing it out. We don't
                        // need to because we'll display it using the orientation
                        // from the parent raw file, so rotating it here would cause
                        // portrait images to rotate _twice_...
                        pix = get_master_pixbuf(Scaling.for_original(), false);
685 686 687 688 689 690 691 692 693 694
                    }
                    
                    if (pix == null) {
                        debug("Could not get preview pixbuf");
                        return;
                    }
                    
                    // Write out the JPEG.
                    PhotoFileWriter writer = PhotoFileFormat.JFIF.create_writer(bps.filepath);
                    writer.write(pix, Jpeg.Quality.HIGH);
695 696 697 698 699 700 701 702 703 704 705 706
                    
                    // Remember that we wrote it (we'll only get here if writing
                    // the jpeg doesn't throw an exception).  We do this because
                    // some cameras' output has non-spec-compliant exif segments
                    // larger than 64k (exiv2 can't cope with this), so saving
                    // metadata to the development could fail, but we want to use
                    // it anyway since the image portion is still valid...
                    wrote_img_to_disk = true;
                    
                    // Write out metadata. An exception could get thrown here as
                    // well, hence the separate check for being able to save the
                    // image above...
707 708 709
                    PhotoMetadata meta = get_master_metadata();
                    PhotoFileMetadataWriter mwriter = PhotoFileFormat.JFIF.create_metadata_writer(bps.filepath);
                    mwriter.write_metadata(meta);
710 711
                } catch (Error err) {
                    debug("Error developing photo: %s", err.message);
712 713 714 715
                } finally {
                    if (wrote_img_to_disk) {
                        try {
                            // Read in backing photo info, add to DB.
716
                            add_backing_photo_for_development(d, bps, notify);
717 718 719 720 721 722 723
                            
                            notify_raw_development_modified();
                        } catch (Error e) {
                            debug("Error adding backing photo as development. Message: %s",
                                e.message);
                        }
                    }
724
                }
725
                
726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744
                break;
                
            case RawDeveloper.CAMERA:
                // No development needed.
                break;
                
            case RawDeveloper.EMBEDDED:
                try {
                    // Read in embedded JPEG.
                    PhotoMetadata meta = get_master_metadata();
                    uint c = meta.get_preview_count();
                    if (c <= 0)
                        return;
                    PhotoPreview? prev = meta.get_preview(c - 1);
                    if (prev == null) {
                        debug("Could not get preview from metadata");
                        return;
                    }
                    
745
                    var pix = prev.flatten();
746 747 748 749
                    if (pix == null) {
                        debug("Could not get preview pixbuf");
                        return;
                    }
750

751
                    // Write out file.
752
                    bps = d.create_backing_row_for_development(row.master.filepath);
753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771

                    // Peek at data. If we really have a JPEG image, just use it,
                    // otherwise do GdkPixbuf roundtrip
                    if (Jpeg.is_jpeg_bytes(pix)) {
                        var outfile = File.new_for_path(bps.filepath);
                        outfile.replace_contents(pix.get_data(), null,
                                false, FileCreateFlags.NONE, null);
                    } else {
                        var pixbuf = prev.get_pixbuf();
                        if (pixbuf == null) {
                            debug("Could not get preview pixbuf");
                            return;
                        }

                        var writer = PhotoFileFormat.JFIF.create_writer(bps.filepath);
                        writer.write(pixbuf, Jpeg.Quality.HIGH);
                    }


772 773 774 775
                    // Remember that we wrote it (see above
                    // case for why this is necessary).
                    wrote_img_to_disk = true;
                    
776 777 778
                    // Write out metadata
                    PhotoFileMetadataWriter mwriter = PhotoFileFormat.JFIF.create_metadata_writer(bps.filepath);
                    mwriter.write_metadata(meta);
779 780 781
                } catch (Error e) {
                    debug("Error accessing embedded preview. Message: %s", e.message);
                    return;
782 783 784 785
                } finally {
                    if (wrote_img_to_disk) {
                        try {
                            // Read in backing photo info, add to DB.
786
                            add_backing_photo_for_development(d, bps, notify);
787 788 789 790 791 792 793
                            
                            notify_raw_development_modified();
                        } catch (Error e) {
                            debug("Error adding backing photo as development. Message: %s",
                                e.message);
                        }
                    }
794 795 796 797 798 799 800 801
                }
                break;
            
            default:
                assert_not_reached();
        }
    }
    
802 803 804 805 806 807 808 809
    // Sets the developer internally, but does not actually develop the backing file.
    public void set_default_raw_developer(RawDeveloper d) {
        lock (row) {
            row.developer = d;
        }
    }
    
    // Sets the developer and develops the photo.
810
    public void set_raw_developer(RawDeveloper d, bool notify = true) {
811
        if (get_master_file_format() != PhotoFileFormat.RAW)
812
            return;
813 814 815 816 817 818
        
        // If the caller has asked for 'embedded', but there's a camera development
        // available, always prefer that instead, as it's likely to be of higher
        // quality and resolution.
        if (is_raw_developer_available(RawDeveloper.CAMERA) && (d == RawDeveloper.EMBEDDED))
            d = RawDeveloper.CAMERA;
819
            
820 821
        // If the embedded preview is too small to be used in the PhotoPage, don't
        // allow EMBEDDED to be chosen.
822
        if (!is_raw_developer_available(RawDeveloper.EMBEDDED) && d != RawDeveloper.CAMERA)
823 824
            d = RawDeveloper.SHOTWELL;
            
825
        lock (developments) {
826 827
            RawDeveloper stale_raw_developer = row.developer;
            
828
            // Perform development, bail out if it doesn't work.
829
            if (!is_raw_developer_complete(d)) {
830
                develop_photo(d, notify);
831
            }
832 833
            if (!developments.has_key(d))
                return; // we tried!
834 835 836 837 838 839
            
            // Disgard changes.
            revert_to_master(false);
            
            // Switch master to the new photo.
            row.developer = d;
840 841
            backing_photo_row = developments.get(d);
            readers.developer = backing_photo_row.file_format.create_reader(backing_photo_row.filepath);
842 843 844 845 846 847 848 849

            try {
                get_prefetched_copy();
            } catch (Error e) {
                // couldn't reload the freshly-developed image, nothing to display
                return;
            }

850 851 852
            set_orientation(backing_photo_row.original_orientation);
            
            try {
853
                PhotoTable.get_instance().update_raw_development(row, d, backing_photo_row.id);
854 855 856
            } catch (Error e) {
                warning("Error updating database: %s", e.message);
            }
857 858 859 860 861 862 863 864 865 866 867 868 869 870
            
            // Is the 'stale' development _NOT_ a camera-supplied one?
            //
            // NOTE: When a raw is first developed, both 'stale' and 'incoming' developers
            // will be the same, so the second test is required for correct operation.
            if ((stale_raw_developer != RawDeveloper.CAMERA) &&
                (stale_raw_developer != row.developer)) {
                // The 'stale' non-Shotwell development we're using was
                // created by us, not the camera, so discard it...
                delete_raw_development(stale_raw_developer);
            }
            
            // Otherwise, don't delete the paired JPEG, since it is user/camera-created
            // and is to be preserved.
871 872
        }
        
873 874
        if (notify)
            notify_altered(new Alteration("image", "developer"));
875
        discard_prefetched();
876
    }
877

878 879 880
    public RawDeveloper get_raw_developer() {
        return row.developer;
    }
881

882 883 884 885 886
    // Removes a development from the database, filesystem, etc.
    // Returns true if a development was removed, otherwise false.
    private bool delete_raw_development(RawDeveloper d) {
        bool ret = false;
        
887 888 889 890
        lock (developments) {
            if (!developments.has_key(d))
                return false;
            
891 892
            // Remove file.  If this is a camera-generated JPEG, we trash it;
            // otherwise, it was generated by us and should be deleted outright.
893 894
            debug("Delete raw development: %s %s", this.to_string(), d.to_string());
            BackingPhotoRow bpr = developments.get(d);
895 896 897
            if (bpr.filepath != null) {
                File f = File.new_for_path(bpr.filepath);
                try {
898 899 900 901
                    if (d == RawDeveloper.CAMERA)
                        f.trash();
                    else
                        f.delete();
902 903
                } catch (Error e) {
                    warning("Unable to delete RAW development: %s error: %s", bpr.filepath, e.message);
904 905 906
                }
            }
            
907
            // Delete references in DB.
908
            try {
909
                PhotoTable.get_instance().remove_development(row, d);
910 911 912 913 914 915 916 917
                BackingPhotoTable.get_instance().remove(bpr.id);
            } catch (Error e) {
                warning("Database error while deleting RAW development: %s", e.message);
            }
            
            ret = developments.unset(d);
        }
        
918
        notify_raw_development_modified();
919 920 921 922 923
        return ret;
    }
    
    // Re-do development for photo.
    public void redevelop_raw(RawDeveloper d) {
924 925 926 927 928 929 930 931
        lock (developments) {
            delete_raw_development(d);
            RawDeveloper dev = d;
            if (dev == RawDeveloper.CAMERA)
                dev = RawDeveloper.EMBEDDED;
            
            set_raw_developer(dev);
        }
932 933
    }
    
934 935 936
    public override BackingFileState[] get_backing_files_state() {
        BackingFileState[] backing = new BackingFileState[0];
        lock (row) {
937
            backing += new BackingFileState.from_photo_row(row.master, row.md5);
938
            if (has_editable())
939
                backing += new BackingFileState.from_photo_row(editable, null);
940 941 942 943 944 945 946 947 948 949
            
            if (is_developed()) {
                Gee.Collection<BackingPhotoRow>? dev_rows = get_raw_development_photo_rows();
                if (dev_rows != null) {
                    foreach (BackingPhotoRow r in dev_rows) {
                        debug("adding: %s", r.filepath);
                        backing += new BackingFileState.from_photo_row(r, null);
                    }
                }
            }
950 951 952 953 954
        }
        
        return backing;
    }
    
955 956 957 958 959 960 961 962 963 964 965
    private PhotoFileReader get_backing_reader(BackingFetchMode mode) {
        switch (mode) {
            case BackingFetchMode.MASTER:
                return get_master_reader();
            
            case BackingFetchMode.BASELINE:
                return get_baseline_reader();
            
            case BackingFetchMode.SOURCE:
                return get_source_reader();
            
966 967 968 969 970 971
            case BackingFetchMode.UNMODIFIED:
                if (this.get_master_file_format() == PhotoFileFormat.RAW)
                    return get_raw_developer_reader();
                else
                    return get_master_reader();
            
972 973 974 975 976
            default:
                error("Unknown backing fetch mode %s", mode.to_string());
        }
    }
    
977 978 979 980 981 982 983 984 985 986 987 988
    private PhotoFileReader get_master_reader() {
        lock (readers) {
            return readers.master;
        }
    }
    
    protected PhotoFileReader? get_editable_reader() {
        lock (readers) {
            return readers.editable;
        }
    }
    
989
    // Returns a reader for the head of the pipeline.
990 991 992 993 994
    private PhotoFileReader get_baseline_reader() {
        lock (readers) {
            if (readers.editable != null)
                return readers.editable;
            
995 996
            if (readers.developer != null)
                return readers.developer;
997 998 999 1000 1001
            
            return readers.master;
        }
    }
    
1002
    // Returns a reader for the photo file that is the source of the image.
1003 1004
    private PhotoFileReader get_source_reader() {
        lock (readers) {
1005 1006 1007 1008 1009 1010 1011
            if (readers.editable != null)
                return readers.editable;
            
            if (readers.developer != null)
                return readers.developer;
            
            return readers.master;
1012 1013
        }
    }
1014
    
1015 1016 1017 1018 1019 1020 1021
    // Returns the reader used for reading the RAW development.
    private PhotoFileReader get_raw_developer_reader() {
        lock (readers) {
            return readers.developer;
        }
    }
    
1022
    public bool is_developed() {
1023
        lock (readers) {
1024
            return readers.developer != null;
1025 1026 1027 1028 1029 1030 1031 1032 1033
        }
    }
    
    public bool has_editable() {
        lock (readers) {
            return readers.editable != null;
        }
    }
    
1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046
    public bool does_master_exist() {
        lock (readers) {
            return readers.master.file_exists();
        }
    }
    
    // Returns false if the backing editable does not exist OR the photo does not have an editable
    public bool does_editable_exist() {
        lock (readers) {
            return readers.editable != null ? readers.editable.file_exists() : false;
        }
    }
    
1047 1048
    public bool is_master_baseline() {
        lock (readers) {
1049
            return readers.editable == null;
1050 1051 1052
        }
    }
    
1053 1054 1055 1056
    public bool is_master_source() {
        return !has_editable();
    }
    
1057 1058
    public bool is_editable_baseline() {
        lock (readers) {
1059
            return readers.editable != null;
1060 1061 1062
        }
    }
    
1063 1064 1065 1066
    public bool is_editable_source() {
        return has_editable();
    }
    
1067
    public BackingPhotoRow get_master_photo_row() {
1068 1069 1070 1071 1072
        lock (row) {
            return row.master;
        }
    }
    
1073
    public BackingPhotoRow? get_editable_photo_row() {
1074 1075 1076 1077 1078 1079 1080 1081 1082
        lock (row) {
            // ternary doesn't work here
            if (row.editable_id.is_valid())
                return editable;
            else
                return null;
        }
    }
    
1083 1084 1085 1086 1087 1088
    public Gee.Collection<BackingPhotoRow>? get_raw_development_photo_rows() {
        lock (row) {
            return developments != null ? developments.values : null;
        }
    }
    
1089 1090 1091 1092 1093 1094
    public BackingPhotoRow? get_raw_development_photo_row(RawDeveloper d) {
        lock (row) {
            return developments != null ? developments.get(d) : null;
        }
    }
    
1095 1096 1097 1098 1099 1100 1101 1102 1103
    public PhotoFileFormat? get_editable_file_format() {
        PhotoFileReader? reader = get_editable_reader();
        if (reader == null)
            return null;
        
        // ternary operator doesn't work here
        return reader.get_file_format();
    }
    
1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 1115 1116 1117 1118 1119 1120 1121 1122 1123 1124 1125 1126 1127 1128 1129 1130 1131 1132 1133 1134 1135 1136 1137 1138 1139 1140 1141 1142 1143 1144 1145 1146
    public PhotoFileFormat get_export_format_for_parameters(ExportFormatParameters params) {
        PhotoFileFormat result = PhotoFileFormat.get_system_default_format();

        switch (params.mode) {
            case ExportFormatMode.UNMODIFIED:
                result = get_master_file_format();
            break;
            
            case ExportFormatMode.CURRENT:
                result = get_best_export_file_format();
            break;
            
            case ExportFormatMode.SPECIFIED:
                result = params.specified_format;
            break;
            
            default:
                error("get_export_format_for_parameters: unsupported export format mode");
        }
        
        return result;
    }
    
    public string get_export_basename_for_parameters(ExportFormatParameters params) {
        string? result = null;

        switch (params.mode) {
            case ExportFormatMode.UNMODIFIED:
                result = get_master_file().get_basename();
            break;
            
            case ExportFormatMode.CURRENT:
            case ExportFormatMode.SPECIFIED:
                return get_export_basename(get_export_format_for_parameters(params));
            
            default:
                error("get_export_basename_for_parameters: unsupported export format mode");
        }

        assert (result != null);
        return result;
    }
    
1147 1148 1149 1150 1151 1152 1153 1154 1155 1156 1157 1158
    // This method interrogates the specified file and returns a PhotoRow with all relevant
    // information about it.  It uses the PhotoFileInterrogator to do so.  The caller should create
    // a PhotoFileInterrogator with the proper options prior to calling.  prepare_for_import()
    // will determine what's been discovered and fill out in the PhotoRow or return the relevant
    // objects and information.  If Thumbnails is not null, thumbnails suitable for caching or
    // framing will be returned as well.  Note that this method will call interrogate() and
    // perform all error-handling; the caller simply needs to construct the object.
    //
    // This is the acid-test; if unable to generate a pixbuf or thumbnails, that indicates the 
    // photo itself is bogus and should be discarded.
    //
    // NOTE: This method is thread-safe.
1159
    public static ImportResult prepare_for_import(PhotoImportParams params) {
1160 1161 1162
#if MEASURE_IMPORT
        Timer total_time = new Timer();
#endif
1163 1164
        File file = params.file;
        
1165 1166
        FileInfo info = null;
        try {
1167 1168
            info = file.query_info(DirectoryMonitor.SUPPLIED_ATTRIBUTES,
                FileQueryInfoFlags.NOFOLLOW_SYMLINKS, null);
1169 1170 1171 1172 1173 1174 1175
        } catch (Error err) {
            return ImportResult.FILE_ERROR;
        }
        
        if (info.get_file_type() != FileType.REGULAR)
            return ImportResult.NOT_A_FILE;
        
1176 1177 1178 1179 1180 1181
        if (!is_file_image(file)) {
            message("Not importing %s: Not an image file", file.get_path());
            
            return ImportResult.NOT_AN_IMAGE;
        }

1182
        if (!PhotoFileFormat.is_file_supported(file)) {
1183 1184
            message("Not importing %s: Unsupported extension", file.get_path());
            
1185 1186 1187
            return ImportResult.UNSUPPORTED_FORMAT;
        }
        
1188
        TimeVal timestamp = info.get_modification_time();
1189
        
1190 1191 1192 1193
        // if all MD5s supplied, don't sniff for them
        if (params.exif_md5 != null && params.thumbnail_md5 != null && params.full_md5 != null)
            params.sniffer_options |= PhotoFileSniffer.Options.NO_MD5;
        
1194
        // interrogate file for photo information
1195
        PhotoFileInterrogator interrogator = new PhotoFileInterrogator(file, params.sniffer_options);
1196
        try {
1197
            interrogator.interrogate();
1198 1199 1200 1201 1202 1203
        } catch (Error err) {
            warning("Unable to interrogate photo file %s: %s", file.get_path(), err.message);
            
            return ImportResult.DECODE_ERROR;
        }
        
1204 1205 1206
        if (interrogator.get_is_photo_corrupted())
            return ImportResult.NOT_AN_IMAGE;
        
1207 1208
        // if not detected photo information, unsupported
        DetectedPhotoInformation? detected = interrogator.get_detected_photo_information();
1209
        if (detected == null || detected.file_format == PhotoFileFormat.UNKNOWN)
1210 1211
            return ImportResult.UNSUPPORTED_FORMAT;
        
1212 1213 1214 1215 1216 1217 1218
        // copy over supplied MD5s if provided
        if ((params.sniffer_options & PhotoFileSniffer.Options.NO_MD5) != 0) {
            detected.exif_md5 = params.exif_md5;
            detected.thumbnail_md5 = params.thumbnail_md5;
            detected.md5 = params.full_md5;
        }
        
1219 1220
        Orientation orientation = Orientation.TOP_LEFT;
        time_t exposure_time = 0;
1221
        string title = "";
1222
        GpsCoords gps_coords = GpsCoords();
1223
        string comment = "";
1224
        Rating rating = Rating.UNRATED;
1225
        
1226
#if TRACE_MD5
1227
        debug("importing MD5 %s: exif=%s preview=%s full=%s", file.get_path(), detected.exif_md5,
1228 1229 1230
            detected.thumbnail_md5, detected.md5);
#endif
        
1231 1232 1233 1234
        if (detected.metadata != null) {
            MetadataDateTime? date_time = detected.metadata.get_exposure_date_time();
            if (date_time != null)
                exposure_time = date_time.get_timestamp();
1235
            
1236 1237
            orientation = detected.metadata.get_orientation();
            title = detected.metadata.get_title();
1238
            gps_coords = detected.metadata.get_gps_coords();
1239
            comment = detected.metadata.get_comment();
1240
            params.keywords = detected.metadata.get_keywords();
1241
            rating = detected.metadata.get_rating();
1242 1243
        }
        
1244
        // verify basic mechanics of photo: RGB 8-bit encoding
1245 1246 1247
        if (detected.colorspace != Gdk.Colorspace.RGB 
            || detected.channels < 3 
            || detected.bits_per_channel != 8) {
1248
            message("Not importing %s: Unsupported color format", file.get_path());
1249
            
1250 1251 1252
            return ImportResult.UNSUPPORTED_FORMAT;
        }
        
1253 1254 1255
        // photo information is initially stored in database in raw, non-modified format ... this is
        // especially important dealing with dimensions and orientation ... Don't trust EXIF
        // dimensions, they can lie or not be present
1256
        params.row.photo_id = PhotoID();
1257 1258 1259 1260
        params.row.master.filepath = file.get_path();
        params.row.master.dim = detected.image_dim;
        params.row.master.filesize = info.get_size();
        params.row.master.timestamp = timestamp.tv_sec;
1261
        params.row.exposure_time = exposure_time;
1262
        params.row.orientation = orientation;
1263
        params.row.master.original_orientation = orientation;
1264 1265 1266 1267 1268 1269 1270 1271
        params.row.import_id = params.import_id;
        params.row.event_id = EventID();
        params.row.transformations = null;
        params.row.md5 = detected.md5;
        params.row.thumbnail_md5 = detected.thumbnail_md5;
        params.row.exif_md5 = detected.exif_md5;
        params.row.time_created = 0;
        params.row.flags = 0;
1272
        params.row.master.file_format = detected.file_format;
1273
        params.row.title = title;
1274
        params.row.gps_coords = gps_coords;
1275
        params.row.comment = comment;
1276
        params.row.rating = rating;
1277 1278
        
        if (params.thumbnails != null) {
1279 1280
            PhotoFileReader reader = params.row.master.file_format.create_reader(
                params.row.master.filepath);
1281
            reader.set_role (PhotoFileReader.Role.THUMBNAIL);
1282
            try {
1283
                ThumbnailCache.generate_for_photo(params.thumbnails, reader, params.row.orientation, 
1284
                    params.row.master.dim);
1285 1286
            } catch (Error err) {
                return ImportResult.convert_error(err, ImportResult.FILE_ERROR);
1287 1288
            }
        }
1289
        
1290 1291 1292
#if MEASURE_IMPORT
        debug("IMPORT: total=%lf", total_time.elapsed());
#endif
1293
        return ImportResult.SUCCESS;
1294 1295
    }
    
1296 1297 1298 1299 1300 1301 1302 1303 1304 1305 1306 1307 1308 1309 1310 1311 1312 1313 1314 1315
    public static void create_pre_import(PhotoImportParams params) {
        File file = params.file;
        params.row.photo_id = PhotoID();
        params.row.master.filepath = file.get_path();
        params.row.master.dim = Dimensions(0,0);
        params.row.master.filesize = 0;
        params.row.master.timestamp = 0;
        params.row.exposure_time = 0;
        params.row.orientation = Orientation.TOP_LEFT;
        params.row.master.original_orientation = Orientation.TOP_LEFT;
        params.row.import_id = params.import_id;
        params.row.event_id = EventID();
        params.row.transformations = null;
        params.row.md5 = null;
        params.row.thumbnail_md5 = null;
        params.row.exif_md5 = null;
        params.row.time_created = 0;
        params.row.flags = 0;
        params.row.master.file_format = PhotoFileFormat.JFIF;
        params.row.title = null;
1316
        params.row.gps_coords = GpsCoords();
1317
        params.row.comment = null;
1318
        params.row.rating = Rating.UNRATED;
1319 1320 1321 1322 1323
        
        PhotoFileInterrogator interrogator = new PhotoFileInterrogator(params.file, params.sniffer_options);
        try {
            interrogator.interrogate();
            DetectedPhotoInformation? detected = interrogator.get_detected_photo_information();
1324
            if (detected != null && !interrogator.get_is_photo_corrupted() && detected.file_format != PhotoFileFormat.UNKNOWN)
1325 1326 1327 1328
                params.row.master.file_format = detected.file_format;
        } catch (Error err) {
            debug("Unable to interrogate photo file %s: %s", file.get_path(), err.message);
        }
1329 1330
    }
    
1331 1332
    protected BackingPhotoRow? query_backing_photo_row(File file, PhotoFileSniffer.Options options,
        out DetectedPhotoInformation detected) throws Error {
1333 1334
        detected = null;
        
1335
        BackingPhotoRow backing = new BackingPhotoRow();
1336 1337 1338
        // get basic file information
        FileInfo info = null;
        try {
1339 1340
            info = file.query_info(DirectoryMonitor.SUPPLIED_ATTRIBUTES,
                FileQueryInfoFlags.NOFOLLOW_SYMLINKS, null);
1341
        } catch (Error err) {
1342 1343
            critical("Unable to read file information for %s: %s", file.get_path(), err.message);
            
1344
            return null;
1345 1346 1347
        }
        
        // sniff photo information
1348
        PhotoFileInterrogator interrogator = new PhotoFileInterrogator(file, options);
1349
        interrogator.interrogate();
1350
        detected = interrogator.get_detected_photo_information();
1351
        if (detected == null || interrogator.get_is_photo_corrupted()) {
1352 1353
            critical("Photo update: %s no longer a recognized image", to_string());
            
1354
            return null;
1355 1356
        }
        
1357
        TimeVal modification_time = info.get_modification_time();
1358
        
1359 1360 1361 1362 1363 1364
        backing.filepath = file.get_path();
        backing.timestamp = modification_time.tv_sec;
        backing.filesize = info.get_size();
        backing.file_format = detected.file_format;
        backing.dim = detected.image_dim;
        backing.original_orientation = detected.metadata != null
1365 1366
            ? detected.metadata.get_orientation() : Orientation.TOP_LEFT;
        
1367
        return backing;
1368 1369
    }
    
1370 1371 1372 1373
    public abstract class ReimportMasterState {
    }
    
    private class ReimportMasterStateImpl : ReimportMasterState {
1374
        public PhotoRow row = new PhotoRow();
1375
        public PhotoMetadata? metadata;
1376
        public string[] alterations;
1377 1378
        public bool metadata_only = false;
        
1379
        public ReimportMasterStateImpl(PhotoRow row, PhotoMetadata? metadata, string[] alterations) {
1380 1381
            this.row = row;
            this.metadata = metadata;
1382
            this.alterations = alterations;
1383 1384 1385 1386 1387 1388 1389
        }
    }
    
    public abstract class ReimportEditableState {
    }
    
    private class ReimportEditableStateImpl : ReimportEditableState {
1390
        public BackingPhotoRow backing_state = new BackingPhotoRow();
1391 1392 1393
        public PhotoMetadata? metadata;
        public bool metadata_only = false;
        
1394
        public ReimportEditableStateImpl(BackingPhotoRow backing_state, PhotoMetadata? metadata) {
1395 1396 1397 1398 1399
            this.backing_state = backing_state;
            this.metadata = metadata;
        }
    }
    
1400 1401 1402 1403
    public abstract class ReimportRawDevelopmentState {
    }
    
    private class ReimportRawDevelopmentStateImpl : ReimportRawDevelopmentState {
1404
        public class DevToReimport {
1405 1406 1407 1408 1409 1410 1411 1412 1413 1414 1415 1416 1417 1418 1419 1420 1421 1422 1423 1424 1425 1426 1427 1428
            public BackingPhotoRow backing = new BackingPhotoRow();
            public PhotoMetadata? metadata;
            
            public DevToReimport(BackingPhotoRow backing, PhotoMetadata? metadata) {
                this.backing = backing;
                this.metadata = metadata;
            }
        }
        
        public Gee.Collection<DevToReimport> list = new Gee.ArrayList<DevToReimport>();
        public bool metadata_only = false;
        
        public ReimportRawDevelopmentStateImpl() {
        }
        
        public void add(BackingPhotoRow backing, PhotoMetadata? metadata) {
            list.add(new DevToReimport(backing, metadata));
        }
        
        public int get_size() {
            return list.size;
        }
    }
    
1429 1430
    // This method is thread-safe.  If returns false the photo should be marked offline (in the
    // main UI thread).
1431
    public bool prepare_for_reimport_master(out ReimportMasterState reimport_state) throws Error {
1432 1433
        reimport_state = null;
        
1434 1435 1436
        File file = get_master_reader().get_file();
        
        DetectedPhotoInformation detected;
1437 1438 1439
        BackingPhotoRow? backing = query_backing_photo_row(file, PhotoFileSniffer.Options.GET_ALL, 
            out detected);
        if (backing == null) {
1440 1441 1442 1443 1444 1445 1446 1447 1448 1449 1450 1451 1452
            warning("Unable to retrieve photo state from %s for reimport", file.get_path());
            return false;
        }
        
        // verify basic mechanics of photo: RGB 8-bit encoding
        if (detected.colorspace != Gdk.Colorspace.RGB 
            || detected.channels < 3 
            || detected.bits_per_channel != 8) {
            warning("Not re-importing %s: Unsupported color format", file.get_path());
            
            return false;
        }
        
1453
        // start with existing row and update appropriate fields
1454
        PhotoRow updated_row = new PhotoRow();
1455
        lock (row) {
1456
            updated_row = row;
1457 1458
        }
        
1459 1460 1461 1462 1463 1464
        // build an Alteration list for the relevant changes
        string[] list = new string[0];
        
        if (updated_row.md5 != detected.md5)
            list += "metadata:md5";
        
1465
        if (updated_row.master.original_orientation != backing.original_orientation) {
1466
            list += "image:orientation";
1467
            updated_row.master.original_orientation = backing.original_orientation;
1468
        }
1469 1470 1471

        GpsCoords gps_coords = GpsCoords();

1472 1473 1474 1475 1476 1477
        if (detected.metadata != null) {
            MetadataDateTime? date_time = detected.metadata.get_exposure_date_time();
            if (date_time != null && updated_row.exposure_time != date_time.get_timestamp())
                list += "metadata:exposure-time";
            
            if (updated_row.title != detected.metadata.get_title())
Lucas Beeler's avatar
Lucas Beeler committed
1478
                list += "metadata:name";
1479 1480 1481 1482 1483

            gps_coords = detected.metadata.get_gps_coords();
            if (updated_row.gps_coords != gps_coords)
                list += "metadata:gps";

1484
            
1485 1486 1487
            if (updated_row.comment != detected.metadata.get_comment())
                list += "metadata:comment";
            
1488 1489 1490 1491
            if (updated_row.rating != detected.metadata.get_rating())
                list += "metadata:rating";
        }
        
1492
        updated_row.master = backing;
1493 1494 1495 1496
        updated_row.md5 = detected.md5;
        updated_row.exif_md5 = detected.exif_md5;
        updated_row.thumbnail_md5 = detected.thumbnail_md5;
        
1497
        PhotoMetadata? metadata = null;