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 * Sets an item as original.
126 Gallery
.Item
.prototype.setAsOriginal = function() {
127 this.original_
= true;
131 * Obtains the last accessed date.
132 * @return {number} Last accessed date.
134 Gallery
.Item
.prototype.getLastAccessedDate = function() {
135 return this.lastAccessed_
;
139 * Updates the last accessed date.
141 Gallery
.Item
.prototype.touch = function() {
142 this.lastAccessed_
= Date
.now();
147 * @type {string} Suffix for a edited copy file name.
150 Gallery
.Item
.COPY_SIGNATURE
= ' - Edited';
153 * Regular expression to match '... - Edited'.
157 Gallery
.Item
.REGEXP_COPY_0
=
158 new RegExp('^(.+)' + Gallery
.Item
.COPY_SIGNATURE
+ '$');
161 * Regular expression to match '... - Edited (N)'.
165 Gallery
.Item
.REGEXP_COPY_N
=
166 new RegExp('^(.+)' + Gallery
.Item
.COPY_SIGNATURE
+ ' \\((\\d+)\\)$');
169 * Creates a name for an edited copy of the file.
171 * @param {!DirectoryEntry} dirEntry Entry.
172 * @param {string} newMimeType Mime type of new image.
173 * @param {function(string)} callback Callback.
176 Gallery
.Item
.prototype.createCopyName_ = function(
177 dirEntry
, newMimeType
, callback
) {
178 var name
= this.getFileName();
180 // If the item represents a file created during the current Gallery session
181 // we reuse it for subsequent saves instead of creating multiple copies.
182 if (!this.original_
) {
187 var baseName
= name
.replace(/\.[^\.\/]+$/, '');
188 var ext
= newMimeType
=== 'image/jpeg' ? '.jpg' : '.png';
190 function tryNext(tries
) {
191 // All the names are used. Let's overwrite the last one.
193 setTimeout(callback
, 0, baseName
+ ext
);
197 // If the file name contains the copy signature add/advance the sequential
199 var matchN
= Gallery
.Item
.REGEXP_COPY_N
.exec(baseName
);
200 var match0
= Gallery
.Item
.REGEXP_COPY_0
.exec(baseName
);
201 if (matchN
&& matchN
[1] && matchN
[2]) {
202 var copyNumber
= parseInt(matchN
[2], 10) + 1;
203 baseName
= matchN
[1] + Gallery
.Item
.COPY_SIGNATURE
+
204 ' (' + copyNumber
+ ')';
205 } else if (match0
&& match0
[1]) {
206 baseName
= match0
[1] + Gallery
.Item
.COPY_SIGNATURE
+ ' (1)';
208 baseName
+= Gallery
.Item
.COPY_SIGNATURE
;
211 dirEntry
.getFile(baseName
+ ext
, {create
: false, exclusive
: false},
212 tryNext
.bind(null, tries
- 1),
213 callback
.bind(null, baseName
+ ext
));
220 * Returns true if the original format is writable format of Gallery.
221 * @return {boolean} True if the original format is writable format.
223 Gallery
.Item
.prototype.isWritableFormat = function() {
224 var type
= FileType
.getType(this.entry_
);
225 return type
.type
=== 'image' &&
226 (type
.subtype
=== 'JPEG' || type
.subtype
=== 'PNG')
230 * Returns true if the entry of item is writable.
231 * @param {!VolumeManagerWrapper} volumeManager Volume manager.
232 * @return {boolean} True if the entry of item is writable.
234 Gallery
.Item
.prototype.isWritableFile = function(volumeManager
) {
235 return this.isWritableFormat() &&
236 !this.locationInfo_
.isReadOnly
&&
237 !GalleryUtil
.isOnMTPVolume(this.entry_
, volumeManager
);
241 * Returns mime type for saving an edit of this item.
242 * @return {string} Mime type.
245 Gallery
.Item
.prototype.getNewMimeType_ = function() {
246 return this.getFileName().match(/\.jpe?g$/i) || FileType
.isRaw(this.entry_
) ?
247 'image/jpeg' : 'image/png';
251 * Return copy name of this item.
252 * @param {!DirectoryEntry} dirEntry Parent directory entry of copied item.
253 * @return {!Promise<string>} A promise which will be fulfilled with copy name.
255 Gallery
.Item
.prototype.getCopyName = function(dirEntry
) {
256 return new Promise(this.createCopyName_
.bind(
257 this, dirEntry
, this.getNewMimeType_()));
261 * Writes the new item content to either the existing or a new file.
263 * @param {!VolumeManagerWrapper} volumeManager Volume manager instance.
264 * @param {!MetadataModel} metadataModel
265 * @param {DirectoryEntry} fallbackDir Fallback directory in case the current
266 * directory is read only.
267 * @param {!HTMLCanvasElement} canvas Source canvas.
268 * @param {boolean} overwrite Set true to overwrite original if it's possible.
269 * @param {function(boolean)} callback Callback accepting true for success.
271 Gallery
.Item
.prototype.saveToFile = function(
272 volumeManager
, metadataModel
, fallbackDir
, canvas
, overwrite
, callback
) {
273 ImageUtil
.metrics
.startInterval(ImageUtil
.getMetricName('SaveTime'));
275 var name
= this.getFileName();
276 var newMimeType
= this.getNewMimeType_();
278 var onSuccess = function(entry
) {
279 var locationInfo
= volumeManager
.getLocationInfo(entry
);
281 // Reuse old location info if it fails to obtain location info.
282 locationInfo
= this.locationInfo_
;
284 ImageUtil
.metrics
.recordEnum(ImageUtil
.getMetricName('SaveResult'), 1, 2);
285 ImageUtil
.metrics
.recordInterval(ImageUtil
.getMetricName('SaveTime'));
288 this.locationInfo_
= locationInfo
;
290 // Updates the metadata.
291 metadataModel
.notifyEntriesChanged([this.entry_
]);
293 metadataModel
.get([entry
], Gallery
.PREFETCH_PROPERTY_NAMES
),
294 new ThumbnailModel(metadataModel
).get([entry
])
295 ]).then(function(metadataLists
) {
296 this.metadataItem_
= metadataLists
[0][0];
297 this.thumbnailMetadataItem_
= metadataLists
[1][0];
299 }.bind(this), function() {
304 var onError = function(error
) {
305 console
.error('Error saving from gallery', name
, error
);
306 ImageUtil
.metrics
.recordEnum(ImageUtil
.getMetricName('SaveResult'), 0, 2);
311 var doSave = function(newFile
, fileEntry
) {
317 ['mediaMimeType', 'contentMimeType', 'ifd', 'exifLittleEndian']
318 ).then(function(metadataItems
) {
319 // Create the blob of new image.
320 var metadataItem
= metadataItems
[0];
321 metadataItem
.modificationTime
= new Date();
322 metadataItem
.mediaMimeType
= newMimeType
;
323 var metadataEncoder
= ImageEncoder
.encodeMetadata(
324 metadataItem
, canvas
, /* quality for thumbnail*/ 0.8);
325 // Contrary to what one might think 1.0 is not a good default. Opening
326 // and saving an typical photo taken with consumer camera increases
327 // its file size by 50-100%. Experiments show that 0.9 is much better.
328 // It shrinks some photos a bit, keeps others about the same size, but
329 // does not visibly lower the quality.
330 blob
= ImageEncoder
.getBlob(canvas
, metadataEncoder
, 0.9);
331 }.bind(this)).then(function() {
333 return new Promise(function(fullfill
, reject
) {
334 fileEntry
.createWriter(fullfill
, reject
);
336 }).then(function(writer
) {
339 // Truncates the file to 0 byte if it overwrites.
340 return new Promise(function(fulfill
, reject
) {
342 fileWriter
.onerror
= reject
;
343 fileWriter
.onwriteend
= fulfill
;
344 fileWriter
.truncate(0);
350 // Writes the blob of new image.
351 return new Promise(function(fulfill
, reject
) {
352 fileWriter
.onerror
= reject
;
353 fileWriter
.onwriteend
= fulfill
;
354 fileWriter
.write(blob
);
356 }).then(onSuccess
.bind(null, fileEntry
))
357 .catch(function(error
) {
360 // Disable all callbacks on the first error.
361 fileWriter
.onerror
= null;
362 fileWriter
.onwriteend
= null;
367 var getFile = function(dir
, newFile
) {
368 dir
.getFile(name
, {create
: newFile
, exclusive
: newFile
},
369 function(fileEntry
) {
370 doSave(newFile
, fileEntry
);
371 }.bind(this), onError
);
374 var checkExistence = function(dir
) {
375 dir
.getFile(name
, {create
: false, exclusive
: false},
376 getFile
.bind(null, dir
, false /* existing file */),
377 getFile
.bind(null, dir
, true /* create new file */));
380 var saveToDir = function(dir
) {
382 !this.locationInfo_
.isReadOnly
&&
383 this.isWritableFormat()) {
388 this.createCopyName_(dir
, newMimeType
, function(copyName
) {
389 this.original_
= false;
395 // Since in-place editing is not supported on MTP volume, Gallery.app handles
396 // MTP volume as read only volume.
397 if (this.locationInfo_
.isReadOnly
||
398 GalleryUtil
.isOnMTPVolume(this.entry_
, volumeManager
)) {
399 saveToDir(fallbackDir
);
401 this.entry_
.getParent(saveToDir
, onError
);
408 * @param {string} displayName New display name (without the extension).
409 * @return {!Promise} Promise fulfilled with when renaming completes, or
410 * rejected with the error message.
412 Gallery
.Item
.prototype.rename = function(displayName
) {
413 var newFileName
= this.entry_
.name
.replace(
414 ImageUtil
.getDisplayNameFromName(this.entry_
.name
), displayName
);
416 if (newFileName
=== this.entry_
.name
)
417 return Promise
.reject('NOT_CHANGED');
419 if (/^\s*$/.test(displayName
))
420 return Promise
.reject(str('ERROR_WHITESPACE_NAME'));
422 var parentDirectoryPromise
= new Promise(
423 this.entry_
.getParent
.bind(this.entry_
));
424 return parentDirectoryPromise
.then(function(parentDirectory
) {
425 var nameValidatingPromise
=
426 util
.validateFileName(parentDirectory
, newFileName
, true);
427 return nameValidatingPromise
.then(function() {
428 var existingFilePromise
= new Promise(parentDirectory
.getFile
.bind(
429 parentDirectory
, newFileName
, {create
: false, exclusive
: false}));
430 return existingFilePromise
.then(function() {
431 return Promise
.reject(str('GALLERY_FILE_EXISTS'));
434 this.entry_
.moveTo
.bind(this.entry_
, parentDirectory
, newFileName
));
437 }.bind(this)).then(function(entry
) {