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.
6 * Object representing an image item (a photo).
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.
16 Gallery.Item = function(
17 entry, locationInfo, metadataItem, thumbnailMetadataItem, original) {
19 * @private {!FileEntry}
24 * @private {!EntryLocation}
26 this.locationInfo_ = locationInfo;
29 * @private {MetadataItem}
31 this.metadataItem_ = metadataItem;
34 * @private {ThumbnailMetadataItem}
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).
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)}
46 this.contentImage = null;
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
53 * @type {HTMLCanvasElement}
55 this.screenImage = null;
58 * Last accessed date to be used for selecting items whose cache are evicted.
62 this.lastAccessed_ = Date.now();
68 this.original_ = original;
72 * @return {!FileEntry} Image entry.
74 Gallery.Item.prototype.getEntry = function() { return this.entry_; };
77 * @return {!EntryLocation} Entry location information.
79 Gallery.Item.prototype.getLocationInfo = function() {
80 return this.locationInfo_;
84 * @return {MetadataItem} Metadata.
86 Gallery.Item.prototype.getMetadataItem = function() {
87 return this.metadataItem_;
91 * @param {!MetadataItem} metadata
93 Gallery.Item.prototype.setMetadataItem = function(metadata) {
94 this.metadataItem_ = metadata;
98 * @return {ThumbnailMetadataItem} Thumbnail metadata item.
100 Gallery.Item.prototype.getThumbnailMetadataItem = function() {
101 return this.thumbnailMetadataItem_;
105 * @param {!ThumbnailMetadataItem} item Thumbnail metadata item.
107 Gallery.Item.prototype.setThumbnailMetadataItem = function(item) {
108 this.thumbnailMetadataItem_ = item;
112 * @return {string} File name.
114 Gallery.Item.prototype.getFileName = function() {
115 return this.entry_.name;
119 * @return {boolean} True if this image has not been created in this session.
121 Gallery.Item.prototype.isOriginal = function() { return this.original_; };
124 * Obtains the last accessed date.
125 * @return {number} Last accessed date.
127 Gallery.Item.prototype.getLastAccessedDate = function() {
128 return this.lastAccessed_;
132 * Updates the last accessed date.
134 Gallery.Item.prototype.touch = function() {
135 this.lastAccessed_ = Date.now();
140 * @type {string} Suffix for a edited copy file name.
143 Gallery.Item.COPY_SIGNATURE = ' - Edited';
146 * Regular expression to match '... - Edited'.
150 Gallery.Item.REGEXP_COPY_0 =
151 new RegExp('^(.+)' + Gallery.Item.COPY_SIGNATURE + '$');
154 * Regular expression to match '... - Edited (N)'.
158 Gallery.Item.REGEXP_COPY_N =
159 new RegExp('^(.+)' + Gallery.Item.COPY_SIGNATURE + ' \\((\\d+)\\)$');
162 * Creates a name for an edited copy of the file.
164 * @param {!DirectoryEntry} dirEntry Entry.
165 * @param {string} newMimeType Mime type of new image.
166 * @param {function(string)} callback Callback.
169 Gallery.Item.prototype.createCopyName_ = function(
170 dirEntry, newMimeType, callback) {
171 var name = this.getFileName();
173 // If the item represents a file created during the current Gallery session
174 // we reuse it for subsequent saves instead of creating multiple copies.
175 if (!this.original_) {
180 var baseName = name.replace(/\.[^\.\/]+$/, '');
181 var ext = newMimeType === 'image/jpeg' ? '.jpg' : '.png';
183 function tryNext(tries) {
184 // All the names are used. Let's overwrite the last one.
186 setTimeout(callback, 0, baseName + ext);
190 // If the file name contains the copy signature add/advance the sequential
192 var matchN = Gallery.Item.REGEXP_COPY_N.exec(baseName);
193 var match0 = Gallery.Item.REGEXP_COPY_0.exec(baseName);
194 if (matchN && matchN[1] && matchN[2]) {
195 var copyNumber = parseInt(matchN[2], 10) + 1;
196 baseName = matchN[1] + Gallery.Item.COPY_SIGNATURE +
197 ' (' + copyNumber + ')';
198 } else if (match0 && match0[1]) {
199 baseName = match0[1] + Gallery.Item.COPY_SIGNATURE + ' (1)';
201 baseName += Gallery.Item.COPY_SIGNATURE;
204 dirEntry.getFile(baseName + ext, {create: false, exclusive: false},
205 tryNext.bind(null, tries - 1),
206 callback.bind(null, baseName + ext));
213 * Writes the new item content to either the existing or a new file.
215 * @param {!VolumeManager} volumeManager Volume manager instance.
216 * @param {!MetadataModel} metadataModel
217 * @param {DirectoryEntry} fallbackDir Fallback directory in case the current
218 * directory is read only.
219 * @param {boolean} overwrite Whether to overwrite the image to the item or not.
220 * @param {!HTMLCanvasElement} canvas Source canvas.
221 * @param {function(boolean)} callback Callback accepting true for success.
223 Gallery.Item.prototype.saveToFile = function(
224 volumeManager, metadataModel, fallbackDir, overwrite, canvas, callback) {
225 ImageUtil.metrics.startInterval(ImageUtil.getMetricName('SaveTime'));
227 var name = this.getFileName();
228 var newMimeType = name.match(/\.jpe?g$/i) || FileType.isRaw(this.entry_) ?
229 'image/jpeg' : 'image/png';
231 var onSuccess = function(entry) {
232 var locationInfo = volumeManager.getLocationInfo(entry);
234 // Reuse old location info if it fails to obtain location info.
235 locationInfo = this.locationInfo_;
237 ImageUtil.metrics.recordEnum(ImageUtil.getMetricName('SaveResult'), 1, 2);
238 ImageUtil.metrics.recordInterval(ImageUtil.getMetricName('SaveTime'));
241 this.locationInfo_ = locationInfo;
243 // Updates the metadata.
244 metadataModel.notifyEntriesChanged([this.entry_]);
246 metadataModel.get([entry], Gallery.PREFETCH_PROPERTY_NAMES),
247 new ThumbnailModel(metadataModel).get([entry])
248 ]).then(function(metadataLists) {
249 this.metadataItem_ = metadataLists[0][0];
250 this.thumbnailMetadataItem_ = metadataLists[1][0];
252 }.bind(this), function() {
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) {
270 ['mediaMimeType', 'contentMimeType', 'ifd', 'exifLittleEndian']
271 ).then(function(metadataItems) {
272 // Create the blob of new image.
273 var metadataItem = metadataItems[0];
274 metadataItem.modificationTime = new Date();
275 metadataItem.mediaMimeType = newMimeType;
276 var metadataEncoder = ImageEncoder.encodeMetadata(
277 metadataItem, canvas, /* quality for thumbnail*/ 0.8);
278 // Contrary to what one might think 1.0 is not a good default. Opening
279 // and saving an typical photo taken with consumer camera increases
280 // its file size by 50-100%. Experiments show that 0.9 is much better.
281 // It shrinks some photos a bit, keeps others about the same size, but
282 // does not visibly lower the quality.
283 blob = ImageEncoder.getBlob(canvas, metadataEncoder, 0.9);
284 }.bind(this)).then(function() {
286 return new Promise(function(fullfill, reject) {
287 fileEntry.createWriter(fullfill, reject);
289 }).then(function(writer) {
292 // Truncates the file to 0 byte if it overwrites.
293 return new Promise(function(fulfill, reject) {
295 fileWriter.onerror = reject;
296 fileWriter.onwriteend = fulfill;
297 fileWriter.truncate(0);
303 // Writes the blob of new image.
304 return new Promise(function(fulfill, reject) {
305 fileWriter.onerror = reject;
306 fileWriter.onwriteend = fulfill;
307 fileWriter.write(blob);
309 }).then(onSuccess.bind(null, fileEntry))
310 .catch(function(error) {
313 // Disable all callbacks on the first error.
314 fileWriter.onerror = null;
315 fileWriter.onwriteend = null;
320 var getFile = function(dir, newFile) {
321 dir.getFile(name, {create: newFile, exclusive: newFile},
322 function(fileEntry) {
323 doSave(newFile, fileEntry);
324 }.bind(this), onError);
327 var checkExistence = function(dir) {
328 dir.getFile(name, {create: false, exclusive: false},
329 getFile.bind(null, dir, false /* existing file */),
330 getFile.bind(null, dir, true /* create new file */));
333 var saveToDir = function(dir) {
335 !this.locationInfo_.isReadOnly &&
336 !FileType.isRaw(this.entry_)) {
339 this.createCopyName_(dir, newMimeType, function(copyName) {
340 this.original_ = false;
347 if (this.locationInfo_.isReadOnly) {
348 saveToDir(fallbackDir);
350 this.entry_.getParent(saveToDir, onError);
357 * @param {string} displayName New display name (without the extension).
358 * @return {!Promise} Promise fulfilled with when renaming completes, or
359 * rejected with the error message.
361 Gallery.Item.prototype.rename = function(displayName) {
362 var newFileName = this.entry_.name.replace(
363 ImageUtil.getDisplayNameFromName(this.entry_.name), displayName);
365 if (newFileName === this.entry_.name)
366 return Promise.reject('NOT_CHANGED');
368 if (/^\s*$/.test(displayName))
369 return Promise.reject(str('ERROR_WHITESPACE_NAME'));
371 var parentDirectoryPromise = new Promise(
372 this.entry_.getParent.bind(this.entry_));
373 return parentDirectoryPromise.then(function(parentDirectory) {
374 var nameValidatingPromise =
375 util.validateFileName(parentDirectory, newFileName, true);
376 return nameValidatingPromise.then(function() {
377 var existingFilePromise = new Promise(parentDirectory.getFile.bind(
378 parentDirectory, newFileName, {create: false, exclusive: false}));
379 return existingFilePromise.then(function() {
380 return Promise.reject(str('GALLERY_FILE_EXISTS'));
383 this.entry_.moveTo.bind(this.entry_, parentDirectory, newFileName));
386 }.bind(this)).then(function(entry) {