OLD | NEW |
1 // Copyright 2014 The Chromium Authors. All rights reserved. | 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 | 2 // Use of this source code is governed by a BSD-style license that can be |
3 // found in the LICENSE file. | 3 // found in the LICENSE file. |
4 | 4 |
5 /** | 5 /** |
6 * Object representing an image item (a photo). | 6 * Object representing an image item (a photo). |
7 * | 7 * |
8 * @param {!FileEntry} entry Image entry. | 8 * @param {!FileEntry} entry Image entry. |
9 * @param {!EntryLocation} locationInfo Entry location information. | 9 * @param {!EntryLocation} locationInfo Entry location information. |
10 * @param {MetadataItem} metadataItem | 10 * @param {MetadataItem} metadataItem |
(...skipping 205 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
216 tryNext(10); | 216 tryNext(10); |
217 }; | 217 }; |
218 | 218 |
219 /** | 219 /** |
220 * Returns true if the original format is writable format of Gallery. | 220 * Returns true if the original format is writable format of Gallery. |
221 * @return {boolean} True if the original format is writable format. | 221 * @return {boolean} True if the original format is writable format. |
222 */ | 222 */ |
223 Gallery.Item.prototype.isWritableFormat = function() { | 223 Gallery.Item.prototype.isWritableFormat = function() { |
224 var type = FileType.getType(this.entry_); | 224 var type = FileType.getType(this.entry_); |
225 return type.type === 'image' && | 225 return type.type === 'image' && |
226 (type.subtype === 'JPEG' || type.subtype === 'PNG') | 226 (type.subtype === 'JPEG' || type.subtype === 'PNG'); |
227 }; | 227 }; |
228 | 228 |
229 /** | 229 /** |
230 * Returns true if the entry of item is writable. | 230 * Returns true if the entry of item is writable. |
231 * @param {!VolumeManagerWrapper} volumeManager Volume manager. | 231 * @param {!VolumeManagerWrapper} volumeManager Volume manager. |
232 * @return {boolean} True if the entry of item is writable. | 232 * @return {boolean} True if the entry of item is writable. |
233 */ | 233 */ |
234 Gallery.Item.prototype.isWritableFile = function(volumeManager) { | 234 Gallery.Item.prototype.isWritableFile = function(volumeManager) { |
235 return this.isWritableFormat() && | 235 return this.isWritableFormat() && |
236 !this.locationInfo_.isReadOnly && | 236 !this.locationInfo_.isReadOnly && |
(...skipping 18 matching lines...) Expand all Loading... |
255 Gallery.Item.prototype.getCopyName = function(dirEntry) { | 255 Gallery.Item.prototype.getCopyName = function(dirEntry) { |
256 return new Promise(this.createCopyName_.bind( | 256 return new Promise(this.createCopyName_.bind( |
257 this, dirEntry, this.getNewMimeType_())); | 257 this, dirEntry, this.getNewMimeType_())); |
258 }; | 258 }; |
259 | 259 |
260 /** | 260 /** |
261 * Writes the new item content to either the existing or a new file. | 261 * Writes the new item content to either the existing or a new file. |
262 * | 262 * |
263 * @param {!VolumeManagerWrapper} volumeManager Volume manager instance. | 263 * @param {!VolumeManagerWrapper} volumeManager Volume manager instance. |
264 * @param {!MetadataModel} metadataModel | 264 * @param {!MetadataModel} metadataModel |
265 * @param {DirectoryEntry} fallbackDir Fallback directory in case the current | 265 * @param {!DirectoryEntry} fallbackDir Fallback directory in case the current |
266 * directory is read only. | 266 * directory is read only. |
267 * @param {!HTMLCanvasElement} canvas Source canvas. | 267 * @param {!HTMLCanvasElement} canvas Source canvas. |
268 * @param {boolean} overwrite Set true to overwrite original if it's possible. | 268 * @param {boolean} overwrite Set true to overwrite original if it's possible. |
269 * @param {function(boolean)} callback Callback accepting true for success. | 269 * @param {function(boolean)} callback Callback accepting true for success. |
270 */ | 270 */ |
271 Gallery.Item.prototype.saveToFile = function( | 271 Gallery.Item.prototype.saveToFile = function( |
272 volumeManager, metadataModel, fallbackDir, canvas, overwrite, callback) { | 272 volumeManager, metadataModel, fallbackDir, canvas, overwrite, callback) { |
273 ImageUtil.metrics.startInterval(ImageUtil.getMetricName('SaveTime')); | 273 ImageUtil.metrics.startInterval(ImageUtil.getMetricName('SaveTime')); |
| 274 var saveResultRecorded = false; |
274 | 275 |
275 var name = this.getFileName(); | 276 Promise.all([this.getEntryToWrite_(overwrite, fallbackDir, volumeManager), |
276 var newMimeType = this.getNewMimeType_(); | 277 this.getBlobForSave_(canvas, metadataModel)]).then(function(results) { |
| 278 // Write content to the entry. |
| 279 var fileEntry = results[0]; |
| 280 var blob = results[1]; |
277 | 281 |
278 var onSuccess = function(entry) { | 282 // Create writer. |
279 var locationInfo = volumeManager.getLocationInfo(entry); | 283 return new Promise(function(resolve, reject) { |
280 if (!locationInfo) { | 284 fileEntry.createWriter(resolve, reject); |
281 // Reuse old location info if it fails to obtain location info. | 285 }).then(function(fileWriter) { |
282 locationInfo = this.locationInfo_; | 286 // Truncates the file to 0 byte if it overwrites existing file. |
283 } | 287 return new Promise(function(resolve, reject) { |
284 ImageUtil.metrics.recordEnum(ImageUtil.getMetricName('SaveResult'), 1, 2); | 288 if (util.isSameEntry(fileEntry, this.entry_)) { |
285 ImageUtil.metrics.recordInterval(ImageUtil.getMetricName('SaveTime')); | |
286 | |
287 this.entry_ = entry; | |
288 this.locationInfo_ = locationInfo; | |
289 | |
290 // Updates the metadata. | |
291 metadataModel.notifyEntriesChanged([this.entry_]); | |
292 Promise.all([ | |
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]; | |
298 callback(true); | |
299 }.bind(this), function() { | |
300 callback(false); | |
301 }); | |
302 }.bind(this); | |
303 | |
304 var onError = function(error) { | |
305 console.error('Error saving from gallery', name, error); | |
306 ImageUtil.metrics.recordEnum(ImageUtil.getMetricName('SaveResult'), 0, 2); | |
307 if (callback) | |
308 callback(false); | |
309 }; | |
310 | |
311 var doSave = function(newFile, fileEntry) { | |
312 var blob; | |
313 var fileWriter; | |
314 | |
315 metadataModel.get( | |
316 [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() { | |
332 // Create writer. | |
333 return new Promise(function(fullfill, reject) { | |
334 fileEntry.createWriter(fullfill, reject); | |
335 }); | |
336 }).then(function(writer) { | |
337 fileWriter = writer; | |
338 | |
339 // Truncates the file to 0 byte if it overwrites. | |
340 return new Promise(function(fulfill, reject) { | |
341 if (!newFile) { | |
342 fileWriter.onerror = reject; | 289 fileWriter.onerror = reject; |
343 fileWriter.onwriteend = fulfill; | 290 fileWriter.onwriteend = resolve; |
344 fileWriter.truncate(0); | 291 fileWriter.truncate(0); |
345 } else { | 292 } else { |
346 fulfill(null); | 293 resolve(null); |
347 } | 294 } |
348 }); | 295 }.bind(this)).then(function() { |
349 }).then(function() { | 296 // Writes the blob of new image. |
350 // Writes the blob of new image. | 297 return new Promise(function(resolve, reject) { |
351 return new Promise(function(fulfill, reject) { | 298 fileWriter.onerror = reject; |
352 fileWriter.onerror = reject; | 299 fileWriter.onwriteend = resolve; |
353 fileWriter.onwriteend = fulfill; | 300 fileWriter.write(blob); |
354 fileWriter.write(blob); | 301 }); |
355 }); | 302 }).catch(function(error) { |
356 }).then(onSuccess.bind(null, fileEntry)) | |
357 .catch(function(error) { | |
358 onError(error); | |
359 if (fileWriter) { | |
360 // Disable all callbacks on the first error. | 303 // Disable all callbacks on the first error. |
361 fileWriter.onerror = null; | 304 fileWriter.onerror = null; |
362 fileWriter.onwriteend = null; | 305 fileWriter.onwriteend = null; |
| 306 |
| 307 return Promise.reject(error); |
| 308 }); |
| 309 }.bind(this)).then(function() { |
| 310 var locationInfo = volumeManager.getLocationInfo(fileEntry); |
| 311 if (!locationInfo) { |
| 312 // Reuse old location info if it fails to obtain location info. |
| 313 locationInfo = this.locationInfo_; |
363 } | 314 } |
364 }); | |
365 }.bind(this); | |
366 | 315 |
367 var getFile = function(dir, newFile) { | 316 ImageUtil.metrics.recordEnum(ImageUtil.getMetricName('SaveResult'), 1, 2); |
368 dir.getFile(name, {create: newFile, exclusive: newFile}, | 317 saveResultRecorded = true; |
369 function(fileEntry) { | 318 ImageUtil.metrics.recordInterval(ImageUtil.getMetricName('SaveTime')); |
370 doSave(newFile, fileEntry); | |
371 }.bind(this), onError); | |
372 }.bind(this); | |
373 | 319 |
374 var checkExistence = function(dir) { | 320 this.entry_ = fileEntry; |
375 dir.getFile(name, {create: false, exclusive: false}, | 321 this.locationInfo_ = locationInfo; |
376 getFile.bind(null, dir, false /* existing file */), | |
377 getFile.bind(null, dir, true /* create new file */)); | |
378 }; | |
379 | 322 |
380 var saveToDir = function(dir) { | 323 // Updates the metadata. |
381 if (overwrite && | 324 metadataModel.notifyEntriesChanged([this.entry_]); |
382 !this.locationInfo_.isReadOnly && | 325 Promise.all([ |
383 this.isWritableFormat()) { | 326 metadataModel.get([this.entry_], Gallery.PREFETCH_PROPERTY_NAMES), |
384 checkExistence(dir); | 327 new ThumbnailModel(metadataModel).get([this.entry_]) |
385 return; | 328 ]).then(function(metadataLists) { |
386 } | 329 this.metadataItem_ = metadataLists[0][0]; |
| 330 this.thumbnailMetadataItem_ = metadataLists[1][0]; |
| 331 callback(true); |
| 332 }.bind(this), function() { |
| 333 callback(false); |
| 334 }); |
| 335 }.bind(this)); |
| 336 }.bind(this)).catch(function(error) { |
| 337 console.error('Error saving from gallery', this.entry_.name, error); |
387 | 338 |
388 this.createCopyName_(dir, newMimeType, function(copyName) { | 339 if (!saveResultRecorded) |
389 this.original_ = false; | 340 ImageUtil.metrics.recordEnum(ImageUtil.getMetricName('SaveResult'), 0, 2); |
390 name = copyName; | |
391 checkExistence(dir); | |
392 }.bind(this)); | |
393 }.bind(this); | |
394 | 341 |
395 // Since in-place editing is not supported on MTP volume, Gallery.app handles | 342 callback(false); |
396 // MTP volume as read only volume. | 343 }.bind(this)); |
397 if (this.locationInfo_.isReadOnly || | |
398 GalleryUtil.isOnMTPVolume(this.entry_, volumeManager)) { | |
399 saveToDir(fallbackDir); | |
400 } else { | |
401 this.entry_.getParent(saveToDir, onError); | |
402 } | |
403 }; | 344 }; |
404 | 345 |
405 /** | 346 /** |
| 347 * Returns file entry to write. |
| 348 * @param {boolean} overwrite True to overwrite original file. |
| 349 * @param {!DirectoryEntry} fallbackDirectory Directory to fallback if current |
| 350 * directory is not writable. |
| 351 * @param {!VolumeManagerWrapper} volumeManager |
| 352 * @return {!Promise<!FileEntry>} |
| 353 * @private |
| 354 */ |
| 355 Gallery.Item.prototype.getEntryToWrite_ = function( |
| 356 overwrite, fallbackDirectory, volumeManager) { |
| 357 return new Promise(function(resolve, reject) { |
| 358 // Since in-place editing is not supported on MTP volume, Gallery.app |
| 359 // handles MTP volume as read only volume. |
| 360 if (this.locationInfo_.isReadOnly || |
| 361 GalleryUtil.isOnMTPVolume(this.entry_, volumeManager)) { |
| 362 resolve(fallbackDirectory); |
| 363 } else { |
| 364 this.entry_.getParent(resolve, reject); |
| 365 } |
| 366 }.bind(this)).then(function(directory) { |
| 367 return new Promise(function(resolve) { |
| 368 // Find file name. |
| 369 if (overwrite && |
| 370 !this.locationInfo_.isReadOnly && |
| 371 this.isWritableFormat()) { |
| 372 resolve(this.getFileName()); |
| 373 return; |
| 374 } |
| 375 |
| 376 this.createCopyName_( |
| 377 directory, this.getNewMimeType_(), function(copyName) { |
| 378 this.original_ = false; |
| 379 resolve(copyName); |
| 380 }.bind(this)); |
| 381 }.bind(this)).then(function(name) { |
| 382 // Get File entry and return. |
| 383 return new Promise(directory.getFile.bind( |
| 384 directory, name, { create: true, exclusive: false })); |
| 385 }); |
| 386 }.bind(this)); |
| 387 }; |
| 388 |
| 389 /** |
| 390 * Returns blob to be saved. |
| 391 * @param {!HTMLCanvasElement} canvas |
| 392 * @param {!MetadataModel} metadataModel |
| 393 * @return {!Promise<!Blob>} |
| 394 * @private |
| 395 */ |
| 396 Gallery.Item.prototype.getBlobForSave_ = function(canvas, metadataModel) { |
| 397 return metadataModel.get( |
| 398 [this.entry_], |
| 399 ['mediaMimeType', 'contentMimeType', 'ifd', 'exifLittleEndian'] |
| 400 ).then(function(metadataItems) { |
| 401 // Create the blob of new image. |
| 402 var metadataItem = metadataItems[0]; |
| 403 metadataItem.modificationTime = new Date(); |
| 404 metadataItem.mediaMimeType = this.getNewMimeType_(); |
| 405 var metadataEncoder = ImageEncoder.encodeMetadata( |
| 406 metadataItem, canvas, /* quality for thumbnail*/ 0.8); |
| 407 // Contrary to what one might think 1.0 is not a good default. Opening |
| 408 // and saving an typical photo taken with consumer camera increases |
| 409 // its file size by 50-100%. Experiments show that 0.9 is much better. |
| 410 // It shrinks some photos a bit, keeps others about the same size, but |
| 411 // does not visibly lower the quality. |
| 412 return ImageEncoder.getBlob(canvas, metadataEncoder, 0.9); |
| 413 }.bind(this)); |
| 414 }; |
| 415 |
| 416 /** |
406 * Renames the item. | 417 * Renames the item. |
407 * | 418 * |
408 * @param {string} displayName New display name (without the extension). | 419 * @param {string} displayName New display name (without the extension). |
409 * @return {!Promise} Promise fulfilled with when renaming completes, or | 420 * @return {!Promise} Promise fulfilled with when renaming completes, or |
410 * rejected with the error message. | 421 * rejected with the error message. |
411 */ | 422 */ |
412 Gallery.Item.prototype.rename = function(displayName) { | 423 Gallery.Item.prototype.rename = function(displayName) { |
413 var newFileName = this.entry_.name.replace( | 424 var newFileName = this.entry_.name.replace( |
414 ImageUtil.getDisplayNameFromName(this.entry_.name), displayName); | 425 ImageUtil.getDisplayNameFromName(this.entry_.name), displayName); |
415 | 426 |
(...skipping 15 matching lines...) Expand all Loading... |
431 return Promise.reject(str('GALLERY_FILE_EXISTS')); | 442 return Promise.reject(str('GALLERY_FILE_EXISTS')); |
432 }, function() { | 443 }, function() { |
433 return new Promise( | 444 return new Promise( |
434 this.entry_.moveTo.bind(this.entry_, parentDirectory, newFileName)); | 445 this.entry_.moveTo.bind(this.entry_, parentDirectory, newFileName)); |
435 }.bind(this)); | 446 }.bind(this)); |
436 }.bind(this)); | 447 }.bind(this)); |
437 }.bind(this)).then(function(entry) { | 448 }.bind(this)).then(function(entry) { |
438 this.entry_ = entry; | 449 this.entry_ = entry; |
439 }.bind(this)); | 450 }.bind(this)); |
440 }; | 451 }; |
OLD | NEW |