Move action_runner.py out of actions folder prior to moving actions to internal.
[chromium-blink-merge.git] / ui / file_manager / gallery / js / gallery_item.js
blobc9088d81e619a317d0f0467e7c0671d0f1bf7e48
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.
5 /**
6  * Object representing an image item (a photo).
7  *
8  * @param {!FileEntry} entry Image entry.
9  * @param {!EntryLocation} locationInfo Entry location information.
10  * @param {!MetadataItem} metadataItem
11  * @param {!ThumbnailMetadataItem} thumbnailMetadataItem
12  * @param {boolean} original Whether the entry is original or edited.
13  * @constructor
14  * @struct
15  */
16 Gallery.Item = function(
17     entry, locationInfo, metadataItem, thumbnailMetadataItem, original) {
18   /**
19    * @private {!FileEntry}
20    */
21   this.entry_ = entry;
23   /**
24    * @private {!EntryLocation}
25    */
26   this.locationInfo_ = locationInfo;
28   /**
29    * @private {!MetadataItem}
30    */
31   this.metadataItem_ = metadataItem;
33   /**
34    * @private {!ThumbnailMetadataItem}
35    */
36   this.thumbnailMetadataItem_ = metadataItem;
38   // TODO(yawano): Change this.contentImage and this.screenImage to private
39   // fields and provide utility methods for them (e.g. revokeFullImageCache).
40   /**
41    * The content cache is used for prefetching the next image when going through
42    * the images sequentially. The real life photos can be large (18Mpix = 72Mb
43    * pixel array) so we want only the minimum amount of caching.
44    * @type {(HTMLCanvasElement|HTMLImageElement)}
45    */
46   this.contentImage = null;
48   /**
49    * We reuse previously generated screen-scale images so that going back to a
50    * recently loaded image looks instant even if the image is not in the content
51    * cache any more. Screen-scale images are small (~1Mpix) so we can afford to
52    * cache more of them.
53    * @type {HTMLCanvasElement}
54    */
55   this.screenImage = null;
57   /**
58    * Last accessed date to be used for selecting items whose cache are evicted.
59    * @type {number}
60    * @private
61    */
62   this.lastAccessed_ = Date.now();
64   /**
65    * @type {boolean}
66    * @private
67    */
68   this.original_ = original;
71 /**
72  * @return {!FileEntry} Image entry.
73  */
74 Gallery.Item.prototype.getEntry = function() { return this.entry_; };
76 /**
77  * @return {!EntryLocation} Entry location information.
78  */
79 Gallery.Item.prototype.getLocationInfo = function() {
80   return this.locationInfo_;
83 /**
84  * @return {!MetadataItem} Metadata.
85  */
86 Gallery.Item.prototype.getMetadataItem = function() {
87   return this.metadataItem_;
90 /**
91  * @return {!ThumbnailMetadataItem} Thumbnail metadata item.
92  */
93 Gallery.Item.prototype.getThumbnailMetadataItem = function() {
94   return this.thumbnailMetadataItem_;
97 /**
98  * @return {string} File name.
99  */
100 Gallery.Item.prototype.getFileName = function() {
101   return this.entry_.name;
105  * @return {boolean} True if this image has not been created in this session.
106  */
107 Gallery.Item.prototype.isOriginal = function() { return this.original_; };
110  * Obtains the last accessed date.
111  * @return {number} Last accessed date.
112  */
113 Gallery.Item.prototype.getLastAccessedDate = function() {
114   return this.lastAccessed_;
118  * Updates the last accessed date.
119  */
120 Gallery.Item.prototype.touch = function() {
121   this.lastAccessed_ = Date.now();
124 // TODO: Localize?
126  * @type {string} Suffix for a edited copy file name.
127  * @const
128  */
129 Gallery.Item.COPY_SIGNATURE = ' - Edited';
132  * Regular expression to match '... - Edited'.
133  * @type {!RegExp}
134  * @const
135  */
136 Gallery.Item.REGEXP_COPY_0 =
137     new RegExp('^(.+)' + Gallery.Item.COPY_SIGNATURE + '$');
140  * Regular expression to match '... - Edited (N)'.
141  * @type {!RegExp}
142  * @const
143  */
144 Gallery.Item.REGEXP_COPY_N =
145     new RegExp('^(.+)' + Gallery.Item.COPY_SIGNATURE + ' \\((\\d+)\\)$');
148  * Creates a name for an edited copy of the file.
150  * @param {!DirectoryEntry} dirEntry Entry.
151  * @param {string} newMimeType Mime type of new image.
152  * @param {function(string)} callback Callback.
153  * @private
154  */
155 Gallery.Item.prototype.createCopyName_ = function(
156     dirEntry, newMimeType, callback) {
157   var name = this.getFileName();
159   // If the item represents a file created during the current Gallery session
160   // we reuse it for subsequent saves instead of creating multiple copies.
161   if (!this.original_) {
162     callback(name);
163     return;
164   }
166   var baseName = name.replace(/\.[^\.\/]+$/, '');
167   var ext = newMimeType === 'image/jpeg' ? '.jpg' : '.png';
169   function tryNext(tries) {
170     // All the names are used. Let's overwrite the last one.
171     if (tries == 0) {
172       setTimeout(callback, 0, baseName + ext);
173       return;
174     }
176     // If the file name contains the copy signature add/advance the sequential
177     // number.
178     var matchN = Gallery.Item.REGEXP_COPY_N.exec(baseName);
179     var match0 = Gallery.Item.REGEXP_COPY_0.exec(baseName);
180     if (matchN && matchN[1] && matchN[2]) {
181       var copyNumber = parseInt(matchN[2], 10) + 1;
182       baseName = matchN[1] + Gallery.Item.COPY_SIGNATURE +
183           ' (' + copyNumber + ')';
184     } else if (match0 && match0[1]) {
185       baseName = match0[1] + Gallery.Item.COPY_SIGNATURE + ' (1)';
186     } else {
187       baseName += Gallery.Item.COPY_SIGNATURE;
188     }
190     dirEntry.getFile(baseName + ext, {create: false, exclusive: false},
191         tryNext.bind(null, tries - 1),
192         callback.bind(null, baseName + ext));
193   }
195   tryNext(10);
199  * Writes the new item content to either the existing or a new file.
201  * @param {!VolumeManager} volumeManager Volume manager instance.
202  * @param {!MetadataModel} metadataModel
203  * @param {DirectoryEntry} fallbackDir Fallback directory in case the current
204  *     directory is read only.
205  * @param {boolean} overwrite Whether to overwrite the image to the item or not.
206  * @param {!HTMLCanvasElement} canvas Source canvas.
207  * @param {function(boolean)} callback Callback accepting true for success.
208  */
209 Gallery.Item.prototype.saveToFile = function(
210     volumeManager, metadataModel, fallbackDir, overwrite, canvas, callback) {
211   ImageUtil.metrics.startInterval(ImageUtil.getMetricName('SaveTime'));
213   var name = this.getFileName();
214   var newMimeType = name.match(/\.jpe?g$/i) || FileType.isRaw(this.entry_) ?
215       'image/jpeg' : 'image/png';
217   var onSuccess = function(entry) {
218     var locationInfo = volumeManager.getLocationInfo(entry);
219     if (!locationInfo) {
220       // Reuse old location info if it fails to obtain location info.
221       locationInfo = this.locationInfo_;
222     }
223     ImageUtil.metrics.recordEnum(ImageUtil.getMetricName('SaveResult'), 1, 2);
224     ImageUtil.metrics.recordInterval(ImageUtil.getMetricName('SaveTime'));
226     this.entry_ = entry;
227     this.locationInfo_ = locationInfo;
229     // Updates the metadata.
230     metadataModel.notifyEntriesChanged([this.entry_]);
231     Promise.all([
232       metadataModel.get([entry], Gallery.PREFETCH_PROPERTY_NAMES),
233       new ThumbnailModel(metadataModel).get([entry])
234     ]).then(function(metadataLists) {
235       this.metadataItem_ = metadataLists[0][0];
236       this.thumbnailMetadataItem_ = metadataLists[1][0];
237       callback(true);
238     }.bind(this), function() {
239       callback(false);
240     });
241   }.bind(this);
243   var onError = function(error) {
244     console.error('Error saving from gallery', name, error);
245     ImageUtil.metrics.recordEnum(ImageUtil.getMetricName('SaveResult'), 0, 2);
246     if (callback)
247       callback(false);
248   };
250   var doSave = function(newFile, fileEntry) {
251     var blob;
252     var fileWriter;
254     metadataModel.get(
255         [fileEntry],
256         ['mediaMimeType', 'contentMimeType', 'ifd', 'exifLittleEndian']
257     ).then(function(metadataItems) {
258       // Create the blob of new image.
259       var metadataItem = metadataItems[0];
260       metadataItem.modificationTime = new Date();
261       metadataItem.mediaMimeType = newMimeType;
262       var metadataEncoder = ImageEncoder.encodeMetadata(
263           metadataItem, canvas, /* quality for thumbnail*/ 0.8);
264       // Contrary to what one might think 1.0 is not a good default. Opening
265       // and saving an typical photo taken with consumer camera increases
266       // its file size by 50-100%. Experiments show that 0.9 is much better.
267       // It shrinks some photos a bit, keeps others about the same size, but
268       // does not visibly lower the quality.
269       blob = ImageEncoder.getBlob(canvas, metadataEncoder, 0.9);
270     }.bind(this)).then(function() {
271       // Create writer.
272       return new Promise(function(fullfill, reject) {
273         fileEntry.createWriter(fullfill, reject);
274       });
275     }).then(function(writer) {
276       fileWriter = writer;
278       // Truncates the file to 0 byte if it overwrites.
279       return new Promise(function(fulfill, reject) {
280         if (!newFile) {
281           fileWriter.onerror = reject;
282           fileWriter.onwriteend = fulfill;
283           fileWriter.truncate(0);
284         } else {
285           fulfill(null);
286         }
287       });
288     }).then(function() {
289       // Writes the blob of new image.
290       return new Promise(function(fulfill, reject) {
291         fileWriter.onerror = reject;
292         fileWriter.onwriteend = fulfill;
293         fileWriter.write(blob);
294       });
295     }).then(onSuccess.bind(null, fileEntry))
296     .catch(function(error) {
297       onError(error);
298       if (fileWriter) {
299         // Disable all callbacks on the first error.
300         fileWriter.onerror = null;
301         fileWriter.onwriteend = null;
302       }
303     });
304   }.bind(this);
306   var getFile = function(dir, newFile) {
307     dir.getFile(name, {create: newFile, exclusive: newFile},
308         function(fileEntry) {
309           doSave(newFile, fileEntry);
310         }.bind(this), onError);
311   }.bind(this);
313   var checkExistence = function(dir) {
314     dir.getFile(name, {create: false, exclusive: false},
315         getFile.bind(null, dir, false /* existing file */),
316         getFile.bind(null, dir, true /* create new file */));
317   };
319   var saveToDir = function(dir) {
320     if (overwrite &&
321         !this.locationInfo_.isReadOnly &&
322         !FileType.isRaw(this.entry_)) {
323       checkExistence(dir);
324     } else {
325       this.createCopyName_(dir, newMimeType, function(copyName) {
326         this.original_ = false;
327         name = copyName;
328         checkExistence(dir);
329       }.bind(this));
330     }
331   }.bind(this);
333   if (this.locationInfo_.isReadOnly) {
334     saveToDir(fallbackDir);
335   } else {
336     this.entry_.getParent(saveToDir, onError);
337   }
341  * Renames the item.
343  * @param {string} displayName New display name (without the extension).
344  * @return {!Promise} Promise fulfilled with when renaming completes, or
345  *     rejected with the error message.
346  */
347 Gallery.Item.prototype.rename = function(displayName) {
348   var newFileName = this.entry_.name.replace(
349       ImageUtil.getDisplayNameFromName(this.entry_.name), displayName);
351   if (newFileName === this.entry_.name)
352     return Promise.reject('NOT_CHANGED');
354   if (/^\s*$/.test(displayName))
355     return Promise.reject(str('ERROR_WHITESPACE_NAME'));
357   var parentDirectoryPromise = new Promise(
358       this.entry_.getParent.bind(this.entry_));
359   return parentDirectoryPromise.then(function(parentDirectory) {
360     var nameValidatingPromise =
361         util.validateFileName(parentDirectory, newFileName, true);
362     return nameValidatingPromise.then(function() {
363       var existingFilePromise = new Promise(parentDirectory.getFile.bind(
364           parentDirectory, newFileName, {create: false, exclusive: false}));
365       return existingFilePromise.then(function() {
366         return Promise.reject(str('GALLERY_FILE_EXISTS'));
367       }, function() {
368         return new Promise(
369             this.entry_.moveTo.bind(this.entry_, parentDirectory, newFileName));
370       }.bind(this));
371     }.bind(this));
372   }.bind(this)).then(function(entry) {
373     this.entry_ = entry;
374   }.bind(this));