Commit 8bb8a094 authored by Trent Apted's avatar Trent Apted Committed by Commit Bot

Load Piex WASM into chrome-untrusted://media-app for decoding RAW.

Piex is the "Preview Image EXtractor" and is used by the Files app on
ChromeOS for creating thumbnails inside the "image loader" component
extension. It uses the `wasm-eval` CSP directive, which is only
currently enabled for apps and extensions (standardisation being
discussed at github.com/WebAssembly/content-security-policy/pull/13 ).

To make the WASM work, this CL enables `wasm-eval` for
chrome-untrusted://* CSP directives, and adds that to
chrome-untrusted://media-app's CSP.

Two new JS files are added to chrome-untrusted://media-app:
 - piex_module.js is a thin wrapper around the existing piex_loader.js
   to do (effectively) RAW -> JPEG conversions of Blob data.
 - piex_module_loader.js adds logic for runtime-loading of the
   WebAssembly, piex_loader.js, and piex_module.js

An end-to-end test using the handcrafted "raw.orf" file is added to
ensure the WASM loads and successfully extracts preview data, and it
is successfully loaded by the app.

Note this CL adds support, but doesn't change file handlers to direct
RAW files to chrome://media-app, which will be gated behind a flag.

Design Doc: go/bl-raw
Launch Bug: https://crbug.com/1126203

Bug: 1030935, b/154062029, b/167496867
Cq-Include-Trybots: luci.chrome.try:linux-chromeos-chrome
Change-Id: I0c1f1da8bc39c8e6223214b49d9ac09dc93bf420
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2422100Reviewed-by: default avatardpapad <dpapad@chromium.org>
Reviewed-by: default avatarDaniel Cheng <dcheng@chromium.org>
Reviewed-by: default avatarNoel Gordon <noel@chromium.org>
Reviewed-by: default avatarPatti <patricialor@chromium.org>
Commit-Queue: Noel Gordon <noel@chromium.org>
Auto-Submit: Trent Apted <tapted@chromium.org>
Cr-Commit-Position: refs/heads/master@{#818636}
parent b5c84d18
...@@ -43,6 +43,10 @@ constexpr char kFilePng800x600[] = "image.png"; ...@@ -43,6 +43,10 @@ constexpr char kFilePng800x600[] = "image.png";
// A 640x480 image/jpeg (all green pixels). // A 640x480 image/jpeg (all green pixels).
constexpr char kFileJpeg640x480[] = "image3.jpg"; constexpr char kFileJpeg640x480[] = "image3.jpg";
// A RAW file from an Olympus camera with the original preview/thumbnail data
// swapped out with "exif.jpg".
constexpr char kRaw378x272[] = "raw.orf";
// A 1-second long 648x486 VP9-encoded video with stereo Opus-encoded audio. // A 1-second long 648x486 VP9-encoded video with stereo Opus-encoded audio.
constexpr char kFileVideoVP9[] = "world.webm"; constexpr char kFileVideoVP9[] = "world.webm";
...@@ -172,6 +176,19 @@ IN_PROC_BROWSER_TEST_P(MediaAppIntegrationTest, MediaAppLaunchWithFile) { ...@@ -172,6 +176,19 @@ IN_PROC_BROWSER_TEST_P(MediaAppIntegrationTest, MediaAppLaunchWithFile) {
EXPECT_EQ("640x480", WaitForImageAlt(app, kFileJpeg640x480)); 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) {
WaitForTestSystemAppInstall();
auto params = LaunchParamsForApp(web_app::SystemAppType::MEDIA);
// Add the handcrafted RAW file to launch params and launch.
params.launch_files.push_back(TestFile(kRaw378x272));
content::WebContents* web_ui = LaunchApp(params);
PrepareAppForTest(web_ui);
EXPECT_EQ("378x272", WaitForImageAlt(web_ui, kRaw378x272));
}
// Ensures that chrome://media-app is available as a file task for the ChromeOS // Ensures that chrome://media-app is available as a file task for the ChromeOS
// file manager and eligible for opening appropriate files / mime types. // file manager and eligible for opening appropriate files / mime types.
IN_PROC_BROWSER_TEST_P(MediaAppIntegrationAllProfilesTest, IN_PROC_BROWSER_TEST_P(MediaAppIntegrationAllProfilesTest,
......
...@@ -33,6 +33,7 @@ static_library("media_app_ui") { ...@@ -33,6 +33,7 @@ static_library("media_app_ui") {
"//content/public/browser", "//content/public/browser",
"//mojo/public/cpp/bindings", "//mojo/public/cpp/bindings",
"//mojo/public/cpp/platform", "//mojo/public/cpp/platform",
"//ui/file_manager:resources",
"//ui/webui", "//ui/webui",
] ]
} }
......
...@@ -7,5 +7,6 @@ include_rules = [ ...@@ -7,5 +7,6 @@ include_rules = [
"+components/content_settings/core/common", "+components/content_settings/core/common",
"+content/public/browser", "+content/public/browser",
"+content/public/common", "+content/public/common",
"+ui/file_manager/grit",
"+ui/webui", "+ui/webui",
] ]
...@@ -13,6 +13,7 @@ ...@@ -13,6 +13,7 @@
#include "content/public/browser/web_ui.h" #include "content/public/browser/web_ui.h"
#include "content/public/browser/web_ui_data_source.h" #include "content/public/browser/web_ui_data_source.h"
#include "services/network/public/mojom/content_security_policy.mojom.h" #include "services/network/public/mojom/content_security_policy.mojom.h"
#include "ui/file_manager/grit/file_manager_resources.h"
namespace chromeos { namespace chromeos {
...@@ -24,6 +25,12 @@ content::WebUIDataSource* CreateMediaAppUntrustedDataSource( ...@@ -24,6 +25,12 @@ content::WebUIDataSource* CreateMediaAppUntrustedDataSource(
source->AddResourcePath("app.html", IDR_MEDIA_APP_APP_HTML); source->AddResourcePath("app.html", IDR_MEDIA_APP_APP_HTML);
source->AddResourcePath("media_app_app_scripts.js", source->AddResourcePath("media_app_app_scripts.js",
IDR_MEDIA_APP_APP_SCRIPTS_JS); IDR_MEDIA_APP_APP_SCRIPTS_JS);
source->AddResourcePath("piex_module_scripts.js",
IDR_MEDIA_APP_PIEX_MODULE_SCRIPTS_JS);
// Add shared resources from chromeos_file_manager_resources.pak.
source->AddResourcePath("piex/piex.js.wasm", IDR_IMAGE_LOADER_PIEX_WASM_JS);
source->AddResourcePath("piex/piex.out.wasm", IDR_IMAGE_LOADER_PIEX_WASM);
// Add resources from chromeos_media_app_bundle_resources.pak that are also // Add resources from chromeos_media_app_bundle_resources.pak that are also
// needed for mocks. If enable_cros_media_app = true, then these calls will // needed for mocks. If enable_cros_media_app = true, then these calls will
...@@ -59,6 +66,12 @@ content::WebUIDataSource* CreateMediaAppUntrustedDataSource( ...@@ -59,6 +66,12 @@ content::WebUIDataSource* CreateMediaAppUntrustedDataSource(
source->OverrideContentSecurityPolicy( source->OverrideContentSecurityPolicy(
network::mojom::CSPDirectiveName::StyleSrc, network::mojom::CSPDirectiveName::StyleSrc,
"style-src 'self' 'unsafe-inline';"); "style-src 'self' 'unsafe-inline';");
// Allow wasm.
source->OverrideContentSecurityPolicy(
network::mojom::CSPDirectiveName::ScriptSrc,
"script-src 'self' 'wasm-eval';");
// TODO(crbug.com/1098685): Trusted Type remaining WebUI. // TODO(crbug.com/1098685): Trusted Type remaining WebUI.
source->DisableTrustedTypesCSP(); source->DisableTrustedTypesCSP();
return source; return source;
......
...@@ -56,10 +56,19 @@ js_library("error_reporter") { ...@@ -56,10 +56,19 @@ js_library("error_reporter") {
externs_list = [ "$externs_path/crash_report_private.js" ] externs_list = [ "$externs_path/crash_report_private.js" ]
} }
js_library("piex_module_loader") {
}
js_library("piex_module") {
deps = [ "//ui/file_manager/image_loader:piex_loader" ]
}
js_library("receiver") { js_library("receiver") {
externs_list = [ "media_app.externs.js" ] externs_list = [ "media_app.externs.js" ]
deps = [ deps = [
":message_types", ":message_types",
":piex_module",
":piex_module_loader",
"//chromeos/components/system_apps/public/js:message_pipe", "//chromeos/components/system_apps/public/js:message_pipe",
"//ui/webui/resources/js:load_time_data", "//ui/webui/resources/js:load_time_data",
] ]
......
...@@ -600,6 +600,15 @@ async function getFileFromHandle(fileSystemHandle) { ...@@ -600,6 +600,15 @@ async function getFileFromHandle(fileSystemHandle) {
return {file, handle}; return {file, handle};
} }
/**
* Returns whether `filename` has an extension indicating a possible RAW image.
* @param {string} filename
* @return {boolean}
*/
function isRawImageFile(filename) {
return /\.(arw|cr2|dng|nef|nrw|orf|raf|rw2)$/.test(filename.toLowerCase());
}
/** /**
* Returns whether `file` is a video or image file. * Returns whether `file` is a video or image file.
* @param {!File} file * @param {!File} file
...@@ -609,7 +618,7 @@ function isVideoOrImage(file) { ...@@ -609,7 +618,7 @@ function isVideoOrImage(file) {
// Check for .mkv explicitly because it is not a web-supported type, but is in // Check for .mkv explicitly because it is not a web-supported type, but is in
// common use on ChromeOS. // common use on ChromeOS.
return /^(image)|(video)\//.test(file.type) || return /^(image)|(video)\//.test(file.type) ||
/\.mkv$/.test(file.name.toLowerCase()); /\.mkv$/.test(file.name.toLowerCase()) || isRawImageFile(file.name);
} }
/** /**
......
...@@ -166,6 +166,13 @@ mediaApp.ClientApiDelegate.prototype.requestSaveFile = function( ...@@ -166,6 +166,13 @@ mediaApp.ClientApiDelegate.prototype.requestSaveFile = function(
* @return {!Promise<undefined>} * @return {!Promise<undefined>}
*/ */
mediaApp.ClientApiDelegate.prototype.openFile = function() {}; mediaApp.ClientApiDelegate.prototype.openFile = function() {};
/**
* Attempts to extract a JPEG "preview" from a RAW image file. Throws on any
* failure. Note this is typically a full-sized preview, not a thumbnail.
* @param {!Blob} file
* @return {!Promise<!File>} A Blob-backed File with type: image/jpeg.
*/
mediaApp.ClientApiDelegate.prototype.extractPreview = function(file) {};
/** /**
* The client Api for interacting with the media app instance. * The client Api for interacting with the media app instance.
......
...@@ -7,5 +7,6 @@ ...@@ -7,5 +7,6 @@
// <include src="../../../../../ui/webui/resources/js/load_time_data.js"> // <include src="../../../../../ui/webui/resources/js/load_time_data.js">
// <include src="../../../system_apps/public/js/message_pipe.js"> // <include src="../../../system_apps/public/js/message_pipe.js">
// <include src="message_types.js"> // <include src="message_types.js">
// <include src="piex_module_loader.js">
// <include src="receiver.js"> // <include src="receiver.js">
// <include src="app_context_test_support.js"> // <include src="app_context_test_support.js">
// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/**
* Set when PiexLoader has an unrecoverable error to disable future attempts.
* @type {boolean}
*/
let piexEnabled = true;
/** Handles wasm load failures. */
function onPiexModuleFailed() {
piexEnabled = false;
}
/**
* Extracts a JPEG from a RAW Image ArrayBuffer.
* @param {!ArrayBuffer} buffer
* @return {!Promise<!File>}
*/
async function extractFromRawImageBuffer(buffer) {
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`.
return new File(
[response.thumbnail], 'raw-preview', {type: response.mimeType});
}
// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/** @type {?Promise} */
let _piexLoadPromise = null;
/**
* Loads PIEX, the "Preview Image Extractor", via wasm.
* @return {!Promise}
*/
function loadPiex() {
async function startLoad() {
/** @type {function(string): !Promise} */
const loadJs = (/** string */ path) => new Promise((resolve, reject) => {
const script =
/** @type {!HTMLScriptElement} */ (document.createElement('script'));
script.onload = resolve;
script.onerror = reject;
script.src = path;
assertCast(document.head).appendChild(script);
});
await loadJs('piex/piex.js.wasm');
await loadJs('piex_module_scripts.js');
await new Promise(resolve => {
PiexModule['onRuntimeInitialized'] = resolve;
});
}
if (!_piexLoadPromise) {
_piexLoadPromise = startLoad();
}
return _piexLoadPromise;
}
// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/** @fileoverview Concatenation of JS files for loading Piex WASM. */
// <include
// src="../../../../../ui/file_manager/image_loader/piex_loader.js">
// <include src="piex_module.js">
...@@ -239,6 +239,23 @@ const DELEGATE = { ...@@ -239,6 +239,23 @@ const DELEGATE = {
*/ */
async openFile() { async openFile() {
await parentMessagePipe.sendMessage(Message.OPEN_FILE); await parentMessagePipe.sendMessage(Message.OPEN_FILE);
},
/**
* @param {!Blob} file
* @return {!Promise<!File>}
*/
async extractPreview(file) {
try {
const [buffer] = /** @type {!Array<!ArrayBuffer>} */ (
await Promise.all([file.arrayBuffer(), loadPiex()]));
return await extractFromRawImageBuffer(buffer);
} catch (/** @type {!Error} */ e) {
console.warn(e);
if (e.name === 'Error') {
e.name = 'JpegNotFound';
}
throw e;
}
} }
}; };
......
...@@ -27,6 +27,7 @@ ...@@ -27,6 +27,7 @@
<!-- Unprivileged guest contents. --> <!-- Unprivileged guest contents. -->
<include name="IDR_MEDIA_APP_APP_HTML" file="app.html" type="BINDATA" /> <include name="IDR_MEDIA_APP_APP_HTML" file="app.html" type="BINDATA" />
<include name="IDR_MEDIA_APP_APP_SCRIPTS_JS" file="js/media_app_app_scripts.js" flattenhtml="true" compress="brotli" type="BINDATA" /> <include name="IDR_MEDIA_APP_APP_SCRIPTS_JS" file="js/media_app_app_scripts.js" flattenhtml="true" compress="brotli" type="BINDATA" />
<include name="IDR_MEDIA_APP_PIEX_MODULE_SCRIPTS_JS" file="js/piex_module_scripts.js" flattenhtml="true" compress="brotli" type="BINDATA" />
</includes> </includes>
</release> </release>
</grit> </grit>
...@@ -25,10 +25,10 @@ const createVideoChild = async (blobSrc) => { ...@@ -25,10 +25,10 @@ const createVideoChild = async (blobSrc) => {
}; };
/** @type {ModuleHandler} */ /** @type {ModuleHandler} */
const createImgChild = async (blobSrc, altText) => { const createImgChild = async (blobSrc, fileName) => {
const img = /** @type {!HTMLImageElement} */ (document.createElement('img')); const img = /** @type {!HTMLImageElement} */ (document.createElement('img'));
img.src = blobSrc; img.src = blobSrc;
img.alt = altText; img.alt = fileName;
try { try {
await img.decode(); await img.decode();
} catch (error) { } catch (error) {
...@@ -55,12 +55,30 @@ class BacklightApp extends HTMLElement { ...@@ -55,12 +55,30 @@ class BacklightApp extends HTMLElement {
this.appendChild(this.currentMedia); this.appendChild(this.currentMedia);
/** @type {?mediaApp.AbstractFileList} */ /** @type {?mediaApp.AbstractFileList} */
this.files; this.files;
/** @type {?mediaApp.ClientApiDelegate} */
this.delegate;
}
/**
* Emulates the preprocessing done in the "real" BacklightApp to hook in the
* RAW file converter. See go/media-app-element.
*
* @param {?mediaApp.AbstractFile} file
* @private
*/
async preprocessFile(file) {
// This mock is only used for tests (which only test a .orf RAW file). We
// don't maintain the full list of RAW extensions here.
if (file && file.name.toLowerCase().endsWith('.orf')) {
file.blob = await this.delegate.extractPreview(file.blob);
}
} }
/** @override */ /** @override */
async loadFiles(files) { async loadFiles(files) {
let child; let child;
const file = files.item(0); const file = files.item(0);
await this.preprocessFile(file);
if (file) { if (file) {
const isVideo = file.mimeType.match('^video/'); const isVideo = file.mimeType.match('^video/');
const factory = isVideo ? createVideoChild : createImgChild; const factory = isVideo ? createVideoChild : createImgChild;
...@@ -88,7 +106,9 @@ class BacklightApp extends HTMLElement { ...@@ -88,7 +106,9 @@ class BacklightApp extends HTMLElement {
} }
/** @override */ /** @override */
setDelegate(delegate) {} setDelegate(delegate) {
this.delegate = delegate;
}
/** @param {!mediaApp.AbstractFileList} files */ /** @param {!mediaApp.AbstractFileList} files */
onNewFiles(files) { onNewFiles(files) {
......
...@@ -277,6 +277,9 @@ std::string WebUIDataSourceImpl::GetMimeType(const std::string& path) const { ...@@ -277,6 +277,9 @@ std::string WebUIDataSourceImpl::GetMimeType(const std::string& path) const {
if (base::EndsWith(file_path, ".mp4", base::CompareCase::INSENSITIVE_ASCII)) if (base::EndsWith(file_path, ".mp4", base::CompareCase::INSENSITIVE_ASCII))
return "video/mp4"; return "video/mp4";
if (base::EndsWith(file_path, ".wasm", base::CompareCase::INSENSITIVE_ASCII))
return "application/wasm";
return "text/html"; return "text/html";
} }
......
...@@ -223,6 +223,7 @@ TEST_F(WebUIDataSourceTest, MimeType) { ...@@ -223,6 +223,7 @@ TEST_F(WebUIDataSourceTest, MimeType) {
const char* html = "text/html"; const char* html = "text/html";
const char* js = "application/javascript"; const char* js = "application/javascript";
const char* png = "image/png"; const char* png = "image/png";
EXPECT_EQ(GetMimeType(std::string()), html); EXPECT_EQ(GetMimeType(std::string()), html);
EXPECT_EQ(GetMimeType("foo"), html); EXPECT_EQ(GetMimeType("foo"), html);
EXPECT_EQ(GetMimeType("foo.html"), html); EXPECT_EQ(GetMimeType("foo.html"), html);
...@@ -243,6 +244,14 @@ TEST_F(WebUIDataSourceTest, MimeType) { ...@@ -243,6 +244,14 @@ TEST_F(WebUIDataSourceTest, MimeType) {
EXPECT_EQ(GetMimeType("foo.html?abc?abc"), html); EXPECT_EQ(GetMimeType("foo.html?abc?abc"), html);
EXPECT_EQ(GetMimeType("foo.css?abc?abc"), css); EXPECT_EQ(GetMimeType("foo.css?abc?abc"), css);
EXPECT_EQ(GetMimeType("foo.js?abc?abc"), js); EXPECT_EQ(GetMimeType("foo.js?abc?abc"), js);
EXPECT_EQ(GetMimeType("foo.json"), "application/json");
EXPECT_EQ(GetMimeType("foo.pdf"), "application/pdf");
EXPECT_EQ(GetMimeType("foo.svg"), "image/svg+xml");
EXPECT_EQ(GetMimeType("foo.jpg"), "image/jpeg");
EXPECT_EQ(GetMimeType("foo.mp4"), "video/mp4");
EXPECT_EQ(GetMimeType("foo.js.wasm"), "application/wasm");
EXPECT_EQ(GetMimeType("foo.out.wasm"), "application/wasm");
} }
TEST_F(WebUIDataSourceTest, ShouldServeMimeTypeAsContentTypeHeader) { TEST_F(WebUIDataSourceTest, ShouldServeMimeTypeAsContentTypeHeader) {
......
...@@ -1033,6 +1033,8 @@ void RenderThreadImpl::RegisterSchemes() { ...@@ -1033,6 +1033,8 @@ void RenderThreadImpl::RegisterSchemes() {
chrome_untrusted_scheme); chrome_untrusted_scheme);
WebSecurityPolicy::RegisterURLSchemeAsSupportingFetchAPI( WebSecurityPolicy::RegisterURLSchemeAsSupportingFetchAPI(
chrome_untrusted_scheme); chrome_untrusted_scheme);
WebSecurityPolicy::RegisterURLSchemeAsAllowingWasmEvalCSP(
chrome_untrusted_scheme);
// devtools: // devtools:
WebString devtools_scheme(WebString::FromASCII(kChromeDevToolsScheme)); WebString devtools_scheme(WebString::FromASCII(kChromeDevToolsScheme));
......
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