blob: d1f6a026f34d7c4975fe809e6aafbfe594521ea4 [file] [log] [blame]
// Copyright 2024 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "components/services/font_data/font_data_service_impl.h"
#if BUILDFLAG(IS_WIN)
#include <windows.h>
#endif // BUILDFLAG(IS_WIN)
#include <algorithm>
#include <utility>
#include "base/check.h"
#include "base/containers/heap_array.h"
#include "base/debug/dump_without_crashing.h"
#include "base/feature_list.h"
#include "base/metrics/histogram_functions.h"
#include "base/metrics/histogram_macros.h"
#include "base/no_destructor.h"
#include "base/notreached.h"
#include "base/task/thread_pool.h"
#include "base/trace_event/trace_event.h"
#include "skia/ext/font_utils.h"
#include "third_party/skia/include/core/SkFontStyle.h"
#include "third_party/skia/include/core/SkStream.h"
#include "third_party/skia/include/core/SkString.h"
#include "third_party/skia/include/core/SkTypeface.h"
namespace font_data_service {
namespace {
// Recorded in Chrome.FontDataService.CreateResult, don't modify/reorder without
// also changing FontDataServiceCreateResult in
// tools/metrics/histograms/metadata/chrome/enums.xml
enum class CreateResult {
kNoTypeface = 0,
kSuccessExistingSharedMemory = 1,
kFailureExistingSharedMemory = 2,
kSuccessSharingFileHandle = 3,
kSuccessSharingNewMemoryRegion = 4,
kFailureSharingNewMemoryRegion = 5,
kMaxValue = kFailureSharingNewMemoryRegion,
};
// Recorded in Chrome.FontDataService.InvokedIPC, don't modify or re-order
// without also changing FontDataServiceIPC.
enum class FontDataServiceIPC {
kMatchFamilyName = 0,
kMatchFamilyNameCharacter = 1,
kGetAllFamilyNames = 2,
kLegacyMakeTypeface = 3,
kMaxValue = kLegacyMakeTypeface,
};
// Value is arbitrary. The number should be small to conserve memory but large
// enough to fit a meaningful amount of fonts.
constexpr int kMemoryMapCacheSize = 128;
BASE_FEATURE(kDumpOnOOBFontDataServiceCache, base::FEATURE_DISABLED_BY_DEFAULT);
base::SequencedTaskRunner* GetFontDataServiceTaskRunner() {
static base::NoDestructor<scoped_refptr<base::SequencedTaskRunner>>
task_runner{base::ThreadPool::CreateSequencedTaskRunner(
{base::MayBlock(), base::TaskPriority::USER_BLOCKING})};
return task_runner->get();
}
void BindToFontService(
mojo::PendingReceiver<font_data_service::mojom::FontDataService> receiver) {
static base::NoDestructor<font_data_service::FontDataServiceImpl> service;
service->BindReceiver(std::move(receiver));
}
constexpr SkFontStyle::Slant ConvertToFontStyle(mojom::TypefaceSlant slant) {
switch (slant) {
case mojom::TypefaceSlant::kRoman:
return SkFontStyle::Slant::kUpright_Slant;
case mojom::TypefaceSlant::kItalic:
return SkFontStyle::Slant::kItalic_Slant;
case mojom::TypefaceSlant::kOblique:
return SkFontStyle::Slant::kOblique_Slant;
}
NOTREACHED();
}
} // namespace
FontDataServiceImpl::MappedAsset::MappedAsset(
std::unique_ptr<SkStreamAsset> asset,
base::MappedReadOnlyRegion shared_memory)
: asset(std::move(asset)), shared_memory(std::move(shared_memory)) {}
FontDataServiceImpl::MappedAsset::~MappedAsset() = default;
FontDataServiceImpl::FontDataServiceImpl()
: font_manager_(skia::DefaultFontMgr()) {
CHECK(font_manager_);
}
FontDataServiceImpl::~FontDataServiceImpl() = default;
void FontDataServiceImpl::ConnectToFontService(
mojo::PendingReceiver<font_data_service::mojom::FontDataService> receiver) {
GetFontDataServiceTaskRunner()->PostTask(
FROM_HERE, base::BindOnce(&BindToFontService, std::move(receiver)));
}
void FontDataServiceImpl::BindReceiver(
mojo::PendingReceiver<mojom::FontDataService> receiver) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
receivers_.Add(this, std::move(receiver));
}
std::tuple<base::File, uint64_t> FontDataServiceImpl::GetFileHandle(
SkTypeface& typeface) {
SkString font_path;
typeface.getResourceName(&font_path);
base::UmaHistogramBoolean("Chrome.FontDataService.EmptyPathOnGetFileHandle",
font_path.isEmpty());
#if BUILDFLAG(IS_LINUX)
// TODO(crbug.com/463411679): `getResourceName()` is not implemented for
// Linux, so the returned file will always be invalid and a memory region will
// be shared instead.
CHECK(font_path.isEmpty());
#endif // BUILDFLAG(IS_LINUX)
if (font_path.isEmpty()) {
return {};
}
auto font_file_path = base::FilePath::FromUTF8Unsafe(font_path.c_str());
base::UmaHistogramBoolean(
"Chrome.FontDataService.FileHandlePathReferencesParent",
font_file_path.ReferencesParent());
auto font_file =
base::File(font_file_path, base::File::FLAG_OPEN | base::File::FLAG_READ |
base::File::FLAG_WIN_EXCLUSIVE_WRITE);
#if BUILDFLAG(IS_WIN)
if (!font_file.IsValid()) {
base::UmaHistogramSparse("Chrome.FontDataService.WinLastError",
::GetLastError());
}
#endif // BUILDFLAG(IS_WIN)
return std::make_tuple(std::move(font_file), GetUniqueFileId(font_file_path));
}
// This is a port of a DWriteFontProxy workaround, reimplemented for
// compatibility. This workaround makes Chrome non-compliant with the CSS font
// matching algorithm standard (see
// https://www.w3.org/TR/css-fonts-3/#font-style-matching) and should be removed
// (see crbug.com/475810976).
// As a workaround for crbug.com/635932, refuse to
// load some common fonts that do not contain certain styles. We found that
// sometimes these fonts are installed only in specialized styles ('Open Sans'
// might only be available in the condensed light variant, or Helvetica might
// only be available in bold). That results in a poor user experience because
// websites that use those fonts usually expect them to be rendered in the
// regular variant.
//
// The specific logic is:
// If the matched typeface's style matches the requested style, return it
// (the exact requested font is installed)
//
// - Request is regular and match is regular -> return regular
// - Request is non-regular and match is non-regular -> return non-regular
//
// If the matched typeface's style doesn't match the requested style, but the
// matched typeface's style is regular, return it. Otherwise return null (we
// assume DirectWrite returns the most appropriate font it can find, so we have
// to evaluate if it's good enough)
//
// - Request is regular and match is non-regular -> return null (addresses the
// original bug)
// - Request is non-regular and match is regular -> return regular (complies
// with the standard + using regular for a non-regular request is less
// problematic UX-wise than using a styled font for a regular request)
//
// For each of these criteria, the exact value for each style component isn't
// used directly. For instance, the weight doesn't need to be an exact match but
// does need to be in the same "direction" (light/normal/bold) as the request.
// Likewise for width (condensed/normal/expanded).
bool FontDataServiceImpl::CheckMatchesRequiredStyle(
const SkFontStyle& actual_style,
const std::string& requested_family_name,
const SkFontStyle& requested_style) {
#if BUILDFLAG(IS_WIN)
static const std::string kFamiliesWithRequiredStyles[] = {
// The regular version of Gill Sans is actually in the Gill Sans MT
// family,
// and the Gill Sans family typically contains just the ultra-bold styles.
"gill sans",
"helvetica",
"open sans",
};
// Returns the "direction" of a given style component. For example, using this
// lambda for "weight" will return -1 if the typeface is light, 1 if it's
// bold, and 0 if it's normal. Used to compare styles directionally between 2
// typefaces.
auto find_style_component_direction = [](auto value, auto lower_bound,
auto upper_bound) {
if (value <= lower_bound) {
return -1;
}
if (value >= upper_bound) {
return 1;
}
return 0;
};
// SkTypeface defines "is bold" as >= Semibold. The enum has "light" as the
// first weight under "normal". That only leaves "medium" to classify, which
// is between "normal" and "semi-bold", so let's consider medium as "normal".
int requested_weight_direction = find_style_component_direction(
requested_style.weight(), SkFontStyle::kLight_Weight,
SkFontStyle::kSemiBold_Weight);
// For width, there's no precedent anywhere else in the codebase but the enum
// is pretty explicit. Anything under semi-condensed is "condensed", anything
// above semi-expanded is "expanded", and "normal" is right in the middle.
int requested_width_direction = find_style_component_direction(
requested_style.width(), SkFontStyle::kSemiCondensed_Width,
SkFontStyle::kSemiExpanded_Width);
for (const auto& family_name : kFamiliesWithRequiredStyles) {
if (base::EqualsCaseInsensitiveASCII(requested_family_name, family_name)) {
int found_weight_direction = find_style_component_direction(
actual_style.weight(), SkFontStyle::kLight_Weight,
SkFontStyle::kSemiBold_Weight);
int found_width_direction = find_style_component_direction(
actual_style.width(), SkFontStyle::kSemiCondensed_Width,
SkFontStyle::kSemiExpanded_Width);
// The regular variant is always usable if it's found.
if (found_weight_direction == 0 && found_width_direction == 0 &&
actual_style.slant() == SkFontStyle::kUpright_Slant) {
return true;
}
// If the requested style and the found typeface's styles match, consider
// the found typeface to be usable.
return requested_weight_direction == found_weight_direction &&
requested_width_direction == found_width_direction &&
requested_style.slant() == actual_style.slant();
}
}
#endif
// The requested family doesn't have style requirements, consider it usable.
return true;
}
void FontDataServiceImpl::MatchFamilyName(const std::string& family_name,
mojom::TypefaceStylePtr style,
MatchFamilyNameCallback callback) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
TRACE_EVENT("fonts", "FontDataServiceImpl::MatchFamilyName", "family_name",
family_name);
base::UmaHistogramEnumeration("Chrome.FontDataService.InvokedIPC",
FontDataServiceIPC::kMatchFamilyName);
// Call the font manager of the browser process to process the proxied match
// family request.
SkFontStyle sk_font_style(style->weight, style->width,
ConvertToFontStyle(style->slant));
sk_sp<SkTypeface> typeface =
font_manager_->matchFamilyStyle(family_name.c_str(), sk_font_style);
std::move(callback).Run(
CreateMatchFamilyNameResult(typeface, family_name, sk_font_style));
}
void FontDataServiceImpl::MatchFamilyNameCharacter(
const std::string& family_name,
mojom::TypefaceStylePtr style,
const std::vector<std::string>& bcp47s,
int32_t character,
MatchFamilyNameCharacterCallback callback) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
TRACE_EVENT("fonts", "FontDataServiceImpl::MatchFamilyNameCharacter",
"family_name", family_name);
base::UmaHistogramEnumeration("Chrome.FontDataService.InvokedIPC",
FontDataServiceIPC::kMatchFamilyNameCharacter);
// Call the font manager of the browser process to process the proxied match
// family request.
SkFontStyle sk_font_style(style->weight, style->width,
ConvertToFontStyle(style->slant));
// Skia passes the language tags as an array of null-terminated c-strings with
// a count. We transform that to an std::vector<std::string> to pass it over
// mojo, but have to recreate the same structure before passing it to skia
// functions again.
std::vector<const char*> bcp47s_array;
for (const auto& bcp47 : bcp47s) {
bcp47s_array.push_back(bcp47.c_str());
}
sk_sp<SkTypeface> typeface = font_manager_->matchFamilyStyleCharacter(
family_name.c_str(), sk_font_style, bcp47s_array.data(), bcp47s.size(),
character);
std::move(callback).Run(
CreateMatchFamilyNameResult(typeface, family_name, sk_font_style));
}
void FontDataServiceImpl::GetAllFamilyNames(
GetAllFamilyNamesCallback callback) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
TRACE_EVENT("fonts", "FontDataServiceImpl::GetAllFamilyNames");
base::UmaHistogramEnumeration("Chrome.FontDataService.InvokedIPC",
FontDataServiceIPC::kGetAllFamilyNames);
int family_count = font_manager_->countFamilies();
std::vector<std::string> result;
result.reserve(family_count);
for (int i = 0; i < family_count; ++i) {
SkString out;
font_manager_->getFamilyName(i, &out);
result.emplace_back(out.begin(), out.end());
}
std::move(callback).Run(std::move(result));
}
void FontDataServiceImpl::LegacyMakeTypeface(
const std::optional<std::string>& family_name,
mojom::TypefaceStylePtr style,
LegacyMakeTypefaceCallback callback) {
base::UmaHistogramEnumeration("Chrome.FontDataService.InvokedIPC",
FontDataServiceIPC::kLegacyMakeTypeface);
SkFontStyle sk_font_style(style->weight, style->width,
ConvertToFontStyle(style->slant));
sk_sp<SkTypeface> typeface = font_manager_->legacyMakeTypeface(
family_name ? family_name->c_str() : nullptr, sk_font_style);
// CreateMatchFamilyNameResult uses `family_name` to check against a list of
// hard-coded family names so passing "" when `family_name` is `nullopt` is
// OK.
std::move(callback).Run(CreateMatchFamilyNameResult(
typeface, family_name ? *family_name : "", sk_font_style));
}
size_t FontDataServiceImpl::GetOrCreateAssetIndex(
std::unique_ptr<SkStreamAsset> asset) {
TRACE_EVENT("fonts", "FontDataServiceImpl::GetOrCreateAssetIndex");
// An asset can be used for multiple typefaces (a.k.a different ttc_index).
// On Windows, with DWrite font manager.
// SkDWriteFontFileStream : public SkStreamMemory
// getMemoryBase would not be a nullptr in this case.
intptr_t memory_base = reinterpret_cast<intptr_t>(asset->getMemoryBase());
// Check into the memory assets cache.
if (auto iter = address_to_asset_index_.find(memory_base);
iter != address_to_asset_index_.end()) {
return iter->second;
}
size_t asset_length = asset->getLength();
base::MappedReadOnlyRegion shared_memory_region =
base::ReadOnlySharedMemoryRegion::Create(asset_length);
PCHECK(shared_memory_region.IsValid());
size_t asset_index = assets_.size();
{
TRACE_EVENT("fonts",
"FontDataServiceImpl::GetOrCreateAssetIndex - memory copy",
"size", asset_length);
size_t bytes_read = asset->read(shared_memory_region.mapping.memory(),
shared_memory_region.mapping.size());
CHECK_EQ(bytes_read, asset_length);
}
assets_.push_back(std::make_unique<MappedAsset>(
std::move(asset), std::move(shared_memory_region)));
// Update the assets cache.
address_to_asset_index_[memory_base] = asset_index;
return asset_index;
}
uint64_t FontDataServiceImpl::GetUniqueFileId(base::FilePath path) {
uint64_t new_id = unique_path_ids_.size() + 1;
auto [it, inserted] = unique_path_ids_.try_emplace(path, new_id);
return it->second;
}
mojom::MatchFamilyNameResultPtr
FontDataServiceImpl::CreateMatchFamilyNameResult(
sk_sp<SkTypeface> typeface,
const std::string& family_name,
const SkFontStyle& requested_style) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
CreateResult result_status = CreateResult::kNoTypeface;
auto result = mojom::MatchFamilyNameResult::New();
if (typeface) {
if (!CheckMatchesRequiredStyle(typeface->fontStyle(), family_name,
requested_style)) {
return nullptr;
}
auto iter = typeface_to_asset_index_.find(typeface->uniqueID());
if (iter != typeface_to_asset_index_.end()) {
const size_t asset_index = iter->second.asset_index;
base::ReadOnlySharedMemoryRegion region =
assets_[asset_index]->shared_memory.region.Duplicate();
result->ttc_index = iter->second.ttc_index;
if (region.IsValid()) {
result->typeface_data =
mojom::TypefaceData::NewRegion(std::move(region));
result_status = CreateResult::kSuccessExistingSharedMemory;
} else {
result_status = CreateResult::kFailureExistingSharedMemory;
}
} else {
// While the stream is not necessary for file handles, fetch the ttc_index
// if available. It is possible that the index will be set even if
// openStream fails.
auto stream = typeface->openStream(&result->ttc_index);
// Try to share the font with a base::File. This is avoiding copy of the
// content of the file.
base::File font_file;
uint64_t font_file_unique_id;
std::tie(font_file, font_file_unique_id) = GetFileHandle(*typeface);
if (font_file.IsValid()) {
TRACE_EVENT("fonts", "FontDataServiceImpl - sharing file handle");
result->typeface_data =
mojom::TypefaceData::NewFontFile(mojom::TypefaceFile::New(
std::move(font_file), font_file_unique_id));
result_status = CreateResult::kSuccessSharingFileHandle;
} else {
TRACE_EVENT("fonts", "FontDataServiceImpl - sharing memory region");
// If it failed to share as an base::File, try sharing with shared
// memory. Try to open the stream and prepare shared memory that will be
// shared with renderers. The content of the stream is copied into the
// shared memory. If the stream data is invalid or if the cache is full,
// return an invalid memory map region.
// TODO(crbug.com/335680565): Improve cache by transitioning to LRU.
if (stream && stream->hasLength() && (stream->getLength() > 0u) &&
stream->getMemoryBase()) {
UMA_HISTOGRAM_COUNTS_10000(
"Chrome.FontDataService.MemoryMapCacheSize", assets_.size());
if (assets_.size() >= kMemoryMapCacheSize &&
base::FeatureList::IsEnabled(kDumpOnOOBFontDataServiceCache)) {
base::debug::DumpWithoutCrashing();
}
const size_t asset_index = GetOrCreateAssetIndex(std::move(stream));
base::ReadOnlySharedMemoryRegion region =
assets_[asset_index]->shared_memory.region.Duplicate();
typeface_to_asset_index_[typeface->uniqueID()] =
MappedTypeface{asset_index, result->ttc_index};
if (region.IsValid()) {
result->typeface_data =
mojom::TypefaceData::NewRegion(std::move(region));
result_status = CreateResult::kSuccessSharingNewMemoryRegion;
} else {
result_status = CreateResult::kFailureSharingNewMemoryRegion;
}
}
}
}
}
UMA_HISTOGRAM_ENUMERATION("Chrome.FontDataService.CreateResult",
result_status);
if (!result->typeface_data) {
return nullptr;
}
result->synthetic_bold = typeface->isSyntheticBold();
result->synthetic_oblique = typeface->isSyntheticOblique();
const int axis_count = typeface->getVariationDesignPosition({});
if (axis_count > 0) {
auto coordinate_list =
base::HeapArray<SkFontArguments::VariationPosition::Coordinate>::Uninit(
axis_count);
if (typeface->getVariationDesignPosition(coordinate_list) > 0) {
result->variation_position = mojom::VariationPosition::New();
result->variation_position->coordinates.reserve(coordinate_list.size());
result->variation_position->coordinateCount = axis_count;
std::ranges::transform(
coordinate_list,
std::back_inserter(result->variation_position->coordinates),
[](const SkFontArguments::VariationPosition::Coordinate& coordinate) {
return mojom::Coordinate::New(coordinate.axis, coordinate.value);
});
}
}
return result;
}
} // namespace font_data_service