Landing Recent QUIC Changes.
[chromium-blink-merge.git] / ui / file_manager / gallery / js / gallery_item.js
blob227940a63eeb78b878c0459c841d4eb4c23d59fc
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
14 * @struct
16 Gallery.Item = function(
17 entry, locationInfo, metadata, metadataCache, original) {
18 /**
19 * @type {!FileEntry}
20 * @private
22 this.entry_ = entry;
24 /**
25 * @type {!EntryLocation}
26 * @private
28 this.locationInfo_ = locationInfo;
30 /**
31 * @type {!Object}
32 * @private
34 this.metadata_ = Object.preventExtensions(metadata);
36 /**
37 * @type {!MetadataCache}
38 * @private
39 * @const
41 this.metadataCache_ = metadataCache;
43 // TODO(yawano): Change this.contentImage and this.screenImage to private
44 // fields and provide utility methods for them (e.g. revokeFullImageCache).
45 /**
46 * The content cache is used for prefetching the next image when going through
47 * the images sequentially. The real life photos can be large (18Mpix = 72Mb
48 * pixel array) so we want only the minimum amount of caching.
49 * @type {(HTMLCanvasElement|HTMLImageElement)}
51 this.contentImage = null;
53 /**
54 * We reuse previously generated screen-scale images so that going back to a
55 * recently loaded image looks instant even if the image is not in the content
56 * cache any more. Screen-scale images are small (~1Mpix) so we can afford to
57 * cache more of them.
58 * @type {HTMLCanvasElement}
60 this.screenImage = null;
62 /**
63 * Last accessed date to be used for selecting items whose cache are evicted.
64 * @type {number}
65 * @private
67 this.lastAccessed_ = Date.now();
69 /**
70 * @type {boolean}
71 * @private
73 this.original_ = original;
76 /**
77 * @return {!FileEntry} Image entry.
79 Gallery.Item.prototype.getEntry = function() { return this.entry_; };
81 /**
82 * @return {!EntryLocation} Entry location information.
84 Gallery.Item.prototype.getLocationInfo = function() {
85 return this.locationInfo_;
88 /**
89 * @return {!Object} Metadata.
91 Gallery.Item.prototype.getMetadata = function() { return this.metadata_; };
93 /**
94 * Obtains the latest media metadata.
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.
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.');
111 }.bind(this));
115 * @return {string} File name.
117 Gallery.Item.prototype.getFileName = function() {
118 return this.entry_.name;
122 * @return {boolean} True if this image has not been created in this session.
124 Gallery.Item.prototype.isOriginal = function() { return this.original_; };
127 * Obtains the last accessed date.
128 * @return {number} Last accessed date.
130 Gallery.Item.prototype.getLastAccessedDate = function() {
131 return this.lastAccessed_;
135 * Updates the last accessed date.
137 Gallery.Item.prototype.touch = function() {
138 this.lastAccessed_ = Date.now();
141 // TODO: Localize?
143 * @type {string} Suffix for a edited copy file name.
144 * @const
146 Gallery.Item.COPY_SIGNATURE = ' - Edited';
149 * Regular expression to match '... - Edited'.
150 * @type {!RegExp}
151 * @const
153 Gallery.Item.REGEXP_COPY_0 =
154 new RegExp('^(.+)' + Gallery.Item.COPY_SIGNATURE + '$');
157 * Regular expression to match '... - Edited (N)'.
158 * @type {!RegExp}
159 * @const
161 Gallery.Item.REGEXP_COPY_N =
162 new RegExp('^(.+)' + Gallery.Item.COPY_SIGNATURE + ' \\((\\d+)\\)$');
165 * Creates a name for an edited copy of the file.
167 * @param {!DirectoryEntry} dirEntry Entry.
168 * @param {function(string)} callback Callback.
169 * @private
171 Gallery.Item.prototype.createCopyName_ = function(dirEntry, callback) {
172 var name = this.getFileName();
174 // If the item represents a file created during the current Gallery session
175 // we reuse it for subsequent saves instead of creating multiple copies.
176 if (!this.original_) {
177 callback(name);
178 return;
181 var ext = '';
182 var index = name.lastIndexOf('.');
183 if (index != -1) {
184 ext = name.substr(index);
185 name = name.substr(0, index);
188 if (!ext.match(/jpe?g/i)) {
189 // Chrome can natively encode only two formats: JPEG and PNG.
190 // All non-JPEG images are saved in PNG, hence forcing the file extension.
191 ext = '.png';
194 function tryNext(tries) {
195 // All the names are used. Let's overwrite the last one.
196 if (tries == 0) {
197 setTimeout(callback, 0, name + ext);
198 return;
201 // If the file name contains the copy signature add/advance the sequential
202 // number.
203 var matchN = Gallery.Item.REGEXP_COPY_N.exec(name);
204 var match0 = Gallery.Item.REGEXP_COPY_0.exec(name);
205 if (matchN && matchN[1] && matchN[2]) {
206 var copyNumber = parseInt(matchN[2], 10) + 1;
207 name = matchN[1] + Gallery.Item.COPY_SIGNATURE + ' (' + copyNumber + ')';
208 } else if (match0 && match0[1]) {
209 name = match0[1] + Gallery.Item.COPY_SIGNATURE + ' (1)';
210 } else {
211 name += Gallery.Item.COPY_SIGNATURE;
214 dirEntry.getFile(name + ext, {create: false, exclusive: false},
215 tryNext.bind(null, tries - 1),
216 callback.bind(null, name + ext));
219 tryNext(10);
223 * Writes the new item content to either the existing or a new file.
225 * @param {!VolumeManager} volumeManager Volume manager instance.
226 * @param {DirectoryEntry} fallbackDir Fallback directory in case the current
227 * directory is read only.
228 * @param {boolean} overwrite Whether to overwrite the image to the item or not.
229 * @param {!HTMLCanvasElement} canvas Source canvas.
230 * @param {function(boolean)=} opt_callback Callback accepting true for success.
232 Gallery.Item.prototype.saveToFile = function(
233 volumeManager, fallbackDir, overwrite, canvas, opt_callback) {
234 ImageUtil.metrics.startInterval(ImageUtil.getMetricName('SaveTime'));
236 var name = this.getFileName();
238 var onSuccess = function(entry) {
239 var locationInfo = volumeManager.getLocationInfo(entry);
240 if (!locationInfo) {
241 // Reuse old location info if it fails to obtain location info.
242 locationInfo = this.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 // Updates the metadata.
251 this.metadataCache_.clear([this.entry_], '*');
252 this.metadataCache_.getLatest(
253 [this.entry_],
254 Gallery.METADATA_TYPE,
255 function(metadataList) {
256 if (metadataList.length === 1) {
257 this.metadata_ = metadataList[0];
258 if (opt_callback)
259 opt_callback(true);
260 } else {
261 if (opt_callback)
262 opt_callback(false);
264 }.bind(this));
265 }.bind(this);
267 var onError = function(error) {
268 console.error('Error saving from gallery', name, error);
269 ImageUtil.metrics.recordEnum(ImageUtil.getMetricName('SaveResult'), 0, 2);
270 if (opt_callback)
271 opt_callback(false);
274 var doSave = function(newFile, fileEntry) {
275 fileEntry.createWriter(function(fileWriter) {
276 var writeContent = function() {
277 fileWriter.onwriteend = onSuccess.bind(null, fileEntry);
278 // TODO(hirono): Remove the quality 1 for thumbanils. The code path is
279 // no longer used.
280 var metadataEncoder = ImageEncoder.encodeMetadata(
281 this.metadata_, canvas, 1 /* quality */);
282 // Contrary to what one might think 1.0 is not a good default. Opening
283 // and saving an typical photo taken with consumer camera increases its
284 // file size by 50-100%. Experiments show that 0.9 is much better. It
285 // shrinks some photos a bit, keeps others about the same size, but does
286 // not visibly lower the quality.
287 fileWriter.write(ImageEncoder.getBlob(canvas, metadataEncoder, 0.9));
288 }.bind(this);
289 fileWriter.onerror = function(error) {
290 onError(error);
291 // Disable all callbacks on the first error.
292 fileWriter.onerror = null;
293 fileWriter.onwriteend = null;
295 if (newFile) {
296 writeContent();
297 } else {
298 fileWriter.onwriteend = writeContent;
299 fileWriter.truncate(0);
301 }.bind(this), onError);
302 }.bind(this);
304 var getFile = function(dir, newFile) {
305 dir.getFile(name, {create: newFile, exclusive: newFile},
306 function(fileEntry) {
307 doSave(newFile, fileEntry);
308 }.bind(this), onError);
309 }.bind(this);
311 var checkExistence = function(dir) {
312 dir.getFile(name, {create: false, exclusive: false},
313 getFile.bind(null, dir, false /* existing file */),
314 getFile.bind(null, dir, true /* create new file */));
317 var saveToDir = function(dir) {
318 if (overwrite && !this.locationInfo_.isReadOnly) {
319 checkExistence(dir);
320 } else {
321 this.createCopyName_(dir, function(copyName) {
322 this.original_ = false;
323 name = copyName;
324 checkExistence(dir);
325 }.bind(this));
327 }.bind(this);
329 if (this.locationInfo_.isReadOnly) {
330 saveToDir(fallbackDir);
331 } else {
332 this.entry_.getParent(saveToDir, onError);
337 * Renames the item.
339 * @param {string} displayName New display name (without the extension).
340 * @return {!Promise} Promise fulfilled with when renaming completes, or
341 * rejected with the error message.
343 Gallery.Item.prototype.rename = function(displayName) {
344 var newFileName = this.entry_.name.replace(
345 ImageUtil.getDisplayNameFromName(this.entry_.name), displayName);
347 if (newFileName === this.entry_.name)
348 return Promise.reject('NOT_CHANGED');
350 if (/^\s*$/.test(displayName))
351 return Promise.reject(str('ERROR_WHITESPACE_NAME'));
353 var parentDirectoryPromise = new Promise(
354 this.entry_.getParent.bind(this.entry_));
355 return parentDirectoryPromise.then(function(parentDirectory) {
356 var nameValidatingPromise =
357 util.validateFileName(parentDirectory, newFileName, true);
358 return nameValidatingPromise.then(function() {
359 var existingFilePromise = new Promise(parentDirectory.getFile.bind(
360 parentDirectory, newFileName, {create: false, exclusive: false}));
361 return existingFilePromise.then(function() {
362 return Promise.reject(str('GALLERY_FILE_EXISTS'));
363 }, function() {
364 return new Promise(
365 this.entry_.moveTo.bind(this.entry_, parentDirectory, newFileName));
366 }.bind(this));
367 }.bind(this));
368 }.bind(this)).then(function(entry) {
369 this.entry_ = entry;
370 }.bind(this));