Commit 1c6838de authored by Kuo Jen Wei's avatar Kuo Jen Wei Committed by Chromium LUCI CQ

CCA: Fix exif loss of photo saved in photo mode

Fixed by not calling |orientPhoto()| on saved photo but only on
thumbnail to be scaled.

Bug: b/173690479
Test: Manually check exif the result photo taking from different
orientation and photo can display correctly in backlight and files app.

Change-Id: I7374f0303169dd5e8efb6b7f6d8e04e90aefaf44
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2559474Reviewed-by: default avatarShik Chen <shik@chromium.org>
Commit-Queue: Inker Kuo <inker@chromium.org>
Auto-Submit: Inker Kuo <inker@chromium.org>
Cr-Commit-Position: refs/heads/master@{#832255}
parent 9ecc2a31
......@@ -65,11 +65,8 @@ class CoverPhoto {
*/
static async create(file) {
const isVideo = filesystem.hasVideoPrefix(file);
const fileUrl = await filesystem.pictureURL(file);
const thumbnail =
await util.scalePicture(fileUrl, isVideo, THUMBNAIL_WIDTH);
URL.revokeObjectURL(fileUrl);
await util.scalePicture(await file.file(), isVideo, THUMBNAIL_WIDTH);
return new CoverPhoto(file, URL.createObjectURL(thumbnail));
}
}
......@@ -183,15 +180,7 @@ export class GalleryButton {
* @override
*/
async savePhoto(blob, name) {
const orientedPhoto = await new Promise((resolve) => {
// Ignore errors since it is better to save something than
// nothing.
// TODO(yuli): Support showing images by EXIF orientation
// instead.
util.orientPhoto(blob, resolve, () => resolve(blob));
});
const file = await filesystem.saveBlob(orientedPhoto, name);
assert(file !== null);
const file = await filesystem.saveBlob(blob, name);
await this.updateCover_(file);
}
......
......@@ -32,152 +32,6 @@ export function newDrawingCanvas({width, height}) {
return {canvas, ctx};
}
/**
* Gets the clockwise rotation and flip that can orient a photo to its upright
* position of the photo and drop its orientation information.
* @param {!Blob} blob JPEG blob that might contain EXIF orientation field.
* @return {!Promise<{rotation: number, flip: boolean, blob: !Blob}>} The
* rotation, flip information of photo and the photo blob after drop those
* information.
*/
function dropPhotoOrientation(blob) {
let /** !Blob */ blobWithoutOrientation = blob;
const getOrientation = (async () => {
const buffer = await blob.arrayBuffer();
const view = new DataView(buffer);
if (view.getUint16(0, false) !== 0xFFD8) {
return 1;
}
const length = view.byteLength;
let offset = 2;
while (offset < length) {
if (view.getUint16(offset + 2, false) <= 8) {
break;
}
const marker = view.getUint16(offset, false);
offset += 2;
if (marker === 0xFFE1) {
if (view.getUint32(offset += 2, false) !== 0x45786966) {
break;
}
const little = view.getUint16(offset += 6, false) === 0x4949;
offset += view.getUint32(offset + 4, little);
const tags = view.getUint16(offset, little);
offset += 2;
for (let i = 0; i < tags; i++) {
if (view.getUint16(offset + (i * 12), little) === 0x0112) {
offset += (i * 12) + 8;
const orientation = view.getUint16(offset, little);
view.setUint16(offset, 1, little);
blobWithoutOrientation = new Blob([buffer]);
return orientation;
}
}
} else if ((marker & 0xFF00) !== 0xFF00) {
break;
} else {
offset += view.getUint16(offset, false);
}
}
return 1;
})();
return getOrientation
.then((orientation) => {
switch (orientation) {
case 1:
return {rotation: 0, flip: false};
case 2:
return {rotation: 0, flip: true};
case 3:
return {rotation: 180, flip: false};
case 4:
return {rotation: 180, flip: true};
case 5:
return {rotation: 90, flip: true};
case 6:
return {rotation: 90, flip: false};
case 7:
return {rotation: 270, flip: true};
case 8:
return {rotation: 270, flip: false};
default:
return {rotation: 0, flip: false};
}
})
.then((orientInfo) => {
return Object.assign(orientInfo, {blob: blobWithoutOrientation});
});
}
/**
* Orients a photo to the upright orientation.
* @param {!Blob} blob Photo as a blob.
* @param {function(!Blob)} onSuccess Success callback with the result photo as
* a blob.
* @param {function()} onFailure Failure callback.
*/
export function orientPhoto(blob, onSuccess, onFailure) {
// TODO(shenghao): Revise or remove this function if it's no longer
// applicable.
const drawPhoto = function(original, orientation, onSuccess, onFailure) {
const canvasSquareLength = Math.max(original.width, original.height);
const {canvas, ctx} = newDrawingCanvas(
{width: canvasSquareLength, height: canvasSquareLength});
const [centerX, centerY] = [canvas.width / 2, canvas.height / 2];
ctx.translate(centerX, centerY);
ctx.rotate(orientation.rotation * Math.PI / 180);
if (orientation.flip) {
ctx.scale(-1, 1);
}
ctx.drawImage(
original, -original.width / 2, -original.height / 2, original.width,
original.height);
if (orientation.flip) {
ctx.scale(-1, 1);
}
ctx.rotate(-orientation.rotation * Math.PI / 180);
ctx.translate(-centerX, -centerY);
const outputSize = (() => {
if (orientation.rotation === 90 || orientation.rotation === 270) {
return {width: original.height, height: original.width};
} else {
return {width: original.width, height: original.height};
}
})();
const {canvas: outputCanvas, ctx: outputCtx} = newDrawingCanvas(outputSize);
const imageData = ctx.getImageData(
(canvasSquareLength - outputCanvas.width) / 2,
(canvasSquareLength - outputCanvas.height) / 2, outputCanvas.width,
outputCanvas.height);
outputCtx.putImageData(imageData, 0, 0);
outputCanvas.toBlob(function(blob) {
if (blob) {
onSuccess(blob);
} else {
onFailure();
}
}, 'image/jpeg');
};
dropPhotoOrientation(blob).then((orientation) => {
if (orientation.rotation === 0 && !orientation.flip) {
onSuccess(blob);
} else {
const original = dom.create('img', HTMLImageElement);
original.onload = function() {
drawPhoto(original, orientation, onSuccess, onFailure);
};
original.onerror = onFailure;
original.src = URL.createObjectURL(orientation.blob);
}
});
}
/**
* Cancels animating the element by removing 'animate' class.
* @param {!HTMLElement} element Element for canceling animation.
......@@ -350,7 +204,7 @@ export function getDefaultFacing() {
/**
* Scales the input picture to target width and height with respect to original
* aspect ratio.
* @param {string} url Picture as an URL.
* @param {!Blob} blob Blob of photo or video to be scaled.
* @param {boolean} isVideo Picture is a video.
* @param {number} width Target width to be scaled to.
* @param {number=} height Target height to be scaled to. In default, set to
......@@ -358,61 +212,61 @@ export function getDefaultFacing() {
* ratio of input picture.
* @return {!Promise<!Blob>} Promise for the result.
*/
export async function scalePicture(url, isVideo, width, height = undefined) {
export async function scalePicture(blob, isVideo, width, height = undefined) {
const element = isVideo ? dom.create('video', HTMLVideoElement) :
dom.create('img', HTMLImageElement);
if (isVideo) {
element.preload = 'auto';
}
await new Promise((resolve, reject) => {
element.addEventListener(isVideo ? 'canplay' : 'load', resolve);
element.addEventListener('error', () => {
if (isVideo) {
let msg = 'Failed to load video';
/**
* https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/error
* @type {?MediaError}
*/
const err = element.error;
if (err !== null) {
msg += `: ${err.message}`;
try {
await new Promise((resolve, reject) => {
element.addEventListener(isVideo ? 'canplay' : 'load', resolve);
element.addEventListener('error', () => {
if (isVideo) {
let msg = 'Failed to load video';
/**
* https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/error
* @type {?MediaError}
*/
const err = element.error;
if (err !== null) {
msg += `: ${err.message}`;
}
reject(new Error(msg));
} else {
reject(new Error('Failed to load image'));
}
reject(new Error(msg));
} else {
reject(new Error('Failed to load image'));
}
});
element.src = URL.createObjectURL(blob);
});
element.src = url;
});
if (height === undefined) {
const ratio = isVideo ? element.videoHeight / element.videoWidth :
element.height / element.width;
height = Math.round(width * ratio);
}
const {canvas, ctx} = newDrawingCanvas({width, height});
ctx.drawImage(element, 0, 0, width, height);
if (height === undefined) {
const ratio = isVideo ? element.videoHeight / element.videoWidth :
element.height / element.width;
height = Math.round(width * ratio);
}
const {canvas, ctx} = newDrawingCanvas({width, height});
ctx.drawImage(element, 0, 0, width, height);
/**
* @type {!Uint8ClampedArray} A one-dimensional pixels array in RGBA order.
*/
const data = ctx.getImageData(0, 0, width, height).data;
if (data.every((byte) => byte === 0)) {
reportError(
ErrorType.BROKEN_THUMBNAIL, ErrorLevel.ERROR,
new Error('The thumbnail content is broken.'));
// Do not throw an error here. A black thumbnail is still better than no
// thumbnail to let user open the corresponding picutre in gallery.
}
/**
* @type {!Uint8ClampedArray} A one-dimensional pixels array in RGBA order.
*/
const data = ctx.getImageData(0, 0, width, height).data;
if (data.every((byte) => byte === 0)) {
reportError(
ErrorType.BROKEN_THUMBNAIL, ErrorLevel.ERROR,
new Error('The thumbnail content is broken.'));
// Do not throw an error here. A black thumbnail is still better than no
// thumbnail to let user open the corresponding picutre in gallery.
}
return new Promise((resolve, reject) => {
canvas.toBlob((blob) => {
if (blob) {
resolve(blob);
} else {
reject(new Error('Failed to create thumbnail.'));
}
}, 'image/jpeg');
});
return new Promise((resolve) => {
// TODO(b/174190121): Patch important exif entries from input blob to
// result blob.
canvas.toBlob(resolve, 'image/jpeg');
});
} finally {
URL.revokeObjectURL(element.src);
}
}
/**
......
......@@ -21,15 +21,21 @@ import {
*/
async function cropSquare(blob) {
const img = await util.blobToImage(blob);
const side = Math.min(img.width, img.height);
const {canvas, ctx} = util.newDrawingCanvas({width: side, height: side});
ctx.drawImage(
img, Math.floor((img.width - side) / 2),
Math.floor((img.height - side) / 2), side, side, 0, 0, side, side);
const croppedBlob = await new Promise((resolve) => {
canvas.toBlob(resolve, 'image/jpeg');
});
return croppedBlob;
try {
const side = Math.min(img.width, img.height);
const {canvas, ctx} = util.newDrawingCanvas({width: side, height: side});
ctx.drawImage(
img, Math.floor((img.width - side) / 2),
Math.floor((img.height - side) / 2), side, side, 0, 0, side, side);
const croppedBlob = await new Promise((resolve) => {
// TODO(b/174190121): Patch important exif entries from input blob to
// result blob.
canvas.toBlob(resolve, 'image/jpeg');
});
return croppedBlob;
} finally {
URL.revokeObjectURL(img.src);
}
}
/**
......@@ -51,11 +57,6 @@ class SquarePhotoHandler {
* @override
*/
async handleResultPhoto(result, ...args) {
// Since the image blob after square cut will lose its EXIF including
// orientation information. Corrects the orientation before the square
// cut.
result.blob = await new Promise(
(resolve, reject) => util.orientPhoto(result.blob, resolve, reject));
result.blob = await cropSquare(result.blob);
await this.handler_.handleResultPhoto(result, ...args);
}
......
......@@ -56,7 +56,7 @@ export class CameraIntent extends Camera {
const ratio = Math.sqrt(
DOWNSCALE_INTENT_MAX_PIXEL_NUM / (image.width * image.height));
blob = await util.scalePicture(
image.src, false, Math.floor(image.width * ratio),
blob, false, Math.floor(image.width * ratio),
Math.floor(image.height * ratio));
}
const buf = await blob.arrayBuffer();
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment