Explicitly add python-numpy dependency to install-build-deps.
[chromium-blink-merge.git] / ui / file_manager / gallery / js / gallery_item.js
blob994d5a244452fcc22a5248a794420e239149581e
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).
8 * @param {FileEntry} entry Image entry.
9 * @param {EntryLocation} locationInfo Entry location information.
10 * @param {Object} metadata Metadata for the entry.
11 * @param {MetadataCache} metadataCache Metadata cache instance.
12 * @param {boolean} original Whether the entry is original or edited.
13 * @constructor
15 Gallery.Item = function(
16 entry, locationInfo, metadata, metadataCache, original) {
17 /**
18 * @type {FileEntry}
19 * @private
21 this.entry_ = entry;
23 /**
24 * @type {EntryLocation}
25 * @private
27 this.locationInfo_ = locationInfo;
29 /**
30 * @type {Object}
31 * @private
33 this.metadata_ = Object.freeze(metadata);
35 /**
36 * @type {MetadataCache}
37 * @private
39 this.metadataCache_ = metadataCache;
41 /**
42 * The content cache is used for prefetching the next image when going through
43 * the images sequentially. The real life photos can be large (18Mpix = 72Mb
44 * pixel array) so we want only the minimum amount of caching.
45 * @type {HTMLCanvasElement}
47 this.screenImage = null;
49 /**
50 * We reuse previously generated screen-scale images so that going back to a
51 * recently loaded image looks instant even if the image is not in the content
52 * cache any more. Screen-scale images are small (~1Mpix) so we can afford to
53 * cache more of them.
54 * @type {HTMLCanvasElement}
56 this.contentImage = null;
58 /**
59 * Last accessed date to be used for selecting items whose cache are evicted.
60 * @type {number}
61 * @private
63 this.lastAccessed_ = Date.now();
65 /**
66 * @type {boolean}
67 * @private
69 this.original_ = original;
71 Object.seal(this);
74 /**
75 * @return {FileEntry} Image entry.
77 Gallery.Item.prototype.getEntry = function() { return this.entry_; };
79 /**
80 * @return {EntryLocation} Entry location information.
82 Gallery.Item.prototype.getLocationInfo = function() {
83 return this.locationInfo_;
86 /**
87 * @return {Object} Metadata.
89 Gallery.Item.prototype.getMetadata = function() { return this.metadata_; };
91 /**
92 * Obtains the latest media metadata.
94 * This is a heavy operation since it forces to load the image data to obtain
95 * the metadata.
96 * @return {Promise} Promise to be fulfilled with fetched metadata.
98 Gallery.Item.prototype.getFetchedMedia = function() {
99 return new Promise(function(fulfill, reject) {
100 this.metadataCache_.getLatest(
101 [this.entry_],
102 'fetchedMedia',
103 function(metadata) {
104 if (metadata[0])
105 fulfill(metadata[0]);
106 else
107 reject('Failed to load metadata.');
109 }.bind(this));
113 * Sets the metadata.
114 * @param {Object} metadata New metadata.
116 Gallery.Item.prototype.setMetadata = function(metadata) {
117 this.metadata_ = Object.freeze(metadata);
121 * @return {string} File name.
123 Gallery.Item.prototype.getFileName = function() {
124 return this.entry_.name;
128 * @return {boolean} True if this image has not been created in this session.
130 Gallery.Item.prototype.isOriginal = function() { return this.original_; };
133 * Obtains the last accessed date.
134 * @return {number} Last accessed date.
136 Gallery.Item.prototype.getLastAccessedDate = function() {
137 return this.lastAccessed_;
141 * Updates the last accessed date.
143 Gallery.Item.prototype.touch = function() {
144 this.lastAccessed_ = Date.now();
147 // TODO: Localize?
149 * @type {string} Suffix for a edited copy file name.
151 Gallery.Item.COPY_SIGNATURE = ' - Edited';
154 * Regular expression to match '... - Edited'.
155 * @type {RegExp}
157 Gallery.Item.REGEXP_COPY_0 =
158 new RegExp('^(.+)' + Gallery.Item.COPY_SIGNATURE + '$');
161 * Regular expression to match '... - Edited (N)'.
162 * @type {RegExp}
164 Gallery.Item.REGEXP_COPY_N =
165 new RegExp('^(.+)' + Gallery.Item.COPY_SIGNATURE + ' \\((\\d+)\\)$');
168 * Creates a name for an edited copy of the file.
170 * @param {DirectoryEntry} dirEntry Entry.
171 * @param {function(string)} callback Callback.
172 * @private
174 Gallery.Item.prototype.createCopyName_ = function(dirEntry, callback) {
175 var name = this.getFileName();
177 // If the item represents a file created during the current Gallery session
178 // we reuse it for subsequent saves instead of creating multiple copies.
179 if (!this.original_) {
180 callback(name);
181 return;
184 var ext = '';
185 var index = name.lastIndexOf('.');
186 if (index != -1) {
187 ext = name.substr(index);
188 name = name.substr(0, index);
191 if (!ext.match(/jpe?g/i)) {
192 // Chrome can natively encode only two formats: JPEG and PNG.
193 // All non-JPEG images are saved in PNG, hence forcing the file extension.
194 ext = '.png';
197 function tryNext(tries) {
198 // All the names are used. Let's overwrite the last one.
199 if (tries == 0) {
200 setTimeout(callback, 0, name + ext);
201 return;
204 // If the file name contains the copy signature add/advance the sequential
205 // number.
206 var matchN = Gallery.Item.REGEXP_COPY_N.exec(name);
207 var match0 = Gallery.Item.REGEXP_COPY_0.exec(name);
208 if (matchN && matchN[1] && matchN[2]) {
209 var copyNumber = parseInt(matchN[2], 10) + 1;
210 name = matchN[1] + Gallery.Item.COPY_SIGNATURE + ' (' + copyNumber + ')';
211 } else if (match0 && match0[1]) {
212 name = match0[1] + Gallery.Item.COPY_SIGNATURE + ' (1)';
213 } else {
214 name += Gallery.Item.COPY_SIGNATURE;
217 dirEntry.getFile(name + ext, {create: false, exclusive: false},
218 tryNext.bind(null, tries - 1),
219 callback.bind(null, name + ext));
222 tryNext(10);
226 * Writes the new item content to either the existing or a new file.
228 * @param {VolumeManager} volumeManager Volume manager instance.
229 * @param {string} fallbackDir Fallback directory in case the current directory
230 * is read only.
231 * @param {boolean} overwrite Whether to overwrite the image to the item or not.
232 * @param {HTMLCanvasElement} canvas Source canvas.
233 * @param {ImageEncoder.MetadataEncoder} metadataEncoder MetadataEncoder.
234 * @param {function(boolean)=} opt_callback Callback accepting true for success.
236 Gallery.Item.prototype.saveToFile = function(
237 volumeManager, fallbackDir, overwrite, canvas, metadataEncoder,
238 opt_callback) {
239 ImageUtil.metrics.startInterval(ImageUtil.getMetricName('SaveTime'));
241 var name = this.getFileName();
243 var onSuccess = function(entry, locationInfo) {
244 ImageUtil.metrics.recordEnum(ImageUtil.getMetricName('SaveResult'), 1, 2);
245 ImageUtil.metrics.recordInterval(ImageUtil.getMetricName('SaveTime'));
247 this.entry_ = entry;
248 this.locationInfo_ = locationInfo;
250 this.metadataCache_.clear([this.entry_], 'fetchedMedia');
251 if (opt_callback)
252 opt_callback(true);
253 }.bind(this);
255 var onError = function(error) {
256 console.error('Error saving from gallery', name, error);
257 ImageUtil.metrics.recordEnum(ImageUtil.getMetricName('SaveResult'), 0, 2);
258 if (opt_callback)
259 opt_callback(false);
262 var doSave = function(newFile, fileEntry) {
263 fileEntry.createWriter(function(fileWriter) {
264 function writeContent() {
265 fileWriter.onwriteend = onSuccess.bind(null, fileEntry);
266 fileWriter.write(ImageEncoder.getBlob(canvas, metadataEncoder));
268 fileWriter.onerror = function(error) {
269 onError(error);
270 // Disable all callbacks on the first error.
271 fileWriter.onerror = null;
272 fileWriter.onwriteend = null;
274 if (newFile) {
275 writeContent();
276 } else {
277 fileWriter.onwriteend = writeContent;
278 fileWriter.truncate(0);
280 }, onError);
283 var getFile = function(dir, newFile) {
284 dir.getFile(name, {create: newFile, exclusive: newFile},
285 function(fileEntry) {
286 var locationInfo = volumeManager.getLocationInfo(fileEntry);
287 // If the volume is gone, then abort the saving operation.
288 if (!locationInfo) {
289 onError('NotFound');
290 return;
292 doSave(newFile, fileEntry, locationInfo);
293 }.bind(this), onError);
294 }.bind(this);
296 var checkExistence = function(dir) {
297 dir.getFile(name, {create: false, exclusive: false},
298 getFile.bind(null, dir, false /* existing file */),
299 getFile.bind(null, dir, true /* create new file */));
302 var saveToDir = function(dir) {
303 if (overwrite && !this.locationInfo_.isReadOnly) {
304 checkExistence(dir);
305 } else {
306 this.createCopyName_(dir, function(copyName) {
307 this.original_ = false;
308 name = copyName;
309 checkExistence(dir);
310 }.bind(this));
312 }.bind(this);
314 if (this.locationInfo_.isReadOnly) {
315 saveToDir(fallbackDir);
316 } else {
317 this.entry_.getParent(saveToDir, onError);
322 * Renames the item.
324 * @param {string} displayName New display name (without the extension).
325 * @return {Promise} Promise fulfilled with when renaming completes, or rejected
326 * with the error message.
328 Gallery.Item.prototype.rename = function(displayName) {
329 var newFileName = this.entry_.name.replace(
330 ImageUtil.getDisplayNameFromName(this.entry_.name), displayName);
332 if (newFileName === this.entry_.name)
333 return Promise.reject('NOT_CHANGED');
335 if (/^\s*$/.test(displayName))
336 return Promise.reject(str('ERROR_WHITESPACE_NAME'));
338 var parentDirectoryPromise = new Promise(
339 this.entry_.getParent.bind(this.entry_));
340 return parentDirectoryPromise.then(function(parentDirectory) {
341 var nameValidatingPromise =
342 util.validateFileName(parentDirectory, newFileName, true);
343 return nameValidatingPromise.then(function() {
344 var existingFilePromise = new Promise(parentDirectory.getFile.bind(
345 parentDirectory, newFileName, {create: false, exclusive: false}));
346 return existingFilePromise.then(function() {
347 return Promise.reject(str('GALLERY_FILE_EXISTS'));
348 }, function() {
349 return new Promise(
350 this.entry_.moveTo.bind(this.entry_, parentDirectory, newFileName));
351 }.bind(this));
352 }.bind(this));
353 }.bind(this)).then(function(entry) {
354 this.entry_ = entry;
355 }.bind(this));