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 |