blob: 5bbd093ecea7b4a48838a51f13fbb85c6bbfa67f [file] [log] [blame]
// Copyright 2021 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/browser/icon_transcoder/svg_icon_transcoder.h"
#include "base/base64.h"
#include "base/files/file_util.h"
#include "base/task/thread_pool.h"
#include "chrome/browser/profiles/profile.h"
#include "content/public/browser/web_contents.h"
#include "third_party/skia/include/core/SkBitmap.h"
#include "ui/gfx/codec/png_codec.h"
namespace apps {
namespace {
constexpr char kSvgDataUrlPrefix[] = "data:image/svg+xml;base64,";
std::string ReadSvgOnFileThread(base::FilePath svg_path) {
std::string svg_data;
if (base::PathExists(svg_path)) {
base::ReadFileToString(svg_path, &svg_data);
LOG_IF(ERROR, svg_data.empty()) << "Empty svg data at path " << svg_path;
}
return svg_data;
}
void SaveIconOnFileThread(const base::FilePath& icon_path,
const std::string& content) {
DCHECK(!content.empty());
base::File::Error file_error;
if (!base::CreateDirectoryAndGetError(icon_path.DirName(), &file_error)) {
LOG(ERROR) << "Failed to create dir " << icon_path.DirName()
<< " with error " << file_error;
return;
}
if (!base::WriteFile(icon_path, content)) {
LOG(ERROR) << "Failed to write icon file: " << icon_path;
if (!base::DeleteFile(icon_path)) {
LOG(ERROR) << "Couldn't delete broken icon file" << icon_path;
}
}
}
} // namespace
SvgIconTranscoder::SvgIconTranscoder(Profile* profile) : profile_(profile) {}
SvgIconTranscoder::~SvgIconTranscoder() {
RemoveObserver();
}
// Reads the svg data at svg_path and invokes the string Transcode method.
// |callback| is invoked with and empty string on failure. Blocking call.
void SvgIconTranscoder::Transcode(const base::FilePath&& svg_path,
const base::FilePath&& png_path,
gfx::Size preferred_size,
IconContentCallback callback) {
base::ThreadPool::PostTaskAndReplyWithResult(
FROM_HERE, {base::MayBlock(), base::TaskPriority::BEST_EFFORT},
base::BindOnce(&ReadSvgOnFileThread, std::move(svg_path)),
base::BindOnce(
[](base::WeakPtr<SvgIconTranscoder> weak_this,
const base::FilePath&& png_path, gfx::Size preferred_size,
IconContentCallback callback, std::string svg_data) {
if (weak_this && !svg_data.empty()) {
weak_this->Transcode(std::move(svg_data), std::move(png_path),
preferred_size, std::move(callback));
return;
}
std::move(callback).Run(std::string());
},
GetWeakPtr(), std::move(png_path), preferred_size,
std::move(callback)));
}
// Validates and trims the svg_data before base64 encoding and dispatching to
// |web_contents_| in a data: URI. |callback| is invoked with and empty
// string on failure. Blocking call.
void SvgIconTranscoder::Transcode(const std::string& svg_data,
const base::FilePath&& png_path,
gfx::Size preferred_size,
IconContentCallback callback) {
if (!PrepareWebContents()) {
LOG(ERROR) << "Can't transcode svg. WebContents not ready.";
std::move(callback).Run(std::string());
return;
}
auto pos = svg_data.find("<svg");
if (pos == std::string::npos) {
LOG(ERROR) << "Invalid data. Couldn't find <svg.";
std::move(callback).Run(std::string());
return;
}
// Form a data: uri from the svg_data starting at the <svg. Excess ASCII
// whitespace is also removed.
std::string base64_svg;
base::Base64Encode(base::CollapseWhitespaceASCII(svg_data.substr(pos), false),
&base64_svg);
GURL data_url(kSvgDataUrlPrefix + base64_svg);
web_contents_->DownloadImage(
data_url, /*is_favicon=*/false, preferred_size, /*max_bitmap_size=*/0,
/*bypass_cache=*/true,
base::BindOnce(&SvgIconTranscoder::OnDownloadImage, GetWeakPtr(),
std::move(png_path), std::move(callback)));
}
void SvgIconTranscoder::MaybeCreateWebContents() {
if (!web_contents_) {
auto params = content::WebContents::CreateParams(profile_);
params.initially_hidden = true;
params.desired_renderer_state =
content::WebContents::CreateParams::kInitializeAndWarmupRendererProcess;
web_contents_ = content::WebContents::Create(params);
// When we observe RenderProcessExited, we will need to recreate.
web_contents_->GetPrimaryMainFrame()->GetProcess()->AddObserver(this);
}
}
bool SvgIconTranscoder::PrepareWebContents() {
if (!web_contents_ready_) {
// Old web_contents_ may have been destroyed.
MaybeCreateWebContents();
if (web_contents_->GetPrimaryMainFrame()->IsRenderFrameLive()) {
web_contents_ready_ = true;
}
VLOG(1) << "web_contents "
<< (web_contents_ready_ ? "ready " : "not ready");
}
return web_contents_ready_;
}
void SvgIconTranscoder::RenderProcessReady(content::RenderProcessHost* host) {
web_contents_ready_ = true;
}
void SvgIconTranscoder::RenderProcessExited(
content::RenderProcessHost* host,
const content::ChildProcessTerminationInfo& info) {
web_contents_ready_ = false;
RemoveObserver();
web_contents_.reset();
}
void SvgIconTranscoder::RemoveObserver() {
if (web_contents_ && web_contents_->GetPrimaryMainFrame()) {
web_contents_->GetPrimaryMainFrame()->GetProcess()->RemoveObserver(this);
}
}
// Compresses the first received bitmap and saves compressed data to
// |png_path| if non-empty. If the file can't be saved, that's not considered
// and error. Next time lucky.
void SvgIconTranscoder::OnDownloadImage(base::FilePath png_path,
IconContentCallback callback,
int id,
int http_status_code,
const GURL& image_url,
const std::vector<SkBitmap>& bitmaps,
const std::vector<gfx::Size>& sizes) {
if (bitmaps.empty()) {
VLOG(1) << "status " << http_status_code << " for download id " << id;
VLOG(1) << "Failed to download image from " << image_url;
std::move(callback).Run(std::string());
return;
}
const SkBitmap& bitmap = bitmaps[0];
base::ThreadPool::PostTaskAndReplyWithResult(
FROM_HERE, {base::MayBlock(), base::TaskPriority::BEST_EFFORT},
base::BindOnce(
[](const SkBitmap& bitmap) {
std::vector<unsigned char> compressed;
if (!bitmap.empty() &&
gfx::PNGCodec::EncodeBGRASkBitmap(bitmap, false, &compressed)) {
return std::string(compressed.begin(), compressed.end());
}
return std::string();
},
bitmap),
base::BindOnce(
[](base::FilePath png_path, IconContentCallback callback,
std::string compressed) {
if (!compressed.empty() && !png_path.empty()) {
base::ThreadPool::PostTaskAndReply(
FROM_HERE,
{base::MayBlock(), base::TaskPriority::BEST_EFFORT},
base::BindOnce(&SaveIconOnFileThread, std::move(png_path),
compressed),
base::BindOnce(std::move(callback), compressed));
} else {
std::move(callback).Run(std::move(compressed));
}
},
std::move(png_path), std::move(callback)));
}
} // namespace apps