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.
8 * Object representing an image item (a photo).
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.
17 Gallery.Item = function(
18 entry, locationInfo, metadata, metadataCache, original) {
26 * @type {EntryLocation}
29 this.locationInfo_ = locationInfo;
35 this.metadata_ = Object.freeze(metadata);
38 * @type {MetadataCache}
41 this.metadataCache_ = metadataCache;
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.
49 this.screenImage = null;
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
58 this.contentImage = null;
61 * Last accessed date to be used for selecting items whose cache are evicted.
65 this.lastAccessed_ = Date.now();
71 this.original_ = original;
77 * @return {FileEntry} Image entry.
79 Gallery.Item.prototype.getEntry = function() { return this.entry_; };
82 * @return {EntryLocation} Entry location information.
84 Gallery.Item.prototype.getLocationInfo = function() {
85 return this.locationInfo_;
89 * @return {Object} Metadata.
91 Gallery.Item.prototype.getMetadata = function() { return this.metadata_; };
94 * Obtains the latest media metadata.
96 * This is a heavy operation since it forces to load the image data to obtain
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(
107 fulfill(metadata[0]);
109 reject('Failed to load metadata.');
116 * @param {Object} metadata New metadata.
118 Gallery.Item.prototype.setMetadata = function(metadata) {
119 this.metadata_ = Object.freeze(metadata);
123 * @return {string} File name.
125 Gallery.Item.prototype.getFileName = function() {
126 return this.entry_.name;
130 * @return {boolean} True if this image has not been created in this session.
132 Gallery.Item.prototype.isOriginal = function() { return this.original_; };
135 * Obtains the last accessed date.
136 * @return {number} Last accessed date.
138 Gallery.Item.prototype.getLastAccessedDate = function() {
139 return this.lastAccessed_;
143 * Updates the last accessed date.
145 Gallery.Item.prototype.touch = function() {
146 this.lastAccessed_ = Date.now();
151 * @type {string} Suffix for a edited copy file name.
153 Gallery.Item.COPY_SIGNATURE = ' - Edited';
156 * Regular expression to match '... - Edited'.
159 Gallery.Item.REGEXP_COPY_0 =
160 new RegExp('^(.+)' + Gallery.Item.COPY_SIGNATURE + '$');
163 * Regular expression to match '... - Edited (N)'.
166 Gallery.Item.REGEXP_COPY_N =
167 new RegExp('^(.+)' + Gallery.Item.COPY_SIGNATURE + ' \\((\\d+)\\)$');
170 * Creates a name for an edited copy of the file.
172 * @param {DirectoryEntry} dirEntry Entry.
173 * @param {function} callback Callback.
176 Gallery.Item.prototype.createCopyName_ = function(dirEntry, callback) {
177 var name = this.getFileName();
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_) {
187 var index = name.lastIndexOf('.');
189 ext = name.substr(index);
190 name = name.substr(0, index);
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.
199 function tryNext(tries) {
200 // All the names are used. Let's overwrite the last one.
202 setTimeout(callback, 0, name + ext);
206 // If the file name contains the copy signature add/advance the sequential
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)';
216 name += Gallery.Item.COPY_SIGNATURE;
219 dirEntry.getFile(name + ext, {create: false, exclusive: false},
220 tryNext.bind(null, tries - 1),
221 callback.bind(null, name + ext));
228 * Writes the new item content to either the existing or a new file.
230 * @param {VolumeManager} volumeManager Volume manager instance.
231 * @param {string} fallbackDir Fallback directory in case the current directory
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.
238 Gallery.Item.prototype.saveToFile = function(
239 volumeManager, fallbackDir, overwrite, canvas, metadataEncoder,
241 ImageUtil.metrics.startInterval(ImageUtil.getMetricName('SaveTime'));
243 var name = this.getFileName();
245 var onSuccess = function(entry, locationInfo) {
246 ImageUtil.metrics.recordEnum(ImageUtil.getMetricName('SaveResult'), 1, 2);
247 ImageUtil.metrics.recordInterval(ImageUtil.getMetricName('SaveTime'));
250 this.locationInfo_ = locationInfo;
252 this.metadataCache_.clear([this.entry_], 'fetchedMedia');
257 var onError = function(error) {
258 console.error('Error saving from gallery', name, error);
259 ImageUtil.metrics.recordEnum(ImageUtil.getMetricName('SaveResult'), 0, 2);
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));
270 fileWriter.onerror = function(error) {
272 // Disable all callbacks on the first error.
273 fileWriter.onerror = null;
274 fileWriter.onwriteend = null;
279 fileWriter.onwriteend = writeContent;
280 fileWriter.truncate(0);
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.
294 doSave(newFile, fileEntry, locationInfo);
295 }.bind(this), onError);
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 */));
304 var saveToDir = function(dir) {
305 if (overwrite && !this.locationInfo_.isReadOnly) {
308 this.createCopyName_(dir, function(copyName) {
309 this.original_ = false;
316 if (this.locationInfo_.isReadOnly) {
317 saveToDir(fallbackDir);
319 this.entry_.getParent(saveToDir, onError);
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.
330 Gallery.Item.prototype.rename = function(displayName) {
331 var newFileName = this.entry_.name.replace(
332 ImageUtil.getDisplayNameFromName(this.entry_.name), displayName);
334 if (newFileName === this.entry_.name)
335 return Promise.reject('NOT_CHANGED');
337 if (/^\s*$/.test(displayName))
338 return Promise.reject(str('ERROR_WHITESPACE_NAME'));
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'));
352 this.entry_.moveTo.bind(this.entry_, parentDirectory, newFileName));
355 }.bind(this)).then(function(entry) {