| // 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 { |
| |
| // 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)); |
| } |
| |
| base::File FontDataServiceImpl::GetFileHandle(SkTypeface& typeface) { |
| SkString font_path; |
| typeface.getResourceName(&font_path); |
| base::UmaHistogramBoolean("Chrome.FontDataService.EmptyPathOnGetFileHandle", |
| font_path.isEmpty()); |
| if (font_path.isEmpty()) { |
| return {}; |
| } |
| |
| auto font_file = base::File(base::FilePath::FromUTF8Unsafe(font_path.c_str()), |
| 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 font_file; |
| } |
| |
| 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); |
| |
| // 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)); |
| } |
| |
| 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); |
| |
| // 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)); |
| } |
| |
| void FontDataServiceImpl::GetAllFamilyNames( |
| GetAllFamilyNamesCallback callback) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| TRACE_EVENT("fonts", "FontDataServiceImpl::GetAllFamilyNames"); |
| |
| 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) { |
| 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); |
| |
| std::move(callback).Run(CreateMatchFamilyNameResult(typeface)); |
| } |
| |
| 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; |
| } |
| |
| mojom::MatchFamilyNameResultPtr |
| FontDataServiceImpl::CreateMatchFamilyNameResult(sk_sp<SkTypeface> typeface) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| auto result = mojom::MatchFamilyNameResult::New(); |
| |
| if (typeface) { |
| 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)); |
| } |
| } 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 = GetFileHandle(*typeface); |
| if (font_file.IsValid()) { |
| TRACE_EVENT("fonts", "FontDataServiceImpl - sharing file handle"); |
| result->typeface_data = |
| mojom::TypefaceData::NewFontFile(std::move(font_file)); |
| } 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)); |
| } |
| } |
| } |
| } |
| } |
| |
| if (!result->typeface_data) { |
| return nullptr; |
| } |
| |
| 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 |