Commit 347acbb3 authored by Trent Apted's avatar Trent Apted Committed by Commit Bot

CrOS media-app: Splice in a minimal EXIF header to capture RAW rotations.

The PiexLoader module used for loading RAW image types in
chrome://media-app returns the JPEG "preview", with an orientation field
from the Exif data embedded in the RAW file headers separate. Currently,
chrome://media-app ignores this orientation.

To fix, "splice" in a minimal Exif header at the front of the returned
JPEG data in the RAW loading codepath.

Bug: 1030935, b/169717921
Change-Id: Ie5454736078d6ece854318b5f6142d7ee42f187b
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2500684
Commit-Queue: Trent Apted <tapted@chromium.org>
Reviewed-by: default avatarBugs Nash <bugsnash@chromium.org>
Cr-Commit-Position: refs/heads/master@{#821108}
parent 41b69354
......@@ -176,8 +176,8 @@ IN_PROC_BROWSER_TEST_P(MediaAppIntegrationTest, MediaAppLaunchWithFile) {
EXPECT_EQ("640x480", WaitForImageAlt(app, kFileJpeg640x480));
}
// Test that the MediaApp can load a RAW file passed on launch params.
IN_PROC_BROWSER_TEST_P(MediaAppIntegrationTest, HandleRawFile) {
// Test that the MediaApp can load RAW files passed on launch params.
IN_PROC_BROWSER_TEST_P(MediaAppIntegrationTest, HandleRawFiles) {
WaitForTestSystemAppInstall();
auto params = LaunchParamsForApp(web_app::SystemAppType::MEDIA);
......@@ -187,6 +187,34 @@ IN_PROC_BROWSER_TEST_P(MediaAppIntegrationTest, HandleRawFile) {
PrepareAppForTest(web_ui);
EXPECT_EQ("378x272", WaitForImageAlt(web_ui, kRaw378x272));
// Loading a raw file will put the RAW loading module into the JS context.
// Inject a script to manipulate the RAW loader into returning a result that
// includes an Exif rotation.
constexpr char kAdd270DegreeRotation[] = R"(
(function() {
const realPiexLoad = PiexLoader.load;
PiexLoader.load = async (buffer, onFailure) => {
const response = await realPiexLoad(buffer, onPiexModuleFailed);
response.orientation = 8;
return response;
};
})();
)";
content::RenderFrameHost* app = MediaAppUiBrowserTest::GetAppFrame(web_ui);
EXPECT_EQ(true, ExecuteScript(app, kAdd270DegreeRotation));
// Launch with a file that has a different name to ensure the rotated version
// of the file is detected robustly.
auto clearFileParams = LaunchParamsForApp(web_app::SystemAppType::MEDIA);
clearFileParams.launch_files = {TestFile(kFileJpeg640x480)};
LaunchAppWithoutWaiting(clearFileParams);
EXPECT_EQ("640x480", WaitForImageAlt(web_ui, kFileJpeg640x480));
LaunchAppWithoutWaiting(params);
// Width and height should be swapped now.
EXPECT_EQ("272x378", WaitForImageAlt(web_ui, kRaw378x272));
}
// Ensures that chrome://media-app is available as a file task for the ChromeOS
......
......@@ -13,20 +13,175 @@ function onPiexModuleFailed() {
piexEnabled = false;
}
const SIZEOF_APP1_PREFIX = 8;
const SIZEOF_TIFF_HEADER = 8;
const SIZEOF_SINGLE_IFD_FRAME = 18;
const NUM_IFD_FRAMES = 1;
/**
* For the minimal header, just dump an orientation tag into the "0th" frame and
* nothing else. There _should_ be other stuff, like a pointer to the "EXIF" IFD
* frame (containing ExifVersion, etc.), and tags for image resolution. But
* these are not used by Chrome for rendering.
*/
const SIZEOF_APP1_HEADER = SIZEOF_APP1_PREFIX + SIZEOF_TIFF_HEADER +
NUM_IFD_FRAMES * SIZEOF_SINGLE_IFD_FRAME;
/**
* A minimal EXIF header to include an orientation field. Includes the "Start of
* image" prefix that should already be present, so that this can just form the
* start of the resulting JPEG; with the quantization table following.
*
* The table at https://www.exif.org/Exif2-2.PDF#page=70 was the starting point
* for this, but details come from all over the document.
*
* | Offset | Code | Meaning |
* | ------ | ---- | --------------- |
* | -2 | 0xFF | SOI Prefix |
* | -1 | 0xD8 | Start of Image |
* | +0 | 0xFF | Marker Prefix |
* | +1 | 0xE1 | APP1 |
* | +2 | 0x.. | Length[1] | // Always big-endian.
* | +3 | 0x.. | Length[0] | // SIZEOF_APP1_HEADER.
* | +4 | 0x45 | 'E' |
* | +5 | 0x78 | 'x' |
* | +6 | 0x69 | 'i' |
* | +7 | 0x66 | 'f' |
* | +8 | 0x00 | NULL |
* | +9 | 0x00 | Padding |
* | | | TIFF header | // 8 bytes. Offsets start from here.
* | +10 | 0x49 | ByteOrder ('I') | // Little-endian (seems more common).
* | +11 | 0x49 | ByteOrder ('I') |
* | +12 | 0x2a | IFD marker[0] |
* | +13 | 0x00 | IFD marker[1] |
* | +14 | 0x08 | IFD offset[0] | // 8 (0th IFD immediately follows).
* | +15 | 0x00 | IFD offset[1] |
* | +16 | 0x00 | IFD offset[2] |
* | +17 | 0x00 | IFD offset[3] |
* | | | IFD frame | // 18 bytes
* | +18 | 0x01 | Field count[0] | // 1 Field (just orientation).
* | +19 | 0x00 | Field count[1] |
* | +20 | 0x12 | TAG[0] | // E.g. TAG_ORIENTATION (0x0112)
* | +21 | 0x01 | TAG[1] |
* | +22 | 0x03 | Data type[0] | // 3 = "Short"
* | +23 | 0x00 | Data type[1] |
* | +24 | 0x01 | Data count[0] | // 1
* | +25 | 0x00 | Data count[1] |
* | +26 | 0x00 | Data count[2] |
* | +27 | 0x00 | Data count[3] |
* | 28-29 | 0x.. | Value | // Orientation goes here! <omitted>
* | 30-31 | 0x00 | Padding |
* | 32-35 | 0x00 | Offset to next | // 0 to indicate "no more".
*/
const TIFF_HEADER = new Uint8Array([
0xff,
0xd8,
0xff,
0xe1,
SIZEOF_APP1_HEADER >> 8,
SIZEOF_APP1_HEADER & 0xff,
0x45,
0x78,
0x69,
0x66,
0x00,
0x00,
0x49,
0x49,
0x2a,
0x00,
0x08,
0x00,
0x00,
0x00,
]);
/**
* Makes an 18-byte little-endian IFD frame for the Exif orientation value.
* This is a number [1, 8]. The Exif spec requests a 16-bit unsigned int to be
* written.
* Reference: https://www.exif.org/Exif2-2.PDF#page=19.
* @param {number} value orientation value.
* @return {!ArrayBuffer}
*/
function makeOrientationIfdFrame(value) {
const LITTLE_ENDIAN = true;
const TAG_ORIENTATION = 0x0112;
const TYPE_UINT16 = 3; // 1=BYTE, 2=ASCII, 3=SHORT, 4=LONG, etc.
const FIELD_COUNT = 1; // Just writing orientation.
const VALUE_LENGTH = 1; // Writing one "short".
const NEXT_OFFSET = 0; // No more frames.
const buffer = new ArrayBuffer(SIZEOF_SINGLE_IFD_FRAME);
const view = new DataView(buffer);
view.setUint16(0, FIELD_COUNT, LITTLE_ENDIAN);
view.setUint16(2, TAG_ORIENTATION, LITTLE_ENDIAN);
view.setUint16(4, TYPE_UINT16, LITTLE_ENDIAN);
view.setUint32(6, VALUE_LENGTH, LITTLE_ENDIAN);
// The value is <= 4 bytes, so write directly. Otherwise an offset to the
// value data would be written here. Note the type "hugs" the low-order bits
// and is followed by padding if the data size is < 4 bytes.
view.setUint16(10, value, LITTLE_ENDIAN);
view.setUint32(14, NEXT_OFFSET, LITTLE_ENDIAN);
return buffer;
}
/**
* Extracts a JPEG from a RAW Image ArrayBuffer.
* @param {!ArrayBuffer} buffer
* @return {!Promise<!File>}
*/
async function extractFromRawImageBuffer(buffer) {
/** Quantization table. */
const DQT_MARKER = 0xffdb;
/** SOI. Page 64. */
const START_OF_IMAGE = 0xffd8;
/** Field value for no rotation. Don't add an Exif header in this case. */
const NO_ROTATION = 1;
if (!piexEnabled) {
throw new Error('Piex disabled');
}
const response = await PiexLoader.load(buffer, onPiexModuleFailed);
// Note the "thumbnail" is usually the full-sized image "preview", but may
// fall back to a thumbnail when that is unavailable.
// The mime type may be unsupported - let the caller deal with that.
// TOD(b/169717921): Apply `response.orientation`.
/** @type {!ArrayBuffer} */
const jpegData = response.thumbnail;
/**
* @param {string=} warning
* @return {!File}
*/
function original(warning = '') {
if (warning) {
console.warn(`Returning unrotated image: ${warning}.`);
}
return new File([jpegData], 'raw-preview', {type: response.mimeType});
}
if (response.orientation === NO_ROTATION) {
return original();
}
const view = new DataView(jpegData);
if (view.getUint16(0) !== START_OF_IMAGE) {
return original('No SOI');
}
// Files returned by Piex should begin immediately with JPEG headers.
if (view.getUint16(2) !== DQT_MARKER) {
return original('Unexpected marker');
}
// Ignore the Start-Of-Image already in `jpegData` (TIFF_HEADER has one).
const jpegWithoutSOI = (new Blob([jpegData])).slice(2);
const orientation = makeOrientationIfdFrame(response.orientation);
return new File(
[response.thumbnail], 'raw-preview', {type: response.mimeType});
[TIFF_HEADER, orientation, jpegWithoutSOI], 'raw-preview',
{type: response.mimeType});
}
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