// Copyright (c) 2011 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. #include "chrome/renderer/chrome_render_observer.h" #include "base/command_line.h" #include "base/metrics/histogram.h" #include "chrome/common/chrome_switches.h" #include "chrome/common/render_messages.h" #include "chrome/common/thumbnail_score.h" #include "chrome/renderer/safe_browsing/phishing_classifier_delegate.h" #include "chrome/renderer/translate_helper.h" #include "content/renderer/content_renderer_client.h" #include "content/renderer/render_view.h" #include "skia/ext/bitmap_platform_device.h" #include "skia/ext/image_operations.h" #include "third_party/WebKit/Source/WebKit/chromium/public/WebDataSource.h" #include "third_party/WebKit/Source/WebKit/chromium/public/WebFrame.h" #include "third_party/WebKit/Source/WebKit/chromium/public/WebRect.h" #include "third_party/WebKit/Source/WebKit/chromium/public/WebSize.h" #include "third_party/WebKit/Source/WebKit/chromium/public/WebView.h" #include "ui/gfx/color_utils.h" #include "ui/gfx/skbitmap_operations.h" #include "webkit/glue/webkit_glue.h" using WebKit::WebDataSource; using WebKit::WebFrame; using WebKit::WebRect; using WebKit::WebSize; using WebKit::WebView; // Delay in milliseconds that we'll wait before capturing the page contents // and thumbnail. static const int kDelayForCaptureMs = 500; // Typically, we capture the page data once the page is loaded. // Sometimes, the page never finishes to load, preventing the page capture // To workaround this problem, we always perform a capture after the following // delay. static const int kDelayForForcedCaptureMs = 6000; // define to write the time necessary for thumbnail/DOM text retrieval, // respectively, into the system debug log // #define TIME_TEXT_RETRIEVAL // maximum number of characters in the document to index, any text beyond this // point will be clipped static const size_t kMaxIndexChars = 65535; // Size of the thumbnails that we'll generate static const int kThumbnailWidth = 212; static const int kThumbnailHeight = 132; static bool PaintViewIntoCanvas(WebView* view, skia::PlatformCanvas& canvas) { view->layout(); const WebSize& size = view->size(); if (!canvas.initialize(size.width, size.height, true)) return false; view->paint(webkit_glue::ToWebCanvas(&canvas), WebRect(0, 0, size.width, size.height)); // TODO: Add a way to snapshot the whole page, not just the currently // visible part. return true; } // Calculates how "boring" a thumbnail is. The boring score is the // 0,1 ranged percentage of pixels that are the most common // luma. Higher boring scores indicate that a higher percentage of a // bitmap are all the same brightness. static double CalculateBoringScore(SkBitmap* bitmap) { int histogram[256] = {0}; color_utils::BuildLumaHistogram(bitmap, histogram); int color_count = *std::max_element(histogram, histogram + 256); int pixel_count = bitmap->width() * bitmap->height(); return static_cast(color_count) / pixel_count; } ChromeRenderObserver::ChromeRenderObserver( RenderView* render_view, TranslateHelper* translate_helper, safe_browsing::PhishingClassifierDelegate* phishing_classifier) : RenderViewObserver(render_view), translate_helper_(translate_helper), phishing_classifier_(phishing_classifier), last_indexed_page_id_(-1), ALLOW_THIS_IN_INITIALIZER_LIST(page_info_method_factory_(this)) { } ChromeRenderObserver::~ChromeRenderObserver() { } bool ChromeRenderObserver::OnMessageReceived(const IPC::Message& message) { bool handled = true; IPC_BEGIN_MESSAGE_MAP(ChromeRenderObserver, message) IPC_MESSAGE_HANDLER(ViewMsg_CaptureSnapshot, OnCaptureSnapshot) IPC_MESSAGE_UNHANDLED(handled = false) IPC_END_MESSAGE_MAP() return handled; } void ChromeRenderObserver::OnCaptureSnapshot() { SkBitmap snapshot; bool error = false; WebFrame* main_frame = render_view()->webview()->mainFrame(); if (!main_frame) error = true; if (!error && !CaptureSnapshot(render_view()->webview(), &snapshot)) error = true; DCHECK(error == snapshot.empty()) << "Snapshot should be empty on error, non-empty otherwise."; // Send the snapshot to the browser process. Send(new ViewHostMsg_Snapshot(routing_id(), snapshot)); } void ChromeRenderObserver::DidStopLoading() { MessageLoop::current()->PostDelayedTask( FROM_HERE, page_info_method_factory_.NewRunnableMethod( &ChromeRenderObserver::CapturePageInfo, render_view()->page_id(), false), render_view()->content_state_immediately() ? 0 : kDelayForCaptureMs); } void ChromeRenderObserver::DidCommitProvisionalLoad(WebFrame* frame, bool is_new_navigation) { if (!is_new_navigation) return; MessageLoop::current()->PostDelayedTask( FROM_HERE, page_info_method_factory_.NewRunnableMethod( &ChromeRenderObserver::CapturePageInfo, render_view()->page_id(), true), kDelayForForcedCaptureMs); } void ChromeRenderObserver::CapturePageInfo(int load_id, bool preliminary_capture) { if (load_id != render_view()->page_id()) return; // This capture call is no longer relevant due to navigation. if (load_id == last_indexed_page_id_) return; // we already indexed this page if (!render_view()->webview()) return; WebFrame* main_frame = render_view()->webview()->mainFrame(); if (!main_frame) return; // Don't index/capture pages that are in view source mode. if (main_frame->isViewSourceModeEnabled()) return; // Don't index/capture pages that failed to load. This only checks the top // level frame so the thumbnail may contain a frame that failed to load. WebDataSource* ds = main_frame->dataSource(); if (ds && ds->hasUnreachableURL()) return; if (!preliminary_capture) last_indexed_page_id_ = load_id; // Get the URL for this page. GURL url(main_frame->url()); if (url.is_empty()) return; // Retrieve the frame's full text. string16 contents; CaptureText(main_frame, &contents); if (contents.size()) { if (translate_helper_) translate_helper_->PageCaptured(contents); // Send the text to the browser for indexing (the browser might decide not // to index, if the URL is HTTPS for instance) and language discovery. Send(new ViewHostMsg_PageContents(routing_id(), url, load_id, contents)); } // Generate the thumbnail here if the in-browser thumbnailing isn't // enabled. TODO(satorux): Remove this and related code once // crbug.com/65936 is complete. if (!CommandLine::ForCurrentProcess()->HasSwitch( switches::kEnableInBrowserThumbnailing)) { CaptureThumbnail(); } // Will swap out the string. if (phishing_classifier_) phishing_classifier_->PageCaptured(&contents, preliminary_capture); } void ChromeRenderObserver::CaptureText(WebFrame* frame, string16* contents) { contents->clear(); if (!frame) return; #ifdef TIME_TEXT_RETRIEVAL double begin = time_util::GetHighResolutionTimeNow(); #endif // get the contents of the frame *contents = frame->contentAsText(kMaxIndexChars); #ifdef TIME_TEXT_RETRIEVAL double end = time_util::GetHighResolutionTimeNow(); char buf[128]; sprintf_s(buf, "%d chars retrieved for indexing in %gms\n", contents.size(), (end - begin)*1000); OutputDebugStringA(buf); #endif // When the contents are clipped to the maximum, we don't want to have a // partial word indexed at the end that might have been clipped. Therefore, // terminate the string at the last space to ensure no words are clipped. if (contents->size() == kMaxIndexChars) { size_t last_space_index = contents->find_last_of(kWhitespaceUTF16); if (last_space_index == std::wstring::npos) return; // don't index if we got a huge block of text with no spaces contents->resize(last_space_index); } } void ChromeRenderObserver::CaptureThumbnail() { WebFrame* main_frame = render_view()->webview()->mainFrame(); if (!main_frame) return; // get the URL for this page GURL url(main_frame->url()); if (url.is_empty()) return; if (render_view()->size().IsEmpty()) return; // Don't create an empty thumbnail! ThumbnailScore score; SkBitmap thumbnail; if (!CaptureFrameThumbnail(render_view()->webview(), kThumbnailWidth, kThumbnailHeight, &thumbnail, &score)) return; // send the thumbnail message to the browser process Send(new ViewHostMsg_Thumbnail(routing_id(), url, score, thumbnail)); } bool ChromeRenderObserver::CaptureFrameThumbnail(WebView* view, int w, int h, SkBitmap* thumbnail, ThumbnailScore* score) { base::TimeTicks beginning_time = base::TimeTicks::Now(); skia::PlatformCanvas canvas; // Paint |view| into |canvas|. if (!PaintViewIntoCanvas(view, canvas)) return false; skia::BitmapPlatformDevice& device = static_cast(canvas.getTopPlatformDevice()); const SkBitmap& src_bmp = device.accessBitmap(false); SkRect dest_rect = { 0, 0, SkIntToScalar(w), SkIntToScalar(h) }; float dest_aspect = dest_rect.width() / dest_rect.height(); // Get the src rect so that we can preserve the aspect ratio while filling // the destination. SkIRect src_rect; if (src_bmp.width() < dest_rect.width() || src_bmp.height() < dest_rect.height()) { // Source image is smaller: we clip the part of source image within the // dest rect, and then stretch it to fill the dest rect. We don't respect // the aspect ratio in this case. src_rect.set(0, 0, static_cast(dest_rect.width()), static_cast(dest_rect.height())); score->good_clipping = false; } else { float src_aspect = static_cast(src_bmp.width()) / src_bmp.height(); if (src_aspect > dest_aspect) { // Wider than tall, clip horizontally: we center the smaller thumbnail in // the wider screen. S16CPU new_width = static_cast(src_bmp.height() * dest_aspect); S16CPU x_offset = (src_bmp.width() - new_width) / 2; src_rect.set(x_offset, 0, new_width + x_offset, src_bmp.height()); score->good_clipping = false; } else { src_rect.set(0, 0, src_bmp.width(), static_cast(src_bmp.width() / dest_aspect)); score->good_clipping = true; } } score->at_top = (view->mainFrame()->scrollOffset().height == 0); SkBitmap subset; device.accessBitmap(false).extractSubset(&subset, src_rect); // First do a fast downsample by powers of two to get close to the final size. SkBitmap downsampled_subset = SkBitmapOperations::DownsampleByTwoUntilSize(subset, w, h); // Do a high-quality resize from the downscaled size to the final size. *thumbnail = skia::ImageOperations::Resize( downsampled_subset, skia::ImageOperations::RESIZE_LANCZOS3, w, h); score->boring_score = CalculateBoringScore(thumbnail); HISTOGRAM_TIMES("Renderer4.Thumbnail", base::TimeTicks::Now() - beginning_time); return true; } bool ChromeRenderObserver::CaptureSnapshot(WebView* view, SkBitmap* snapshot) { base::TimeTicks beginning_time = base::TimeTicks::Now(); skia::PlatformCanvas canvas; if (!PaintViewIntoCanvas(view, canvas)) return false; skia::BitmapPlatformDevice& device = static_cast(canvas.getTopPlatformDevice()); const SkBitmap& bitmap = device.accessBitmap(false); if (!bitmap.copyTo(snapshot, SkBitmap::kARGB_8888_Config)) return false; HISTOGRAM_TIMES("Renderer4.Snapshot", base::TimeTicks::Now() - beginning_time); return true; }