| // 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 |