Photo.vala 30.5 KB
Newer Older
1 2 3 4 5 6
/* Copyright 2009 Yorba Foundation
 *
 * This software is licensed under the GNU LGPL (version 2.1 or later).
 * See the COPYING file in this distribution. 
 */

7 8 9 10 11 12 13
public enum ImportResult {
    SUCCESS,
    FILE_ERROR,
    DECODE_ERROR,
    DATABASE_ERROR,
    USER_ABORT,
    NOT_A_FILE,
14 15
    PHOTO_EXISTS,
    UNSUPPORTED_FORMAT
16
}
17 18

public class Photo : Object {
19 20 21 22 23 24 25
    public const int EXCEPTION_NONE          = 0;
    public const int EXCEPTION_ORIENTATION   = 1 << 0;
    public const int EXCEPTION_CROP          = 1 << 1;
    public const int EXCEPTION_REDEYE        = 1 << 2;

    public const Jpeg.Quality EXPORT_JPEG_QUALITY = Jpeg.Quality.HIGH;
    public const Gdk.InterpType EXPORT_INTERP = Gdk.InterpType.BILINEAR;
26
    
27
    private static Gee.HashMap<int64?, Photo> photo_map = null;
28
    private static PhotoTable photo_table = null;
29 30
    private static PhotoID cached_photo_id = PhotoID();
    private static Gdk.Pixbuf cached_raw = null;
31
    
32 33 34 35 36 37
    public enum Currency {
        CURRENT,
        DIRTY,
        GONE
    }
    
38 39
    private PhotoID photo_id;
    
40 41 42 43
    // because fetching some items from the database is high-overhead, certain items are cached
    // here ... really want to be frugal about this, as maintaining coherency is complicated enough
    private time_t exposure_time = -1;
    
44 45
    public static void init() {
        photo_map = new Gee.HashMap<int64?, Photo>(int64_hash, int64_equal, direct_equal);
46
        photo_table = new PhotoTable();
47 48 49 50 51
    }
    
    public static void terminate() {
    }
    
52
    public static ImportResult import(File file, ImportID import_id, out Photo photo) {
53 54
        debug("Importing file %s", file.get_path());

55 56 57 58 59 60 61 62 63 64
        FileInfo info = null;
        try {
            info = file.query_info("*", FileQueryInfoFlags.NOFOLLOW_SYMLINKS, null);
        } catch (Error err) {
            return ImportResult.FILE_ERROR;
        }
        
        if (info.get_file_type() != FileType.REGULAR)
            return ImportResult.NOT_A_FILE;
        
65 66 67 68 69 70 71
        if (info.get_content_type() != GPhoto.MIME.JPEG) {
            message("Not importing %s: Unsupported content type %s", file.get_path(),
                info.get_content_type());

            return ImportResult.UNSUPPORTED_FORMAT;
        }
        
72 73 74
        TimeVal timestamp = TimeVal();
        info.get_modification_time(timestamp);
        
75
        Dimensions dim = Dimensions();
76
        Orientation orientation = Orientation.TOP_LEFT;
77 78 79 80 81
        time_t exposure_time = 0;
        
        // TODO: Try to read JFIF metadata too
        PhotoExif exif = new PhotoExif(file);
        if (exif.has_exif()) {
82
            if (!exif.get_dimensions(out dim))
83
                message("Unable to read EXIF dimensions for %s", file.get_path());
84
            
85
            if (!exif.get_timestamp(out exposure_time))
86
                message("Unable to read EXIF orientation for %s", file.get_path());
87 88

            orientation = exif.get_orientation();
89
        }
90 91 92 93 94
        
        Gdk.Pixbuf pixbuf;
        try {
            pixbuf = new Gdk.Pixbuf.from_file(file.get_path());
        } catch (Error err) {
95 96 97
            // assume a decode error, although technically it could be I/O ... need better Gdk
            // bindings to determine which
            return ImportResult.DECODE_ERROR;
98 99
        }
        
100 101 102 103 104 105 106 107
        // verify basic mechanics of photo: RGB 8-bit encoding
        if (pixbuf.get_colorspace() != Gdk.Colorspace.RGB || pixbuf.get_n_channels() < 3 
            || pixbuf.get_bits_per_sample() != 8) {
            message("Not importing %s: Unsupported color format", file.get_path());
            
            return ImportResult.UNSUPPORTED_FORMAT;
        }
        
108 109 110 111
        // XXX: Trust EXIF or Pixbuf for dimensions?
        if (!dim.has_area())
            dim = Dimensions(pixbuf.get_width(), pixbuf.get_height());

112 113
        if (photo_table.is_photo_stored(file))
            return ImportResult.PHOTO_EXISTS;
114 115 116 117 118
        
        // photo information is stored in database in raw, non-modified format ... this is especially
        // important dealing with dimensions and orientation
        PhotoID photo_id = photo_table.add(file, dim, info.get_size(), timestamp.tv_sec, exposure_time,
            orientation, import_id);
119 120
        if (photo_id.is_invalid())
            return ImportResult.DATABASE_ERROR;
121
        
122 123 124
        // sanity ... this would be very bad
        assert(!photo_map.contains(photo_id.id));
        
125
        // modify pixbuf for thumbnails, which are stored with modifications
126
        pixbuf = orientation.rotate_pixbuf(pixbuf);
127 128 129 130
        
        // import it into the thumbnail cache with modifications
        ThumbnailCache.import(photo_id, pixbuf);
        
131 132 133
        photo = fetch(photo_id);
        
        return ImportResult.SUCCESS;
134
    }
135
    
136
    public static Photo fetch(PhotoID photo_id) {
137
        Photo photo = photo_map.get(photo_id.id);
138 139 140 141 142 143 144 145 146 147 148 149 150

        if (photo == null) {
            photo = new Photo(photo_id);
            photo_map.set(photo_id.id, photo);
        }
        
        return photo;
    }
    
    private Photo(PhotoID photo_id) {
        assert(photo_id.is_valid());
        
        this.photo_id = photo_id;
151 152 153
        
        // catch our own signal, as this can happen in many different places throughout the code
        altered += remove_exportable_file;
154 155 156 157
    }
    
    public signal void altered();
    
158 159
    public signal void thumbnail_altered();
    
160 161 162 163 164 165 166 167 168 169
    public signal void removed();
    
    public PhotoID get_photo_id() {
        return photo_id;
    }
    
    public File get_file() {
        return photo_table.get_file(photo_id);
    }
    
170 171 172 173
    public string get_name() {
        return photo_table.get_name(photo_id);
    }
    
174 175 176 177 178 179 180 181 182 183 184 185 186 187
    public uint64 query_filesize() {
        FileInfo info = null;
        try {
            info = get_file().query_info(FILE_ATTRIBUTE_STANDARD_SIZE, 
                FileQueryInfoFlags.NOFOLLOW_SYMLINKS, null);
        } catch (Error err) {
            debug("Unable to query filesize for %s: %s", get_file().get_path(), err.message);

            return 0;
        }
        
        return info.get_size();
    }
    
188
    public time_t get_exposure_time() {
189 190 191 192
        if (exposure_time == -1)
            exposure_time = photo_table.get_exposure_time(photo_id);
        
        return exposure_time;
193 194
    }
    
195 196 197 198
    public time_t get_timestamp() {
        return photo_table.get_timestamp(photo_id);
    }

199 200 201 202 203 204 205 206
    public EventID get_event_id() {
        return photo_table.get_event(photo_id);
    }
    
    public void set_event_id(EventID event_id) {
        photo_table.set_event(photo_id, event_id);
    }

207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224
    public string to_string() {
        return "[%lld] %s".printf(photo_id.id, get_file().get_path());
    }

    public bool equals(Photo photo) {
        // identity works because of the photo_map, but the photo_table primary key is where the
        // rubber hits the road
        if (this == photo) {
            assert(photo_id.id == photo.photo_id.id);
            
            return true;
        }
        
        assert(photo_id.id != photo.photo_id.id);
        
        return false;
    }
    
225 226 227 228 229 230 231 232 233 234 235 236 237 238 239
    public bool has_transformations() {
        return photo_table.has_transformations(photo_id) 
            || (photo_table.get_orientation(photo_id) != photo_table.get_original_orientation(photo_id));
    }
    
    public void remove_all_transformations() {
        bool altered = photo_table.remove_all_transformations(photo_id);
        
        Orientation orientation = photo_table.get_orientation(photo_id);
        Orientation original_orientation = photo_table.get_original_orientation(photo_id);
        if (orientation != original_orientation) {
            photo_table.set_orientation(photo_id, original_orientation);
            altered = true;
        }

240 241 242 243 244 245 246 247 248
        if (altered) {

            // REDEYE: if photo was altered, clear the pixbuf cache. This is
            // necessary because the redeye transformation, unlike rotate/crop,
            // actually modifies the pixel data in the pixbuf, so we need to
            // re-load the original pixel data from its source file when redeye
            // is cleared
            cached_raw = null;
      
249
            photo_altered();
250
        }
251 252
    }
    
253
    public void rotate(Rotation rotation) {
254
        Orientation orientation = photo_table.get_orientation(photo_id);
255
        
256
        orientation = orientation.perform(rotation);
257 258
        
        photo_table.set_orientation(photo_id, orientation);
Jim Nelson's avatar
Jim Nelson committed
259
        
260 261 262 263 264 265 266 267 268 269
        altered();

        // because rotations are (a) common and available everywhere in the app, (b) the user expects
        // a level of responsiveness not necessarily required by other modifications, and (c) can't 
        // cache a lot of full-sized pixbufs for rotate-and-scale ops, perform the rotation directly 
        // on the already-modified thumbnails.
        foreach (int scale in ThumbnailCache.SCALES) {
            Gdk.Pixbuf thumbnail = ThumbnailCache.fetch(photo_id, scale);
            thumbnail = rotation.perform(thumbnail);
            ThumbnailCache.replace(photo_id, scale, thumbnail);
270
        }
271 272

        thumbnail_altered();
273 274
    }
    
Jim Nelson's avatar
Jim Nelson committed
275 276
    // Returns uncropped (but rotated) dimensions
    public Dimensions get_uncropped_dimensions() {
277
        Dimensions dim = photo_table.get_dimensions(photo_id);
278 279 280
        Orientation orientation = photo_table.get_orientation(photo_id);
        
        return orientation.rotate_dimensions(dim);
Jim Nelson's avatar
Jim Nelson committed
281 282
    }
    
283
    // Returns dimensions for fully-modified photo
Jim Nelson's avatar
Jim Nelson committed
284
    public Dimensions get_dimensions() {
285
        Box crop;
286
        if (get_crop(out crop))
Jim Nelson's avatar
Jim Nelson committed
287
            return crop.get_dimensions();
288
        
Jim Nelson's avatar
Jim Nelson committed
289
        return get_uncropped_dimensions();
290 291
    }
    
292 293 294 295
    public bool has_crop() {
        return photo_table.get_transformation(photo_id, "crop") != null;
    }
    
296 297
    // Returns the crop in the raw photo's coordinate system
    private bool get_raw_crop(out Box crop) {
298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314
        KeyValueMap map = photo_table.get_transformation(photo_id, "crop");
        if (map == null)
            return false;
        
        int left = map.get_int("left", -1);
        int top = map.get_int("top", -1);
        int right = map.get_int("right", -1);
        int bottom = map.get_int("bottom", -1);
        
        if (left == -1 || top == -1 || right == -1 || bottom == -1)
            return false;
        
        crop = Box(left, top, right, bottom);
        
        return true;
    }
    
315 316 317 318 319 320 321 322 323 324 325 326 327
    // Returns the rotated crop for the photo
    public bool get_crop(out Box crop) {
        Box raw;
        if (!get_raw_crop(out raw))
            return false;
        
        Dimensions dim = photo_table.get_dimensions(photo_id);
        Orientation orientation = photo_table.get_orientation(photo_id);
        crop = orientation.rotate_box(dim, raw);
        
        return true;
    }
    
328 329 330 331
    private Orientation get_orientation() {
        return photo_table.get_orientation(photo_id);
    }
    
332 333
    // Sets the crop using the raw photo's unrotated coordinate system
    private bool set_raw_crop(Box crop) {
334
        KeyValueMap map = new KeyValueMap("crop");
335 336 337 338
        map.set_int("left", crop.left);
        map.set_int("top", crop.top);
        map.set_int("right", crop.right);
        map.set_int("bottom", crop.bottom);
339 340 341 342 343 344 345 346
        
        bool res = photo_table.set_transformation(photo_id, map);
        if (res)
            photo_altered();
        
        return res;
    }
    
347 348 349 350 351 352 353
    // Sets the crop, where the crop is in the rotated coordinate system
    public bool set_crop(Box crop) {
        // return crop to photo's coordinate system
        Dimensions dim = photo_table.get_dimensions(photo_id);
        Orientation orientation = photo_table.get_orientation(photo_id);
        Box derotated = orientation.derotate_box(dim, crop);
        
354 355 356
        assert(derotated.get_width() <= dim.width);
        assert(derotated.get_height() <= dim.height);
        
357 358 359
        return set_raw_crop(derotated);
    }
    
Jim Nelson's avatar
Jim Nelson committed
360 361 362 363 364 365 366
    public bool remove_crop() {
        bool res = photo_table.remove_transformation(photo_id, "crop");
        if (res)
            photo_altered();
        
        return res;
    }
367 368 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 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519

    public bool add_redeye_instance(RedeyeInstance inst_unscaled) {
        Gdk.Rectangle bounds_rect_unscaled =
            RedeyeInstance.to_bounds_rect(inst_unscaled);
        Gdk.Rectangle bounds_rect_raw =
            unscaled_to_raw_rect(bounds_rect_unscaled);
        RedeyeInstance inst = RedeyeInstance.from_bounds_rect(bounds_rect_raw);

        KeyValueMap map = photo_table.get_transformation(photo_id, "redeye");
        if (map == null) {
            map = new KeyValueMap("redeye");
            map.set_int("num_points", 0);
        }
        
        int num_points = map.get_int("num_points", -1);
        assert(num_points >= 0);
        
        num_points++;
        
        string radius_key = "radius%d".printf(num_points - 1);
        string center_key = "center%d".printf(num_points - 1);
        
        map.set_int(radius_key, inst.radius);
        map.set_point(center_key, inst.center);
        
        map.set_int("num_points", num_points);
        
        bool res = photo_table.set_transformation(photo_id, map);
        if (res)
            photo_altered();
                    
        return res;
    }

    private RedeyeInstance[] get_all_redeye() {
        KeyValueMap map = photo_table.get_transformation(photo_id, "redeye");
        if (map != null) {
            int num_points = map.get_int("num_points", -1);
            assert(num_points > 0);

            RedeyeInstance[] res = new RedeyeInstance[num_points];

            Gdk.Point default_point = {0};
            default_point.x = -1;
            default_point.y = -1;

            for (int i = 0; i < num_points; i++) {
                string center_key = "center%d".printf(i);
                string radius_key = "radius%d".printf(i);

                res[i].center = map.get_point(center_key, default_point);
                assert(res[i].center.x != default_point.x);
                assert(res[i].center.y != default_point.y);
                res[i].radius = map.get_int(radius_key, -1);
                assert(res[i].radius != -1);                
            }

            return res;
        }

        return new RedeyeInstance[0];
    }

    private Gdk.Pixbuf do_redeye(owned Gdk.Pixbuf pixbuf,
        owned RedeyeInstance inst) {
        
        /* we remove redeye within a circular region called the "effect
           extent." the effect extent is inscribed within its "bounding
           rectangle." */

        /* for each scanline in the top half-circle of the effect extent,
           compute the number of pixels by which the effect extent is inset
           from the edges of its bounding rectangle. note that we only have
           to do this for the first quadrant because the second quadrant's
           insets can be derived by symmetry */
        double r = (double) inst.radius;
        int[] x_insets_first_quadrant = new int[inst.radius + 1];
        
        int i = 0;
        for (double y = r; y >= 0.0; y -= 1.0) {
            double theta = Math.asin(y / r);
            int x = (int)((r * Math.cos(theta)) + 0.5);
            x_insets_first_quadrant[i] = inst.radius - x;
            
            i++;
        }

        int x_bounds_min = inst.center.x - inst.radius;
        int x_bounds_max = inst.center.x + inst.radius;
        int ymin = inst.center.y - inst.radius;
        ymin = (ymin < 0) ? 0 : ymin;
        int ymax = inst.center.y;
        ymax = (ymax > (pixbuf.height - 1)) ? (pixbuf.height - 1) : ymax;

        /* iterate over all the pixels in the top half-circle of the effect
           extent from top to bottom */
        int inset_index = 0;
        for (int y_it = ymin; y_it <= ymax; y_it++) {
            int xmin = x_bounds_min + x_insets_first_quadrant[inset_index];
            xmin = (xmin < 0) ? 0 : xmin;
            int xmax = x_bounds_max - x_insets_first_quadrant[inset_index];
            xmax = (xmax > (pixbuf.width - 1)) ? (pixbuf.width - 1) : xmax;

            for (int x_it = xmin; x_it <= xmax; x_it++) {
                red_reduce_pixel(pixbuf, x_it, y_it);
            }
            inset_index++;
        }

        /* iterate over all the pixels in the top half-circle of the effect
           extent from top to bottom */
        ymin = inst.center.y;
        ymax = inst.center.y + inst.radius;
        inset_index = x_insets_first_quadrant.length - 1;
        for (int y_it = ymin; y_it <= ymax; y_it++) {  
            int xmin = x_bounds_min + x_insets_first_quadrant[inset_index];
            xmin = (xmin < 0) ? 0 : xmin;
            int xmax = x_bounds_max - x_insets_first_quadrant[inset_index];
            xmax = (xmax > (pixbuf.width - 1)) ? (pixbuf.width - 1) : xmax;

            for (int x_it = xmin; x_it <= xmax; x_it++) {
                red_reduce_pixel(pixbuf, x_it, y_it);
            }
            inset_index--;
        }
        
        return pixbuf;
    }

    private Gdk.Pixbuf red_reduce_pixel(owned Gdk.Pixbuf pixbuf, int x, int y) {
        int px_start_byte_offset = (y * pixbuf.get_rowstride()) +
            (x * pixbuf.get_n_channels());
        
        unowned uchar[] pixel_data = pixbuf.get_pixels();
        
        /* The pupil of the human eye has no pigment, so we expect all
           color channels to be of about equal intensity. This means that at
           any point within the effects region, the value of the red channel
           should be about the same as the values of the green and blue
           channels. So set the value of the red channel to be the mean of the
           values of the red and blue channels. This preserves achromatic
           intensity across all channels while eliminating any extraneous flare
           affecting the red channel only (i.e. the red-eye effect). */
        uchar g = pixel_data[px_start_byte_offset + 1];
        uchar b = pixel_data[px_start_byte_offset + 2];
        
        uchar r = (g + b) / 2;
        
        pixel_data[px_start_byte_offset] = r;
        
        return pixbuf;
    }    

520 521 522
    // Retrieves a full-sized pixbuf for the Photo with all modifications, except those specified
    public Gdk.Pixbuf get_pixbuf(int exceptions = EXCEPTION_NONE) throws Error {
        Gdk.Pixbuf pixbuf = null;
Jim Nelson's avatar
Jim Nelson committed
523
        
524 525 526 527 528
        if (cached_raw != null && cached_photo_id.id == photo_id.id) {
            // used the cached raw pixbuf, which is merely the last loaded pixbuf
            pixbuf = cached_raw;
        } else {
            File file = get_file();
Jim Nelson's avatar
Jim Nelson committed
529 530

            debug("Loading raw photo %s", file.get_path());
531
            pixbuf = new Gdk.Pixbuf.from_file(file.get_path());
Jim Nelson's avatar
Jim Nelson committed
532
        
533 534 535 536
            // stash for next time
            cached_raw = pixbuf;
            cached_photo_id = photo_id;
        }
537
        
538 539 540
        //
        // Image modification pipeline
        //
541 542 543 544 545 546 547 548 549

        // redeye reduction
        if ((exceptions & EXCEPTION_REDEYE) == 0) {
            RedeyeInstance[] redeye_instances = get_all_redeye();
            for (int i = 0; i < redeye_instances.length; i++) {
                pixbuf = do_redeye(pixbuf, redeye_instances[i]);
            }
        }
                
550
        // crop
551 552 553 554 555 556
        if ((exceptions & EXCEPTION_CROP) == 0) {
            Box crop;
            if (get_raw_crop(out crop)) {
                pixbuf = new Gdk.Pixbuf.subpixbuf(pixbuf, crop.left, crop.top, crop.get_width(),
                    crop.get_height());
            }
557 558
        }

559
        // orientation (all modifications are stored in unrotated coordinate system)
560 561 562 563
        if ((exceptions & EXCEPTION_ORIENTATION) == 0) {
            Orientation orientation = photo_table.get_orientation(photo_id);
            pixbuf = orientation.rotate_pixbuf(pixbuf);
        }
564 565 566 567
        
        return pixbuf;
    }
    
568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584
    private File generate_exportable_file() throws Error {
        File original_file = get_file();

        File exportable_dir = AppWindow.get_data_subdir("export");
    
        // use exposure time, then file modified time, for directory (to prevent name collision)
        time_t timestamp = get_exposure_time();
        if (timestamp == 0)
            timestamp = get_timestamp();
        
        if (timestamp != 0) {
            Time tm = Time.local(timestamp);
            exportable_dir = exportable_dir.get_child("%04u".printf(tm.year + 1900));
            exportable_dir = exportable_dir.get_child("%02u".printf(tm.month + 1));
            exportable_dir = exportable_dir.get_child("%02u".printf(tm.day));
        }
        
Jim Nelson's avatar
Jim Nelson committed
585 586 587 588 589 590 591 592 593 594 595 596 597 598 599
        return exportable_dir.get_child(original_file.get_basename());
    }
    
    private void copy_exported_exif(PhotoExif source, PhotoExif dest, Orientation orientation, 
        Dimensions dim) throws Error {
        if (!source.has_exif())
            return;
            
        dest.set_exif(source.get_exif());
        dest.set_dimensions(dim);
        dest.set_orientation(orientation);
        dest.remove_all_tags(Exif.Tag.RELATED_IMAGE_WIDTH);
        dest.remove_all_tags(Exif.Tag.RELATED_IMAGE_LENGTH);
        dest.remove_thumbnail();
        dest.commit();
600 601
    }

Jim Nelson's avatar
Jim Nelson committed
602
    // Returns a file appropriate for export.  The file should NOT be deleted once it's been used.
603 604 605 606 607 608
    //
    // TODO: Lossless transformations, especially for mere rotations of JFIF files.
    public File generate_exportable() throws Error {
        if (!has_transformations())
            return get_file();

Jim Nelson's avatar
Jim Nelson committed
609 610 611
        File dest_file = generate_exportable_file();
        if (dest_file.query_exists(null))
            return dest_file;
612
        
Jim Nelson's avatar
Jim Nelson committed
613 614 615 616 617 618 619
        // generate_exportable_file only generates a filename; create directory if necessary
        File dest_dir = dest_file.get_parent();
        if (!dest_dir.query_exists(null))
            dest_dir.make_directory_with_parents(null);
        
        File original_file = get_file();
        PhotoExif original_exif = new PhotoExif(get_file());
620 621
        
        // if only rotated, only need to copy and modify the EXIF
Jim Nelson's avatar
Jim Nelson committed
622 623
        if (!photo_table.has_transformations(photo_id) && original_exif.has_exif()) {
            original_file.copy(dest_file, FileCopyFlags.OVERWRITE, null, null);
624

Jim Nelson's avatar
Jim Nelson committed
625 626 627
            PhotoExif dest_exif = new PhotoExif(dest_file);
            dest_exif.set_orientation(photo_table.get_orientation(photo_id));
            dest_exif.commit();
628 629
        } else {
            Gdk.Pixbuf pixbuf = get_pixbuf();
Jim Nelson's avatar
Jim Nelson committed
630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648
            pixbuf.save(dest_file.get_path(), "jpeg", "quality", EXPORT_JPEG_QUALITY.get_pct_text());
            copy_exported_exif(original_exif, new PhotoExif(dest_file), Orientation.TOP_LEFT,
                Dimensions.for_pixbuf(pixbuf));
        }
        
        return dest_file;
    }
    
    // Writes a file appropriate for export meeting the specified parameters.
    //
    // TODO: Lossless transformations, especially for mere rotations of JFIF files.
    public void export(File dest_file, int scale, ScaleConstraint constraint,
        Jpeg.Quality quality) throws Error {
        if (constraint == ScaleConstraint.ORIGINAL) {
            // generate a raw exportable file and copy that
            File exportable = generate_exportable();

            exportable.copy(dest_file, FileCopyFlags.OVERWRITE | FileCopyFlags.ALL_METADATA,
                null, null);
649
            
Jim Nelson's avatar
Jim Nelson committed
650
            return;
651
        }
652
        
Jim Nelson's avatar
Jim Nelson committed
653 654 655 656 657 658 659 660 661 662 663 664
        Gdk.Pixbuf pixbuf = get_pixbuf();
        Dimensions dim = Dimensions.for_pixbuf(pixbuf);
        Dimensions scaled = dim.get_scaled_by_constraint(scale, constraint);

        // only scale if necessary ... although scale_simple probably catches this, it's an easy
        // check to avoid image loss
        if (dim.width != scaled.width || dim.height != scaled.height)
            pixbuf = pixbuf.scale_simple(scaled.width, scaled.height, EXPORT_INTERP);
        
        pixbuf.save(dest_file.get_path(), "jpeg", "quality", quality.get_pct_text());
        copy_exported_exif(new PhotoExif(get_file()), new PhotoExif(dest_file), Orientation.TOP_LEFT,
            scaled);
665 666
    }
    
667 668 669 670 671
    // Returns unscaled thumbnail with all modifications applied applicable to the scale
    public Gdk.Pixbuf? get_thumbnail(int scale) {
        return ThumbnailCache.fetch(photo_id, scale);
    }
    
672
    public void remove(bool remove_original = true) {
673 674 675 676 677 678
        // signal all interested parties prior to removal from map
        removed();

        // remove all cached thumbnails
        ThumbnailCache.remove(photo_id);
        
679 680 681
        // remove exportable file
        remove_exportable_file();
        
682 683 684
        // remove original
        if (remove_original)
            remove_original_file();
685

686 687 688 689 690
        // remove from photo table -- should be wiped from storage now (other classes may have added
        // photo_id to other parts of the database ... it's their responsibility to remove them
        // when removed() is called)
        photo_table.remove(photo_id);
        
691 692 693 694 695
        // remove from global map
        photo_map.remove(photo_id.id);
    }
    
    private void photo_altered() {
Jim Nelson's avatar
Jim Nelson committed
696 697 698
        altered();

        // load transformed image for thumbnail generation
699
        Gdk.Pixbuf pixbuf = null;
700
        try {
701
            pixbuf = get_pixbuf();
702 703 704 705
        } catch (Error err) {
            error("%s", err.message);
        }
        
706
        ThumbnailCache.import(photo_id, pixbuf, true);
707
        
708
        thumbnail_altered();
709
    }
710 711 712 713 714 715 716 717 718 719 720 721 722 723
    
    private void remove_exportable_file() {
        File file = null;
        try {
            file = generate_exportable_file();
            if (file.query_exists(null))
                file.delete(null);
        } catch (Error err) {
            if (file != null)
                message("Unable to delete exportable photo file %s: %s", file.get_path(), err.message);
            else
                message("Unable to generate exportable filename for %s", to_string());
        }
    }
724
    
725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758
    private void remove_original_file() {
        File file = get_file();
        
        try {
            file.delete(null);
        } catch (Error err) {
            // log error but don't abend, as this is not fatal to operation (also, could be
            // the photo is removed because it could not be found during a verify)
            message("Unable to delete original photo %s: %s", file.get_path(), err.message);
        }
        
        File parent = file;
        
        // remove empty directories corresponding to imported path
        for (int depth = 0; depth < BatchImport.IMPORT_DIRECTORY_DEPTH; depth++) {
            parent = parent.get_parent();
            if (parent == null)
                break;
            
            if (!query_is_directory_empty(parent))
                break;
            
            try {
                parent.delete(null);
                debug("Deleted empty directory %s", parent.get_path());
            } catch (Error err) {
                // again, log error but don't abend
                message("Unable to delete empty directory %s: %s", parent.get_path(),
                    err.message);
            }
            
        }
    }

759 760 761
    public Currency check_currency() {
        FileInfo info = null;
        try {
762
            info = get_file().query_info("*", FileQueryInfoFlags.NOFOLLOW_SYMLINKS, null);
763 764 765 766 767 768 769 770 771
        } catch (Error err) {
            // treat this as the file has been deleted from the filesystem
            return Currency.GONE;
        }
        
        TimeVal timestamp = TimeVal();
        info.get_modification_time(timestamp);
        
        // trust modification time and file size
772
        if ((timestamp.tv_sec != get_timestamp()) || (info.get_size() != photo_table.get_filesize(photo_id)))
773 774
            return Currency.DIRTY;
        
775 776 777 778
        // verify thumbnail cache is all set
        if (!ThumbnailCache.exists(photo_id))
            return Currency.DIRTY;
        
779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812
        return Currency.CURRENT;
    }
    
    public void update() {
        File file = get_file();
        
        Dimensions dim = Dimensions();
        Orientation orientation = Orientation.TOP_LEFT;
        time_t exposure_time = 0;

        // TODO: Try to read JFIF metadata too
        PhotoExif exif = new PhotoExif(file);
        if (exif.has_exif()) {
            if (!exif.get_dimensions(out dim))
                error("Unable to read EXIF dimensions for %s", to_string());
            
            if (!exif.get_timestamp(out exposure_time))
                error("Unable to read EXIF orientation for %s", to_string());

            orientation = exif.get_orientation();
        } 
    
        FileInfo info = null;
        try {
            info = file.query_info("*", FileQueryInfoFlags.NOFOLLOW_SYMLINKS, null);
        } catch (Error err) {
            error("Unable to read file information for %s: %s", to_string(), err.message);
        }
        
        TimeVal timestamp = TimeVal();
        info.get_modification_time(timestamp);
        
        if (photo_table.update(photo_id, dim, info.get_size(), timestamp.tv_sec, exposure_time,
            orientation)) {
813 814 815
            // cache coherency
            this.exposure_time = exposure_time;
            
816 817 818
            photo_altered();
        }
    }
819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875

    private Gdk.Point unscaled_to_raw_point(Gdk.Point unscaled_point) {
        Orientation unscaled_orientation = get_orientation();
    
        Dimensions unscaled_dims =
            unscaled_orientation.rotate_dimensions(get_dimensions());

        int unscaled_x_offset_raw = 0;
        int unscaled_y_offset_raw = 0;

        Box crop_box = {0};
        bool is_cropped = get_raw_crop(out crop_box);
        if (is_cropped) {
            unscaled_x_offset_raw = crop_box.left;
            unscaled_y_offset_raw = crop_box.top;
        }
        
        Gdk.Point derotated_point =
            unscaled_orientation.derotate_point(unscaled_dims,
            unscaled_point);

        derotated_point.x += unscaled_x_offset_raw;
        derotated_point.y += unscaled_y_offset_raw;

        return derotated_point;    
    }
    
    private Gdk.Rectangle unscaled_to_raw_rect(Gdk.Rectangle unscaled_rect) {
        Gdk.Point upper_left = {0};
        Gdk.Point lower_right = {0};
        upper_left.x = unscaled_rect.x;
        upper_left.y = unscaled_rect.y;
        lower_right.x = upper_left.x + unscaled_rect.width;
        lower_right.y = upper_left.y + unscaled_rect.height;
        
        upper_left = unscaled_to_raw_point(upper_left);
        lower_right = unscaled_to_raw_point(lower_right);
        
        if (upper_left.x > lower_right.x) {
            int temp = upper_left.x;
            upper_left.x = lower_right.x;
            lower_right.x = temp;
        }
        if (upper_left.y > lower_right.y) {
            int temp = upper_left.y;
            upper_left.y = lower_right.y;
            lower_right.y = temp;
        }
        
        Gdk.Rectangle raw_rect = {0};
        raw_rect.x = upper_left.x;
        raw_rect.y = upper_left.y;
        raw_rect.width = lower_right.x - upper_left.x;
        raw_rect.height = lower_right.y - upper_left.y;
        
        return raw_rect;    
    }
876 877
}