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 {!Object} metadata Metadata for the entry.
11 * @param {!MetadataCache} metadataCache Metadata cache instance.
12 * @param {boolean} original Whether the entry is original or edited.
16 Gallery
.Item = function(
17 entry
, locationInfo
, metadata
, metadataCache
, original
) {
25 * @type {!EntryLocation}
28 this.locationInfo_
= locationInfo
;
34 this.metadata_
= Object
.preventExtensions(metadata
);
37 * @type {!MetadataCache}
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).
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;
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
58 * @type {HTMLCanvasElement}
60 this.screenImage
= null;
63 * Last accessed date to be used for selecting items whose cache are evicted.
67 this.lastAccessed_
= Date
.now();
73 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.');
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();
143 * @type {string} Suffix for a edited copy file name.
146 Gallery
.Item
.COPY_SIGNATURE
= ' - Edited';
149 * Regular expression to match '... - Edited'.
153 Gallery
.Item
.REGEXP_COPY_0
=
154 new RegExp('^(.+)' + Gallery
.Item
.COPY_SIGNATURE
+ '$');
157 * Regular expression to match '... - Edited (N)'.
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.
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_
) {
182 var index
= name
.lastIndexOf('.');
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.
194 function tryNext(tries
) {
195 // All the names are used. Let's overwrite the last one.
197 setTimeout(callback
, 0, name
+ ext
);
201 // If the file name contains the copy signature add/advance the sequential
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)';
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
));
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
);
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'));
248 this.locationInfo_
= locationInfo
;
250 // Updates the metadata.
251 this.metadataCache_
.clear([this.entry_
], '*');
252 this.metadataCache_
.getLatest(
254 Gallery
.METADATA_TYPE
,
255 function(metadataList
) {
256 if (metadataList
.length
=== 1) {
257 this.metadata_
= metadataList
[0];
267 var onError = function(error
) {
268 console
.error('Error saving from gallery', name
, error
);
269 ImageUtil
.metrics
.recordEnum(ImageUtil
.getMetricName('SaveResult'), 0, 2);
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
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));
289 fileWriter
.onerror = function(error
) {
291 // Disable all callbacks on the first error.
292 fileWriter
.onerror
= null;
293 fileWriter
.onwriteend
= null;
298 fileWriter
.onwriteend
= writeContent
;
299 fileWriter
.truncate(0);
301 }.bind(this), onError
);
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
);
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
) {
321 this.createCopyName_(dir
, function(copyName
) {
322 this.original_
= false;
329 if (this.locationInfo_
.isReadOnly
) {
330 saveToDir(fallbackDir
);
332 this.entry_
.getParent(saveToDir
, onError
);
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'));
365 this.entry_
.moveTo
.bind(this.entry_
, parentDirectory
, newFileName
));
368 }.bind(this)).then(function(entry
) {