Chromium Code Reviews| 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], | |
|
yawano
2015/09/24 05:19:50
The cause of this issue is this line. When an edit
| |
| 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 if (callback) |
| 396 // MTP volume as read only volume. | 343 callback(false); |
|
hirono
2015/09/24 08:42:50
nit: It looks callback should be non-null.
yawano
2015/09/24 10:22:57
Done.
| |
| 397 if (this.locationInfo_.isReadOnly || | 344 }.bind(this)); |
| 398 GalleryUtil.isOnMTPVolume(this.entry_, volumeManager)) { | |
| 399 saveToDir(fallbackDir); | |
| 400 } else { | |
| 401 this.entry_.getParent(saveToDir, onError); | |
| 402 } | |
| 403 }; | 345 }; |
| 404 | 346 |
| 405 /** | 347 /** |
| 348 * Returns file entry to write. | |
| 349 * @param {boolean} overwrite True to overwrite original file. | |
| 350 * @param {!DirectoryEntry} fallbackDirectory Directory to fallback if current | |
| 351 * directory is not writable. | |
| 352 * @param {!VolumeManagerWrapper} volumeManager | |
| 353 * @return {!Promise<!FileEntry>} | |
| 354 * @private | |
| 355 */ | |
| 356 Gallery.Item.prototype.getEntryToWrite_ = function( | |
| 357 overwrite, fallbackDirectory, volumeManager) { | |
| 358 return new Promise(function(resolve, reject) { | |
| 359 // Since in-place editing is not supported on MTP volume, Gallery.app | |
| 360 // handles MTP volume as read only volume. | |
| 361 if (this.locationInfo_.isReadOnly || | |
| 362 GalleryUtil.isOnMTPVolume(this.entry_, volumeManager)) { | |
| 363 resolve(fallbackDirectory); | |
| 364 } else { | |
| 365 this.entry_.getParent(resolve, reject); | |
| 366 } | |
| 367 }.bind(this)).then(function(directory) { | |
| 368 return new Promise(function(resolve) { | |
| 369 // Find file name. | |
| 370 if (overwrite && | |
| 371 !this.locationInfo_.isReadOnly && | |
| 372 this.isWritableFormat()) { | |
| 373 resolve(this.getFileName()); | |
| 374 return; | |
| 375 } | |
| 376 | |
| 377 this.createCopyName_( | |
| 378 directory, this.getNewMimeType_(), function(copyName) { | |
| 379 this.original_ = false; | |
| 380 resolve(copyName); | |
| 381 }.bind(this)); | |
| 382 }.bind(this)).then(function(name) { | |
| 383 // Check existence of target file. | |
| 384 return new Promise(function(resolve) { | |
| 385 directory.getFile(name, {create: false, exclusive: false}, | |
|
hirono
2015/09/24 08:42:50
Could not we specify {create: true, exclusive: fal
yawano
2015/09/24 10:22:57
Yes, we don't need to check existence of the entry
| |
| 386 resolve.bind(null, true /* existing */), | |
| 387 resolve.bind(null, false /* not existing */)); | |
| 388 }).then(function(exist) { | |
| 389 // Get File entry and return. | |
| 390 return new Promise(directory.getFile.bind(directory, name, | |
| 391 { create: !exist, exclusive: !exist })); | |
| 392 }); | |
| 393 }); | |
| 394 }.bind(this)); | |
| 395 }; | |
| 396 | |
| 397 /** | |
| 398 * Returns blob to be saved. | |
| 399 * @param {!HTMLCanvasElement} canvas | |
| 400 * @param {!MetadataModel} metadataModel | |
| 401 * @return {!Promise<!Blob>} | |
| 402 * @private | |
| 403 */ | |
| 404 Gallery.Item.prototype.getBlobForSave_ = function(canvas, metadataModel) { | |
| 405 return metadataModel.get( | |
| 406 [this.entry_], | |
| 407 ['mediaMimeType', 'contentMimeType', 'ifd', 'exifLittleEndian'] | |
| 408 ).then(function(metadataItems) { | |
| 409 // Create the blob of new image. | |
| 410 var metadataItem = metadataItems[0]; | |
| 411 metadataItem.modificationTime = new Date(); | |
| 412 metadataItem.mediaMimeType = this.getNewMimeType_(); | |
| 413 var metadataEncoder = ImageEncoder.encodeMetadata( | |
| 414 metadataItem, canvas, /* quality for thumbnail*/ 0.8); | |
| 415 // Contrary to what one might think 1.0 is not a good default. Opening | |
| 416 // and saving an typical photo taken with consumer camera increases | |
| 417 // its file size by 50-100%. Experiments show that 0.9 is much better. | |
| 418 // It shrinks some photos a bit, keeps others about the same size, but | |
| 419 // does not visibly lower the quality. | |
| 420 return ImageEncoder.getBlob(canvas, metadataEncoder, 0.9); | |
| 421 }.bind(this)); | |
| 422 }; | |
| 423 | |
| 424 /** | |
| 406 * Renames the item. | 425 * Renames the item. |
| 407 * | 426 * |
| 408 * @param {string} displayName New display name (without the extension). | 427 * @param {string} displayName New display name (without the extension). |
| 409 * @return {!Promise} Promise fulfilled with when renaming completes, or | 428 * @return {!Promise} Promise fulfilled with when renaming completes, or |
| 410 * rejected with the error message. | 429 * rejected with the error message. |
| 411 */ | 430 */ |
| 412 Gallery.Item.prototype.rename = function(displayName) { | 431 Gallery.Item.prototype.rename = function(displayName) { |
| 413 var newFileName = this.entry_.name.replace( | 432 var newFileName = this.entry_.name.replace( |
| 414 ImageUtil.getDisplayNameFromName(this.entry_.name), displayName); | 433 ImageUtil.getDisplayNameFromName(this.entry_.name), displayName); |
| 415 | 434 |
| (...skipping 15 matching lines...) Expand all Loading... | |
| 431 return Promise.reject(str('GALLERY_FILE_EXISTS')); | 450 return Promise.reject(str('GALLERY_FILE_EXISTS')); |
| 432 }, function() { | 451 }, function() { |
| 433 return new Promise( | 452 return new Promise( |
| 434 this.entry_.moveTo.bind(this.entry_, parentDirectory, newFileName)); | 453 this.entry_.moveTo.bind(this.entry_, parentDirectory, newFileName)); |
| 435 }.bind(this)); | 454 }.bind(this)); |
| 436 }.bind(this)); | 455 }.bind(this)); |
| 437 }.bind(this)).then(function(entry) { | 456 }.bind(this)).then(function(entry) { |
| 438 this.entry_ = entry; | 457 this.entry_ = entry; |
| 439 }.bind(this)); | 458 }.bind(this)); |
| 440 }; | 459 }; |
| OLD | NEW |