Commit 48640897 authored by Yi Su's avatar Yi Su Committed by Commit Bot

Get image's data by JavaScript to accelerate Copy Image.

Current Copy Image feature downloads the image outside WKWebView. This
CL adds functionality of fetching image's data by JavaScript in 2 steps:

1. Draws <img> to <canvas> and exports it;
2. If 1 failed, downloads the picture by XMLHttpRequest, which may be
  responsed from cache.

Both 2 methods may fail due to cross-origin restriction, but under
different conditions.

More info in this design doc:
https://docs.google.com/document/d/1twZ1-Ssn4_9L4eN23-7H-wJkKKWSWLJTSt6Kwn0DqsU/edit?usp=sharing

Bug: 163201
Cq-Include-Trybots: luci.chromium.try:ios-simulator-full-configs;master.tryserver.chromium.mac:ios-simulator-cronet
Change-Id: I12bac9d6bd40b4c42d35a23745edaa4dca9fa071
Reviewed-on: https://chromium-review.googlesource.com/1145276
Commit-Queue: Yi Su <mrsuyi@chromium.org>
Reviewed-by: default avatarEugene But <eugenebut@chromium.org>
Cr-Commit-Position: refs/heads/master@{#581176}
parent cd96b8c3
......@@ -115,6 +115,7 @@ source_set("tabs_internal") {
"//ios/chrome/browser/translate",
"//ios/chrome/browser/u2f",
"//ios/chrome/browser/ui",
"//ios/chrome/browser/ui:feature_flags",
"//ios/chrome/browser/ui:ui_internal",
"//ios/chrome/browser/ui/alert_coordinator",
"//ios/chrome/browser/ui/commands",
......
......@@ -40,10 +40,12 @@
#import "ios/chrome/browser/tabs/legacy_tab_helper.h"
#import "ios/chrome/browser/tabs/tab.h"
#import "ios/chrome/browser/translate/chrome_ios_translate_client.h"
#import "ios/chrome/browser/ui/ui_feature_flags.h"
#import "ios/chrome/browser/voice/voice_search_navigations_tab_helper.h"
#import "ios/chrome/browser/web/blocked_popup_tab_helper.h"
#import "ios/chrome/browser/web/features.h"
#import "ios/chrome/browser/web/font_size_tab_helper.h"
#import "ios/chrome/browser/web/image_fetch_tab_helper.h"
#import "ios/chrome/browser/web/load_timing_tab_helper.h"
#import "ios/chrome/browser/web/network_activity_indicator_tab_helper.h"
#import "ios/chrome/browser/web/page_placeholder_tab_helper.h"
......@@ -95,6 +97,10 @@ void AttachTabHelpers(web::WebState* web_state, bool for_prerender) {
FontSizeTabHelper::CreateForWebState(web_state);
}
if (base::FeatureList::IsEnabled(kCopyImage)) {
ImageFetchTabHelper::CreateForWebState(web_state);
}
ReadingListModel* model =
ReadingListModelFactory::GetForBrowserState(browser_state);
ReadingListWebStateObserver::CreateForWebState(web_state, model);
......
......@@ -13,6 +13,8 @@ source_set("web") {
"error_page_util.mm",
"font_size_tab_helper.h",
"font_size_tab_helper.mm",
"image_fetch_tab_helper.h",
"image_fetch_tab_helper.mm",
"load_timing_tab_helper.h",
"load_timing_tab_helper.mm",
"mailto_handler.h",
......@@ -86,6 +88,8 @@ source_set("unit_tests") {
"error_page_util_unittest.mm",
"font_size_js_unittest.mm",
"font_size_tab_helper_unittest.mm",
"image_fetch_js_unittest.mm",
"image_fetch_tab_helper_unittest.mm",
"load_timing_tab_helper_unittest.mm",
"mailto_handler_gmail_unittest.mm",
"mailto_handler_inbox_unittest.mm",
......@@ -100,6 +104,7 @@ source_set("unit_tests") {
]
deps = [
":accessibility",
":image_fetch",
":tab_helper_delegates",
":test_support",
":web",
......@@ -112,6 +117,8 @@ source_set("unit_tests") {
"//ios/web",
"//ios/web/public/test",
"//ios/web/public/test/fakes",
"//ios/web/public/test/http_server",
"//net:test_support",
"//testing/gmock",
"//testing/gtest",
"//third_party/ocmock",
......@@ -136,6 +143,7 @@ js_compile_bundle("chrome_bundle_main_frame") {
"//components/autofill/ios/form_util/resources/fill.js",
"//ios/chrome/browser/passwords/resources/password_controller.js",
"resources/chrome_bundle_main_frame.js",
"resources/image_fetch.js",
"resources/print.js",
]
......@@ -163,6 +171,12 @@ js_compile_checked("accessibility") {
]
}
js_compile_checked("image_fetch") {
sources = [
"resources/image_fetch.js",
]
}
source_set("web_internal") {
configs += [ "//build/config/compiler:enable_arc" ]
sources = [
......
// Copyright 2018 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.
#import <UIKit/UIKit.h>
#include "base/base64.h"
#include "base/macros.h"
#import "base/test/ios/wait_util.h"
#import "ios/web/public/test/web_js_test.h"
#import "ios/web/public/test/web_test_with_web_state.h"
#import "ios/web/public/web_state/web_state.h"
#include "net/test/embedded_test_server/embedded_test_server.h"
#include "net/test/embedded_test_server/http_request.h"
#include "net/test/embedded_test_server/http_response.h"
#include "testing/gtest/include/gtest/gtest.h"
#import "testing/gtest_mac.h"
#include "url/gurl.h"
#if !defined(__has_feature) || !__has_feature(objc_arc)
#error "This file requires ARC support."
#endif
using base::test::ios::WaitUntilConditionOrTimeout;
using base::test::ios::kWaitForJSCompletionTimeout;
namespace {
// Base64 data of a PNG image.
const char kImageBase64[] =
"iVBORw0KGgoAAAANSUhEUgAAAIQAAAB+AQMAAADfgyubAAAABlBMVEX+/"
"v4ODg5o52ONAAABfElEQVRIx+3UO27DMAwAUBoeNBQIe4DAvkLHFDDim/"
"QMHp2lVpAhY49Ubb2GgFxARZcMqVjKn1SUgl6g4WBAz6QByRQB7vGf48Hy4yWWgUV5IQagvsRC"
"LG0sRZAhljIInRMphCjSrLEgi5IysLy7SCrOQSFrlnqIpWHZhp1c45mloVh2LH0mOyFfLJ9hJ7"
"EUJyGnVHily48boiPhlTpm8hZLqEAUchwFpFR1LOqmNFIU6ab1iWwy6YWskHQnhXNcluNEI4Qc"
"K2SNtE9E0d6kOaWhWBpFByNaY8M5OhWVi2zDDgkzWQG5RAohFqmCVNYip7DoGykG/"
"SaTXkip0XeZOCFK42WUx/"
"lwQAF+"
"2yBPk2wBoSYLLN0kHipAMmGn05eKIPUohV2kYdFAl3Jq1tJDD6FTyKOZryicJ6G5ERUVDmoPMB"
"At1/"
"hgx3EwLD+"
"sJDIQpF0Ougzl7TmIW0aGB2h5UV9vXChv7TQF5tHjpvHzO5CQX72Gcncf3vf4M34AIPQSr4HM1"
"a4AAAAASUVORK5CYII=";
// Delay of the http response for image.
const int kImageDelayInMs = 1000;
// Request handler of the http server.
std::unique_ptr<net::test_server::HttpResponse> HandleRequest(
const net::test_server::HttpRequest& request) {
std::string image_binary;
EXPECT_TRUE(base::Base64Decode(kImageBase64, &image_binary));
if (request.GetURL().path() == "/image") {
auto result = std::make_unique<net::test_server::BasicHttpResponse>();
result->set_content_type("image/png");
result->set_content(image_binary);
result->AddCustomHeader("Access-Control-Allow-Origin", "*");
return std::move(result);
}
if (request.GetURL().path() == "/image_delayed") {
auto result = std::make_unique<net::test_server::DelayedHttpResponse>(
base::TimeDelta::FromMilliseconds(kImageDelayInMs));
result->set_content_type("image/png");
result->set_content(image_binary);
result->AddCustomHeader("Access-Control-Allow-Origin", "*");
return std::move(result);
}
return nullptr;
}
// The ID param for calling JS, and will be regained in the message sent back.
const int kCallJavaScriptId = 66666;
// The command prefix for receiving messages from JS.
const std::string kCommandPrefix = "imageFetch";
}
// Test fixture for image_fetch.js testing.
class ImageFetchJsTest : public web::WebJsTest<web::WebTestWithWebState> {
protected:
ImageFetchJsTest()
: web::WebJsTest<web::WebTestWithWebState>(@[ @"image_fetch" ]) {}
void SetUp() override {
WebTestWithWebState::SetUp();
server_.RegisterDefaultHandler(base::BindRepeating(HandleRequest));
ASSERT_TRUE(server_.Start());
web_state()->AddScriptCommandCallback(
base::BindRepeating(&ImageFetchJsTest::OnMessageFromJavaScript,
base::Unretained(this)),
kCommandPrefix);
}
void TearDown() override {
web_state()->RemoveScriptCommandCallback(kCommandPrefix);
WebTestWithWebState::TearDown();
}
bool OnMessageFromJavaScript(const base::DictionaryValue& message,
const GURL& page_url,
bool has_user_gesture,
bool form_in_main_frame) {
message_received_ = true;
message_ = message.Clone();
return true;
}
net::EmbeddedTestServer server_;
base::Value message_;
bool message_received_ = false;
DISALLOW_COPY_AND_ASSIGN(ImageFetchJsTest);
};
// Tests that __gCrWeb.imageFetch.getImageData works when the image is
// same-domain.
TEST_F(ImageFetchJsTest, TestGetSameDomainImageData) {
const GURL image_url = server_.GetURL("/image");
const GURL page_url = server_.GetURL("/");
LoadHtmlAndInject([NSString stringWithFormat:@"<html><img src='%s'></html>",
image_url.spec().c_str()],
page_url);
ExecuteJavaScriptWithFormat(@"__gCrWeb.imageFetch.getImageData(%d, '%s');",
kCallJavaScriptId, image_url.spec().c_str());
ASSERT_TRUE(WaitUntilConditionOrTimeout(kWaitForJSCompletionTimeout, ^{
return message_received_;
}));
ASSERT_TRUE(message_.is_dict());
const base::Value* id_key = message_.FindKey("id");
ASSERT_TRUE(id_key);
ASSERT_TRUE(id_key->is_double());
EXPECT_EQ(kCallJavaScriptId, static_cast<int>(id_key->GetDouble()));
// The fetched image data may not equal to original base64 data because of
// recompression in JavaScript. Therefore decode the returned image data and
// check if it can be rendered to an image.
const base::Value* data = message_.FindKey("data");
ASSERT_TRUE(data);
ASSERT_TRUE(data->is_string());
ASSERT_FALSE(data->GetString().empty());
std::string decoded_data;
ASSERT_TRUE(base::Base64Decode(data->GetString(), &decoded_data));
UIImage* image =
[UIImage imageWithData:[NSData dataWithBytes:decoded_data.c_str()
length:decoded_data.size()]];
EXPECT_TRUE(image);
}
// Tests that __gCrWeb.imageFetch.getImageData works when the image is
// cross-domain.
TEST_F(ImageFetchJsTest, TestGetCrossDomainImageData) {
const GURL image_url = server_.GetURL("/image");
// WebTestWithWebState::LoadHtml uses an HTTPS url for webpage as default. Use
// an HTTP url instead, because XMLHttpRequest with HTTP url sent from HTTPS
// website is forbidden due to the CORS policy.
const GURL page_url("http://chrooooome.com");
LoadHtmlAndInject([NSString stringWithFormat:@"<html><img src='%s'></html>",
image_url.spec().c_str()],
page_url);
ExecuteJavaScriptWithFormat(@"__gCrWeb.imageFetch.getImageData(%d, '%s');",
kCallJavaScriptId, image_url.spec().c_str());
ASSERT_TRUE(WaitUntilConditionOrTimeout(kWaitForJSCompletionTimeout, ^{
return message_received_;
}));
ASSERT_TRUE(message_.is_dict());
const base::Value* id_key = message_.FindKey("id");
ASSERT_TRUE(id_key);
ASSERT_TRUE(id_key->is_double());
EXPECT_EQ(kCallJavaScriptId, static_cast<int>(id_key->GetDouble()));
const base::Value* data = message_.FindKey("data");
ASSERT_TRUE(data);
ASSERT_TRUE(data->is_string());
EXPECT_EQ(kImageBase64, data->GetString());
}
// Tests that __gCrWeb.imageFetch.getImageData fails for timeout when the image
// response is delayed. In this test the image must be cross-domain, otherwise
// image data will be fetched from <img> by <canvas> directly.
TEST_F(ImageFetchJsTest, TestGetDelayedImageData) {
const GURL image_url = server_.GetURL("/image_delayed");
const GURL page_url("http://chrooooome.com");
LoadHtmlAndInject([NSString stringWithFormat:@"<html><img src='%s'></html>",
image_url.spec().c_str()],
page_url);
ExecuteJavaScriptWithFormat(@"__gCrWeb.imageFetch.getImageData(%d, '%s');",
kCallJavaScriptId, image_url.spec().c_str());
ASSERT_TRUE(WaitUntilConditionOrTimeout(kWaitForJSCompletionTimeout, ^{
return message_received_;
}));
ASSERT_TRUE(message_.is_dict());
const base::Value* id_key = message_.FindKey("id");
ASSERT_TRUE(id_key);
ASSERT_TRUE(id_key->is_double());
EXPECT_EQ(kCallJavaScriptId, static_cast<int>(id_key->GetDouble()));
EXPECT_FALSE(message_.FindKey("data"));
}
// Copyright 2018 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.
#ifndef IOS_CHROME_BROWSER_WEB_IMAGE_FETCH_TAB_HELPER_H_
#define IOS_CHROME_BROWSER_WEB_IMAGE_FETCH_TAB_HELPER_H_
#include <unordered_map>
#include "base/macros.h"
#include "ios/web/public/web_state/web_state_observer.h"
#import "ios/web/public/web_state/web_state_user_data.h"
// Gets the image data in binary string by calling injected JavaScript.
// Never keep a reference to this class, instead get it by
// ImageFetchTabHelper::FromWebState everytime.
class ImageFetchTabHelper : public web::WebStateObserver,
public web::WebStateUserData<ImageFetchTabHelper> {
public:
~ImageFetchTabHelper() override;
// Callback for GetImageData. |data| will be in binary format, or nullptr if
// GetImageData failed.
typedef base::OnceCallback<void(const std::string* data)> ImageDataCallback;
// Gets image data as binary string by trying 2 methods in following order:
// 1. Draw <img> to <canvas> and export its data;
// 2. Download the image by XMLHttpRequest and hopefully get responsed from
// cache.
// This method should only be called from UI thread, and |url| should be equal
// to the resolved "src" attribute of <img>, otherwise the method 1 would
// fail. |callback| will be called on UI thread.
void GetImageData(const GURL& url, ImageDataCallback&& callback);
private:
friend class web::WebStateUserData<ImageFetchTabHelper>;
explicit ImageFetchTabHelper(web::WebState* web_state);
// web::WebStateObserver overrides:
void DidStartNavigation(web::WebState* web_state,
web::NavigationContext* navigation_context) override;
void WebStateDestroyed(web::WebState* web_state) override;
// Handler for messages sent back from injected JavaScript.
bool OnImageDataReceived(const base::DictionaryValue& message,
const GURL& page_url,
bool has_user_gesture,
bool form_in_main_frame);
// WebState this tab helper is attached to.
web::WebState* web_state_ = nullptr;
// Store callbacks for GetImageData, with url as key.
std::unordered_map<int, ImageDataCallback> callbacks_;
// |GetImageData| uses this counter as ID to match calls with callbacks. Each
// call on |GetImageData| will increment |call_id_| by 1 and pass it as ID
// when calling JavaScript. The ID will be regained in the message received in
// |OnImageDataReceived| and used to invoke the corresponding callback.
int call_id_ = 0;
DISALLOW_COPY_AND_ASSIGN(ImageFetchTabHelper);
};
#endif // IOS_CHROME_BROWSER_WEB_IMAGE_FETCH_TAB_HELPER_H_
// Copyright 2018 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.
#import "ios/chrome/browser/web/image_fetch_tab_helper.h"
#include "base/base64.h"
#include "base/strings/stringprintf.h"
#include "base/strings/utf_string_conversions.h"
#include "base/values.h"
#import "ios/web/public/web_state/navigation_context.h"
#if !defined(__has_feature) || !__has_feature(objc_arc)
#error "This file requires ARC support."
#endif
DEFINE_WEB_STATE_USER_DATA_KEY(ImageFetchTabHelper);
namespace {
// Command prefix for injected JavaScript.
const char kCommandPrefix[] = "imageFetch";
}
ImageFetchTabHelper::ImageFetchTabHelper(web::WebState* web_state)
: web_state_(web_state) {
web_state->AddObserver(this);
web_state->AddScriptCommandCallback(
base::BindRepeating(&ImageFetchTabHelper::OnImageDataReceived,
base::Unretained(this)),
kCommandPrefix);
}
ImageFetchTabHelper::~ImageFetchTabHelper() = default;
void ImageFetchTabHelper::DidStartNavigation(
web::WebState* web_state,
web::NavigationContext* navigation_context) {
if (navigation_context->IsSameDocument()) {
return;
}
for (auto&& pair : callbacks_)
std::move(pair.second).Run(nullptr);
callbacks_.clear();
}
void ImageFetchTabHelper::WebStateDestroyed(web::WebState* web_state) {
web_state->RemoveScriptCommandCallback(kCommandPrefix);
for (auto&& pair : callbacks_)
std::move(pair.second).Run(nullptr);
web_state->RemoveObserver(this);
web_state_ = nullptr;
}
void ImageFetchTabHelper::GetImageData(const GURL& url,
ImageDataCallback&& callback) {
++call_id_;
DCHECK_EQ(callbacks_.count(call_id_), 0UL);
callbacks_.insert({call_id_, std::move(callback)});
std::string js =
base::StringPrintf("__gCrWeb.imageFetch.getImageData(%d, '%s')", call_id_,
url.spec().c_str());
// TODO(crbug.com/163201): Add timeout for callback in case JavaScript does
// not return.
web_state_->ExecuteJavaScript(base::UTF8ToUTF16(js));
}
// The expected message from JavaScript has format:
//
// For success:
// {'command': 'image.getImageData',
// 'id': id_sent_to_gCrWeb_image_getImageData,
// 'data': image_data_in_base64}
//
// For failure:
// {'command': 'image.getImageData',
// 'id': id_sent_to_gCrWeb_image_getImageData}
bool ImageFetchTabHelper::OnImageDataReceived(
const base::DictionaryValue& message,
const GURL& page_url,
bool has_user_gesture,
bool form_in_main_frame) {
const base::Value* id_key = message.FindKey("id");
if (!id_key || !id_key->is_double()) {
return false;
}
int id_value = static_cast<int>(id_key->GetDouble());
if (!callbacks_.count(id_value)) {
return false;
}
ImageDataCallback callback = std::move(callbacks_[id_value]);
callbacks_.erase(id_value);
const base::Value* data = message.FindKey("data");
std::string decoded_data;
if (data && data->is_string() && !data->GetString().empty() &&
base::Base64Decode(data->GetString(), &decoded_data)) {
std::move(callback).Run(&decoded_data);
} else {
std::move(callback).Run(nullptr);
}
return true;
}
// Copyright 2018 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.
#import "ios/chrome/browser/web/image_fetch_tab_helper.h"
#import "base/test/ios/wait_util.h"
#import "ios/web/public/test/web_test_with_web_state.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "testing/gtest_mac.h"
#if !defined(__has_feature) || !__has_feature(objc_arc)
#error "This file requires ARC support."
#endif
using base::test::ios::WaitUntilConditionOrTimeout;
using base::test::ios::kWaitForJSCompletionTimeout;
// Test fixture for ImageFetchTabHelper class.
class ImageFetchTabHelperTest : public web::WebTestWithWebState {
public:
void OnImageData(const std::string* data) {
if (data) {
image_data_ = std::make_unique<std::string>(*data);
}
on_image_data_called_ = true;
}
protected:
ImageFetchTabHelperTest() = default;
void SetUp() override {
WebTestWithWebState::SetUp();
ASSERT_TRUE(LoadHtml("<html></html>"));
ImageFetchTabHelper::CreateForWebState(web_state());
}
ImageFetchTabHelper* image_fetch_tab_helper() {
return ImageFetchTabHelper::FromWebState(web_state());
}
bool on_image_data_called_ = false;
std::unique_ptr<std::string> image_data_;
DISALLOW_COPY_AND_ASSIGN(ImageFetchTabHelperTest);
};
// Tests that ImageFetchTabHelper::GetImageData returns image data in callback
// when fetching image data succeeded.
TEST_F(ImageFetchTabHelperTest, GetImageDataJsSucceed) {
// Injects fake |__gCrWeb.imageFetch.getImageData| that returns "abc" in
// base64 as image data.
id script_result = ExecuteJavaScript(
@"__gCrWeb.imageFetch = {}; __gCrWeb.imageFetch.getImageData = "
@"function(id, url)"
"{ __gCrWeb.message.invokeOnHost({'command': 'imageFetch.getImageData', "
"'id': id, 'data': btoa('abc')}); }; true;");
ASSERT_NSEQ(@YES, script_result);
image_fetch_tab_helper()->GetImageData(
GURL("http://a.com/"),
base::BindOnce(&ImageFetchTabHelperTest::OnImageData,
base::Unretained(this)));
ASSERT_TRUE(WaitUntilConditionOrTimeout(kWaitForJSCompletionTimeout, ^{
return on_image_data_called_;
}));
ASSERT_TRUE(image_data_);
EXPECT_EQ("abc", *image_data_);
}
// Tests that ImageFetchTabHelper::GetImageData returns nullptr in callback when
// fetching image data failed.
TEST_F(ImageFetchTabHelperTest, GetImageDataJsFail) {
id script_result = ExecuteJavaScript(
@"__gCrWeb.imageFetch = {}; __gCrWeb.imageFetch.getImageData = "
@"function(id, url)"
"{ __gCrWeb.message.invokeOnHost({'command': 'imageFetch.getImageData', "
"'id': id}); }; true;");
ASSERT_NSEQ(@YES, script_result);
image_fetch_tab_helper()->GetImageData(
GURL("http://a.com/"),
base::BindOnce(&ImageFetchTabHelperTest::OnImageData,
base::Unretained(this)));
ASSERT_TRUE(WaitUntilConditionOrTimeout(kWaitForJSCompletionTimeout, ^{
return on_image_data_called_;
}));
EXPECT_FALSE(image_data_);
}
// Tests that ImageFetchTabHelper::GetImageData returns nullptr in callback when
// WebState is destroyed.
TEST_F(ImageFetchTabHelperTest, GetImageDataWebStateDestroy) {
image_fetch_tab_helper()->GetImageData(
GURL("http://a.com/"),
base::BindOnce(&ImageFetchTabHelperTest::OnImageData,
base::Unretained(this)));
DestroyWebState();
ASSERT_TRUE(WaitUntilConditionOrTimeout(kWaitForJSCompletionTimeout, ^{
return on_image_data_called_;
}));
EXPECT_FALSE(image_data_);
}
// Tests that ImageFetchTabHelper::GetImageData returns nullptr in callback when
// WebState navigates to a new web page.
TEST_F(ImageFetchTabHelperTest, GetImageDataWebStateNavigate) {
image_fetch_tab_helper()->GetImageData(
GURL("http://a.com/"),
base::BindOnce(&ImageFetchTabHelperTest::OnImageData,
base::Unretained(this)));
LoadHtml(@"<html>new</html>"), GURL("http://new.webpage.com/");
ASSERT_TRUE(WaitUntilConditionOrTimeout(kWaitForJSCompletionTimeout, ^{
return on_image_data_called_;
}));
EXPECT_FALSE(image_data_);
}
......@@ -9,3 +9,4 @@ goog.require('__crWeb.autofill');
goog.require('__crWeb.fill');
goog.require('__crWeb.passwords');
goog.require('__crWeb.print');
goog.require('__crWeb.imageFetch');
// Copyright 2018 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 Add functionality related to getting image data.
*/
goog.provide('__crWeb.imageFetch');
/**
* Namespace for this file. It depends on |__gCrWeb| having already been
* injected.
*/
__gCrWeb.imageFetch = {};
/**
* Store common namespace object in a global __gCrWeb object referenced by a
* string, so it does not get renamed by closure compiler during the
* minification.
*/
__gCrWeb['imageFetch'] = __gCrWeb.imageFetch;
/* Beginning of anonymous object. */
(function() {
/**
* Returns image data as base64 string, because WKWebView does not support BLOB
* on messages to native code. Try getting data directly from <img> first, and
* if failed try downloading by XMLHttpRequest.
*
* @param {number} id The ID for curent call. It should be attached to the
* message sent back.
* @param {string} url The URL of the requested image.
*/
__gCrWeb.imageFetch.getImageData = function(id, url) {
var onData = function(data) {
__gCrWeb.message.invokeOnHost(
{'command': 'imageFetch.getImageData', 'id': id, 'data': data});
};
var onError = function() {
__gCrWeb.message.invokeOnHost(
{'command': 'imageFetch.getImageData', 'id': id});
};
var data = getImageDataByCanvas(url);
if (data) {
onData(data);
} else {
getImageDataByXMLHttpRequest(url, 100, onData, onError);
}
};
/**
* Returns image data directly from <img> by drawing it to <canvas> and export
* it. If the <img> is cross-origin without "crossorigin=anonymous", this would
* be prevented by the browser. The exported image is in a resolution of 96 dpi.
*
* @param {string} url The URL of the requested image.
* @return {string|null} Image data in base64 string, or null if no <img> with
* "src=|url|" is found or exporting data from <img> failed.
*/
function getImageDataByCanvas(url) {
for (var key in document.images) {
var img = document.images[key];
if (img.src == url) {
var canvas = document.createElement('canvas');
canvas.width = img.naturalWidth;
canvas.height = img.naturalHeight;
var ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0);
var data;
try {
// If the <img> is cross-domain without "crossorigin=anonymous", an
// exception will be thrown.
data = canvas.toDataURL('image/png');
} catch (error) {
return null;
}
// Remove the "data:type/subtype;base64," header.
return data.split(',')[1];
}
}
return null;
};
/**
* Returns image data by downloading it using XMLHttpRequest.
*
* @param {string} url The URL of the requested image.
* @param {number} timeout The timeout in milliseconds for XMLHttpRequest.
* @param {Function} onData Callback when fetching image data succeeded.
* @param {Function} onError Callback when fetching image data failed.
*/
function getImageDataByXMLHttpRequest(url, timeout, onData, onError) {
var xhr = new XMLHttpRequest();
xhr.open('GET', url);
xhr.timeout = timeout;
xhr.responseType = 'blob';
xhr.onload = function() {
if (xhr.status != 200) {
onError();
return;
}
var fr = new FileReader();
fr.onload = function() {
onData(btoa(/** @type{string} */ (fr.result)));
};
fr.onabort = onError;
fr.onerror = onError;
fr.readAsBinaryString(/** @type{!Blob} */ (xhr.response));
};
xhr.onabort = onError;
xhr.onerror = onError;
xhr.ontimeout = onError;
xhr.send();
};
}()); // End of anonymous object
......@@ -30,6 +30,10 @@ class WebJsTest : public WebTestT {
WebTestT::LoadHtml(html);
Inject();
}
void LoadHtmlAndInject(NSString* html, const GURL& url) {
WebTestT::LoadHtml(html, url);
Inject();
}
// Returns an id representation of the JavaScript's evaluation results;
// the JavaScript is passed in as a |format| and its arguments.
......
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