Home | History | Annotate | Download | only in js
      1 // Copyright 2014 The Chromium Authors. All rights reserved.
      2 // Use of this source code is governed by a BSD-style license that can be
      3 // found in the LICENSE file.
      4 
      5 'use strict';
      6 
      7 /**
      8  * Object representing an image item (a photo).
      9  *
     10  * @param {FileEntry} entry Image entry.
     11  * @param {EntryLocation} locationInfo Entry location information.
     12  * @param {Object} metadata Metadata for the entry.
     13  * @param {MetadataCache} metadataCache Metadata cache instance.
     14  * @param {boolean} original Whether the entry is original or edited.
     15  * @constructor
     16  */
     17 Gallery.Item = function(
     18     entry, locationInfo, metadata, metadataCache, original) {
     19   /**
     20    * @type {FileEntry}
     21    * @private
     22    */
     23   this.entry_ = entry;
     24 
     25   /**
     26    * @type {EntryLocation}
     27    * @private
     28    */
     29   this.locationInfo_ = locationInfo;
     30 
     31   /**
     32    * @type {Object}
     33    * @private
     34    */
     35   this.metadata_ = Object.freeze(metadata);
     36 
     37   /**
     38    * @type {MetadataCache}
     39    * @private
     40    */
     41   this.metadataCache_ = metadataCache;
     42 
     43   /**
     44    * The content cache is used for prefetching the next image when going through
     45    * the images sequentially. The real life photos can be large (18Mpix = 72Mb
     46    * pixel array) so we want only the minimum amount of caching.
     47    * @type {Canvas}
     48    */
     49   this.screenImage = null;
     50 
     51   /**
     52    * We reuse previously generated screen-scale images so that going back to a
     53    * recently loaded image looks instant even if the image is not in the content
     54    * cache any more. Screen-scale images are small (~1Mpix) so we can afford to
     55    * cache more of them.
     56    * @type {Canvas}
     57    */
     58   this.contentImage = null;
     59 
     60   /**
     61    * Last accessed date to be used for selecting items whose cache are evicted.
     62    * @type {number}
     63    * @private
     64    */
     65   this.lastAccessed_ = Date.now();
     66 
     67   /**
     68    * @type {boolean}
     69    * @private
     70    */
     71   this.original_ = original;
     72 
     73   Object.seal(this);
     74 };
     75 
     76 /**
     77  * @return {FileEntry} Image entry.
     78  */
     79 Gallery.Item.prototype.getEntry = function() { return this.entry_; };
     80 
     81 /**
     82  * @return {EntryLocation} Entry location information.
     83  */
     84 Gallery.Item.prototype.getLocationInfo = function() {
     85   return this.locationInfo_;
     86 };
     87 
     88 /**
     89  * @return {Object} Metadata.
     90  */
     91 Gallery.Item.prototype.getMetadata = function() { return this.metadata_; };
     92 
     93 /**
     94  * Obtains the latest media metadata.
     95  *
     96  * This is a heavy operation since it forces to load the image data to obtain
     97  * the metadata.
     98  * @return {Promise} Promise to be fulfilled with fetched metadata.
     99  */
    100 Gallery.Item.prototype.getFetchedMedia = function() {
    101   return new Promise(function(fulfill, reject) {
    102     this.metadataCache_.getLatest(
    103         [this.entry_],
    104         'fetchedMedia',
    105         function(metadata) {
    106           if (metadata[0])
    107             fulfill(metadata[0]);
    108           else
    109             reject('Failed to load metadata.');
    110         });
    111   }.bind(this));
    112 };
    113 
    114 /**
    115  * Sets the metadata.
    116  * @param {Object} metadata New metadata.
    117  */
    118 Gallery.Item.prototype.setMetadata = function(metadata) {
    119   this.metadata_ = Object.freeze(metadata);
    120 };
    121 
    122 /**
    123  * @return {string} File name.
    124  */
    125 Gallery.Item.prototype.getFileName = function() {
    126   return this.entry_.name;
    127 };
    128 
    129 /**
    130  * @return {boolean} True if this image has not been created in this session.
    131  */
    132 Gallery.Item.prototype.isOriginal = function() { return this.original_; };
    133 
    134 /**
    135  * Obtains the last accessed date.
    136  * @return {number} Last accessed date.
    137  */
    138 Gallery.Item.prototype.getLastAccessedDate = function() {
    139   return this.lastAccessed_;
    140 };
    141 
    142 /**
    143  * Updates the last accessed date.
    144  */
    145 Gallery.Item.prototype.touch = function() {
    146   this.lastAccessed_ = Date.now();
    147 };
    148 
    149 // TODO: Localize?
    150 /**
    151  * @type {string} Suffix for a edited copy file name.
    152  */
    153 Gallery.Item.COPY_SIGNATURE = ' - Edited';
    154 
    155 /**
    156  * Regular expression to match '... - Edited'.
    157  * @type {RegExp}
    158  */
    159 Gallery.Item.REGEXP_COPY_0 =
    160     new RegExp('^(.+)' + Gallery.Item.COPY_SIGNATURE + '$');
    161 
    162 /**
    163  * Regular expression to match '... - Edited (N)'.
    164  * @type {RegExp}
    165  */
    166 Gallery.Item.REGEXP_COPY_N =
    167     new RegExp('^(.+)' + Gallery.Item.COPY_SIGNATURE + ' \\((\\d+)\\)$');
    168 
    169 /**
    170  * Creates a name for an edited copy of the file.
    171  *
    172  * @param {DirectoryEntry} dirEntry Entry.
    173  * @param {function} callback Callback.
    174  * @private
    175  */
    176 Gallery.Item.prototype.createCopyName_ = function(dirEntry, callback) {
    177   var name = this.getFileName();
    178 
    179   // If the item represents a file created during the current Gallery session
    180   // we reuse it for subsequent saves instead of creating multiple copies.
    181   if (!this.original_) {
    182     callback(name);
    183     return;
    184   }
    185 
    186   var ext = '';
    187   var index = name.lastIndexOf('.');
    188   if (index != -1) {
    189     ext = name.substr(index);
    190     name = name.substr(0, index);
    191   }
    192 
    193   if (!ext.match(/jpe?g/i)) {
    194     // Chrome can natively encode only two formats: JPEG and PNG.
    195     // All non-JPEG images are saved in PNG, hence forcing the file extension.
    196     ext = '.png';
    197   }
    198 
    199   function tryNext(tries) {
    200     // All the names are used. Let's overwrite the last one.
    201     if (tries == 0) {
    202       setTimeout(callback, 0, name + ext);
    203       return;
    204     }
    205 
    206     // If the file name contains the copy signature add/advance the sequential
    207     // number.
    208     var matchN = Gallery.Item.REGEXP_COPY_N.exec(name);
    209     var match0 = Gallery.Item.REGEXP_COPY_0.exec(name);
    210     if (matchN && matchN[1] && matchN[2]) {
    211       var copyNumber = parseInt(matchN[2], 10) + 1;
    212       name = matchN[1] + Gallery.Item.COPY_SIGNATURE + ' (' + copyNumber + ')';
    213     } else if (match0 && match0[1]) {
    214       name = match0[1] + Gallery.Item.COPY_SIGNATURE + ' (1)';
    215     } else {
    216       name += Gallery.Item.COPY_SIGNATURE;
    217     }
    218 
    219     dirEntry.getFile(name + ext, {create: false, exclusive: false},
    220         tryNext.bind(null, tries - 1),
    221         callback.bind(null, name + ext));
    222   }
    223 
    224   tryNext(10);
    225 };
    226 
    227 /**
    228  * Writes the new item content to either the existing or a new file.
    229  *
    230  * @param {VolumeManager} volumeManager Volume manager instance.
    231  * @param {string} fallbackDir Fallback directory in case the current directory
    232  *     is read only.
    233  * @param {boolean} overwrite Whether to overwrite the image to the item or not.
    234  * @param {HTMLCanvasElement} canvas Source canvas.
    235  * @param {ImageEncoder.MetadataEncoder} metadataEncoder MetadataEncoder.
    236  * @param {function(boolean)=} opt_callback Callback accepting true for success.
    237  */
    238 Gallery.Item.prototype.saveToFile = function(
    239     volumeManager, fallbackDir, overwrite, canvas, metadataEncoder,
    240     opt_callback) {
    241   ImageUtil.metrics.startInterval(ImageUtil.getMetricName('SaveTime'));
    242 
    243   var name = this.getFileName();
    244 
    245   var onSuccess = function(entry, locationInfo) {
    246     ImageUtil.metrics.recordEnum(ImageUtil.getMetricName('SaveResult'), 1, 2);
    247     ImageUtil.metrics.recordInterval(ImageUtil.getMetricName('SaveTime'));
    248 
    249     this.entry_ = entry;
    250     this.locationInfo_ = locationInfo;
    251 
    252     this.metadataCache_.clear([this.entry_], 'fetchedMedia');
    253     if (opt_callback)
    254       opt_callback(true);
    255   }.bind(this);
    256 
    257   var onError = function(error) {
    258     console.error('Error saving from gallery', name, error);
    259     ImageUtil.metrics.recordEnum(ImageUtil.getMetricName('SaveResult'), 0, 2);
    260     if (opt_callback)
    261       opt_callback(false);
    262   }
    263 
    264   var doSave = function(newFile, fileEntry) {
    265     fileEntry.createWriter(function(fileWriter) {
    266       function writeContent() {
    267         fileWriter.onwriteend = onSuccess.bind(null, fileEntry);
    268         fileWriter.write(ImageEncoder.getBlob(canvas, metadataEncoder));
    269       }
    270       fileWriter.onerror = function(error) {
    271         onError(error);
    272         // Disable all callbacks on the first error.
    273         fileWriter.onerror = null;
    274         fileWriter.onwriteend = null;
    275       };
    276       if (newFile) {
    277         writeContent();
    278       } else {
    279         fileWriter.onwriteend = writeContent;
    280         fileWriter.truncate(0);
    281       }
    282     }, onError);
    283   }
    284 
    285   var getFile = function(dir, newFile) {
    286     dir.getFile(name, {create: newFile, exclusive: newFile},
    287         function(fileEntry) {
    288           var locationInfo = volumeManager.getLocationInfo(fileEntry);
    289           // If the volume is gone, then abort the saving operation.
    290           if (!locationInfo) {
    291             onError('NotFound');
    292             return;
    293           }
    294           doSave(newFile, fileEntry, locationInfo);
    295         }.bind(this), onError);
    296   }.bind(this);
    297 
    298   var checkExistence = function(dir) {
    299     dir.getFile(name, {create: false, exclusive: false},
    300         getFile.bind(null, dir, false /* existing file */),
    301         getFile.bind(null, dir, true /* create new file */));
    302   }
    303 
    304   var saveToDir = function(dir) {
    305     if (overwrite && !this.locationInfo_.isReadOnly) {
    306       checkExistence(dir);
    307     } else {
    308       this.createCopyName_(dir, function(copyName) {
    309         this.original_ = false;
    310         name = copyName;
    311         checkExistence(dir);
    312       }.bind(this));
    313     }
    314   }.bind(this);
    315 
    316   if (this.locationInfo_.isReadOnly) {
    317     saveToDir(fallbackDir);
    318   } else {
    319     this.entry_.getParent(saveToDir, onError);
    320   }
    321 };
    322 
    323 /**
    324  * Renames the item.
    325  *
    326  * @param {string} displayName New display name (without the extension).
    327  * @return {Promise} Promise fulfilled with when renaming completes, or rejected
    328  *     with the error message.
    329  */
    330 Gallery.Item.prototype.rename = function(displayName) {
    331   var newFileName = this.entry_.name.replace(
    332       ImageUtil.getDisplayNameFromName(this.entry_.name), displayName);
    333 
    334   if (newFileName === this.entry_.name)
    335     return Promise.reject('NOT_CHANGED');
    336 
    337   if (/^\s*$/.test(displayName))
    338     return Promise.reject(str('ERROR_WHITESPACE_NAME'));
    339 
    340   var parentDirectoryPromise = new Promise(
    341       this.entry_.getParent.bind(this.entry_));
    342   return parentDirectoryPromise.then(function(parentDirectory) {
    343     var nameValidatingPromise =
    344         util.validateFileName(parentDirectory, newFileName, true);
    345     return nameValidatingPromise.then(function() {
    346       var existingFilePromise = new Promise(parentDirectory.getFile.bind(
    347           parentDirectory, newFileName, {create: false, exclusive: false}));
    348       return existingFilePromise.then(function() {
    349         return Promise.reject(str('GALLERY_FILE_EXISTS'));
    350       }, function() {
    351         return new Promise(
    352             this.entry_.moveTo.bind(this.entry_, parentDirectory, newFileName));
    353       }.bind(this));
    354     }.bind(this));
    355   }.bind(this)).then(function(entry) {
    356     this.entry_ = entry;
    357   }.bind(this));
    358 };
    359