blob: 6bfbf36f23e8650cde20fc808323046dc5e57b7f [file] [log] [blame]
// Copyright 2018 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 "content/browser/font_unique_name_lookup/font_unique_name_lookup.h"
#include "base/android/build_info.h"
#include "base/files/file.h"
#include "base/files/file_enumerator.h"
#include "base/files/file_util.h"
#include "base/logging.h"
#include "base/metrics/histogram_macros.h"
#include "base/no_destructor.h"
#include "base/path_service.h"
#include "base/strings/utf_string_conversions.h"
#include "base/task/post_task.h"
#include "base/time/time.h"
#include "third_party/blink/public/common/font_unique_name_lookup/font_table_matcher.h"
#include "third_party/blink/public/common/font_unique_name_lookup/font_unique_name_table.pb.h"
#include "third_party/blink/public/common/font_unique_name_lookup/icu_fold_case_util.h"
#include <set>
#include <vector>
#include "third_party/icu/source/common/unicode/unistr.h"
#include FT_TRUETYPE_IDS_H
namespace {
const char kProtobufFilename[] = "font_unique_name_table.pb";
static const char* const kAndroidFontPaths[] = {"/system/fonts",
"/vendor/fonts"};
// These values are logged to UMA. Entries should not be renumbered and
// numeric values should never be reused. Please keep in sync with
// "FontScanningResult" in src/tools/metrics/histograms/enums.xml.
enum class FontScanningResult {
kSuccess = 0,
kFtNewFaceFailed = 1,
kZeroNameTableEntries = 2,
kUnableToRetriveNameEntry = 3,
kNameInvalidUnicode = 4,
kMaxValue = kNameInvalidUnicode
};
void LogUMAFontScanningResult(FontScanningResult result) {
UMA_HISTOGRAM_ENUMERATION("Blink.Fonts.AndroidFontScanningResult", result);
}
void LogUMAPersistSuccess(bool success) {
UMA_HISTOGRAM_BOOLEAN("Blink.Fonts.AndroidFontScanningPersistToFileSuccess",
success);
}
void LogUMALoadFromFileSuccess(bool success) {
UMA_HISTOGRAM_BOOLEAN("Blink.Fonts.AndroidFontScanningLoadFromFileSuccess",
success);
}
void LogUMAFontScanningUpdateNeeded(bool update_needed) {
UMA_HISTOGRAM_BOOLEAN("Blink.Fonts.AndroidFontScanningUpdateNeeded",
update_needed);
}
void LogUMAFontScanningDuration(base::TimeDelta duration) {
UMA_HISTOGRAM_MEDIUM_TIMES("Blink.Fonts.AndroidFontScanningTableBuildTime",
duration);
}
bool SfntNameIsEnglish(const FT_SfntName& sfnt_name) {
if (sfnt_name.platform_id == TT_PLATFORM_MICROSOFT)
return sfnt_name.language_id == TT_MS_LANGID_ENGLISH_UNITED_STATES;
if (sfnt_name.platform_id == TT_PLATFORM_MACINTOSH)
return sfnt_name.language_id == TT_MAC_LANGID_ENGLISH;
return false;
}
// Scoped wrapper for a FreeType library object in order to ensure
// initialization and tear down. Used during scanning font files.
class ScopedFtLibrary {
public:
ScopedFtLibrary() { FT_Init_FreeType(&ft_library_); }
~ScopedFtLibrary() { FT_Done_FreeType(ft_library_); }
FT_Library get() { return ft_library_; }
private:
FT_Library ft_library_;
};
// Convenience scoped wrapper for FT_Face instances. Takes care of handling
// FreeType memory by calling FT_Done_Face on destruction.
class ScopedFtFace {
public:
// Create a new FT_Face instance that will be wrapped by this object.
// Call IsValid() after construction to check for errors.
// |library| is the parent FT_Library instance, |font_path| the input font
// file path, and |ttc_index| the font file index (for TrueType collections).
ScopedFtFace(FT_Library library,
const std::string& font_path,
int32_t ttc_index)
: ft_face_(nullptr),
ft_error_(
FT_New_Face(library, font_path.c_str(), ttc_index, &ft_face_)) {}
// Destructor will destroy the FT_Face instance automatically.
~ScopedFtFace() {
if (IsValid()) {
FT_Done_Face(ft_face_);
}
}
// Returns true iff instance is valid, i.e. construction did not fail.
bool IsValid() const { return ft_error_ == FT_Err_Ok; }
// Return FreeType error code from construction.
FT_Error error() const { return ft_error_; }
// Returns FT_Face value.
FT_Face get() const { return ft_face_; }
private:
FT_Face ft_face_ = nullptr;
FT_Error ft_error_ = FT_Err_Ok;
};
void IndexFile(FT_Library ft_library,
blink::FontUniqueNameTable* font_table,
const std::string& font_file_path,
uint32_t ttc_index) {
ScopedFtFace face(ft_library, font_file_path.c_str(), ttc_index);
if (!face.IsValid()) {
LogUMAFontScanningResult(FontScanningResult::kFtNewFaceFailed);
return;
}
if (!FT_Get_Sfnt_Name_Count(face.get())) {
LogUMAFontScanningResult(FontScanningResult::kZeroNameTableEntries);
return;
}
blink::FontUniqueNameTable_UniqueFont* added_unique_font =
font_table->add_fonts();
added_unique_font->set_file_path(font_file_path);
added_unique_font->set_ttc_index(ttc_index);
int added_font_index = font_table->fonts_size() - 1;
for (size_t i = 0; i < FT_Get_Sfnt_Name_Count(face.get()); ++i) {
FT_SfntName sfnt_name;
if (FT_Get_Sfnt_Name(face.get(), i, &sfnt_name) != 0) {
LogUMAFontScanningResult(FontScanningResult::kUnableToRetriveNameEntry);
return;
}
// From the CSS Fonts spec chapter 4.3. Font reference: the src descriptor
// "For OpenType fonts with multiple localizations of the full font name,
// the US English version is used (language ID = 0x409 for Windows and
// language ID = 0 for Macintosh) or the first localization when a US
// English full font name is not available (the OpenType specification
// recommends that all fonts minimally include US English names)."
// Since we can assume Android system fonts contain an English name,
// continue here.
if (!SfntNameIsEnglish(sfnt_name))
continue;
std::string sfnt_name_string = "";
std::string codepage_name;
// Codepage names from http://demo.icu-project.org/icu-bin/convexp
if (sfnt_name.platform_id == TT_PLATFORM_MICROSOFT &&
sfnt_name.encoding_id == TT_MS_ID_UNICODE_CS) {
codepage_name = "UTF16-BE";
} else if (sfnt_name.platform_id == TT_PLATFORM_MACINTOSH &&
sfnt_name.encoding_id == TT_MAC_ID_ROMAN) {
codepage_name = "macintosh";
}
icu::UnicodeString sfnt_name_unicode(
reinterpret_cast<char*>(sfnt_name.string), sfnt_name.string_len,
codepage_name.c_str());
if (sfnt_name_unicode.isBogus()) {
LogUMAFontScanningResult(FontScanningResult::kNameInvalidUnicode);
return;
}
// Firefox performs case insensitive matching for src: local().
sfnt_name_unicode.foldCase();
sfnt_name_unicode.toUTF8String(sfnt_name_string);
blink::FontUniqueNameTable_UniqueNameToFontMapping* name_mapping =
font_table->add_name_map();
name_mapping->set_font_name(blink::IcuFoldCase(sfnt_name_string));
name_mapping->set_font_index(added_font_index);
}
LogUMAFontScanningResult(FontScanningResult::kSuccess);
}
int32_t NumberOfFacesInFontFile(FT_Library ft_library,
const std::string& font_filename) {
// According to FreeType documentation calling FT_Open_Face with a negative
// index value allows us to probe how many fonts can be found in a font file
// (which can be a single font ttf or a TrueType collection (.ttc)).
ScopedFtFace probe_face(ft_library, font_filename.c_str(), -1);
if (!probe_face.IsValid())
return 0;
return probe_face.get()->num_faces;
}
} // namespace
namespace content {
class PlatformFontUniqueNameLookup : public FontUniqueNameLookup {
public:
PlatformFontUniqueNameLookup() : FontUniqueNameLookup(GetCacheDirectory()) {
ScheduleLoadOrUpdateTable();
}
private:
static base::FilePath GetCacheDirectory() {
base::FilePath cache_directory;
base::PathService::Get(base::DIR_CACHE, &cache_directory);
return cache_directory;
}
};
FontUniqueNameLookup& FontUniqueNameLookup::GetInstance() {
static base::NoDestructor<PlatformFontUniqueNameLookup> sInstance;
return *sInstance.get();
}
FontUniqueNameLookup::FontUniqueNameLookup(
const base::FilePath& cache_directory)
: cache_directory_(cache_directory) {
if (!DirectoryExists(cache_directory_) ||
!base::PathIsWritable(cache_directory_)) {
DCHECK(false) << "Error accessing cache directory for writing: "
<< cache_directory_.value();
cache_directory_ = base::FilePath();
}
}
FontUniqueNameLookup::~FontUniqueNameLookup() = default;
base::ReadOnlySharedMemoryRegion FontUniqueNameLookup::DuplicateMemoryRegion() {
DCHECK(proto_storage_.IsValid() && proto_storage_.mapping.size());
return proto_storage_.region.Duplicate();
}
void FontUniqueNameLookup::QueueShareMemoryRegionWhenReady(
scoped_refptr<base::SequencedTaskRunner> task_runner,
blink::mojom::FontUniqueNameLookup::GetUniqueNameLookupTableCallback
callback) {
pending_callbacks_.emplace_back(std::move(task_runner), std::move(callback));
}
bool FontUniqueNameLookup::IsValid() {
return proto_storage_ready_.IsSignaled() && proto_storage_.IsValid() &&
proto_storage_.mapping.size();
}
bool FontUniqueNameLookup::UpdateTableIfNeeded() {
blink::FontUniqueNameTable font_table;
bool update_needed =
!proto_storage_.IsValid() || !proto_storage_.mapping.size() ||
!font_table.ParseFromArray(proto_storage_.mapping.memory(),
proto_storage_.mapping.size()) ||
font_table.stored_for_platform_version_identifier() !=
GetAndroidBuildFingerprint();
LogUMAFontScanningUpdateNeeded(update_needed);
if (update_needed)
UpdateTable();
return update_needed;
}
bool FontUniqueNameLookup::UpdateTable() {
base::TimeTicks update_table_start_time = base::TimeTicks::Now();
std::vector<std::string> font_files_to_index = GetFontFilePaths();
ScopedFtLibrary ft_library;
blink::FontUniqueNameTable font_table;
font_table.set_stored_for_platform_version_identifier(
GetAndroidBuildFingerprint());
for (const auto& font_file : font_files_to_index) {
int32_t number_of_faces =
NumberOfFacesInFontFile(ft_library.get(), font_file);
for (int32_t i = 0; i < number_of_faces; ++i) {
IndexFile(ft_library.get(), &font_table, font_file, i);
}
}
blink::FontTableMatcher::SortUniqueNameTableForSearch(&font_table);
proto_storage_ =
base::ReadOnlySharedMemoryRegion::Create(font_table.ByteSizeLong());
if (!proto_storage_.IsValid() || !proto_storage_.mapping.size())
return false;
if (!font_table.SerializeToArray(proto_storage_.mapping.memory(),
proto_storage_.mapping.size())) {
proto_storage_ = base::MappedReadOnlyRegion();
return false;
}
base::TimeDelta duration = base::TimeTicks::Now() - update_table_start_time;
LogUMAFontScanningDuration(duration);
return true;
}
bool FontUniqueNameLookup::LoadFromFile() {
// Reset to empty to ensure IsValid() is false if reading fails.
proto_storage_ = base::MappedReadOnlyRegion();
base::File table_cache_file(
TableCacheFilePath(),
base::File::FLAG_OPEN | base::File::Flags::FLAG_READ);
if (!table_cache_file.IsValid()) {
LogUMALoadFromFileSuccess(false);
return false;
}
proto_storage_ =
base::ReadOnlySharedMemoryRegion::Create(table_cache_file.GetLength());
if (!proto_storage_.IsValid() || !proto_storage_.mapping.size()) {
LogUMALoadFromFileSuccess(false);
return false;
}
int read_result = table_cache_file.Read(
0, static_cast<char*>(proto_storage_.mapping.memory()),
table_cache_file.GetLength());
// If no bytes were read or Read() returned -1 we are not able to reconstruct
// a font table from the cached file.
if (read_result <= 0) {
proto_storage_ = base::MappedReadOnlyRegion();
LogUMALoadFromFileSuccess(false);
return false;
}
blink::FontUniqueNameTable font_table;
if (!font_table.ParseFromArray(proto_storage_.mapping.memory(),
proto_storage_.mapping.size())) {
proto_storage_ = base::MappedReadOnlyRegion();
LogUMALoadFromFileSuccess(false);
return false;
}
LogUMALoadFromFileSuccess(true);
return true;
}
bool FontUniqueNameLookup::PersistToFile() {
DCHECK(proto_storage_.IsValid() && proto_storage_.mapping.size());
base::File table_cache_file(
TableCacheFilePath(),
base::File::FLAG_CREATE_ALWAYS | base::File::Flags::FLAG_WRITE);
if (!table_cache_file.IsValid()) {
LogUMAPersistSuccess(false);
return false;
}
if (table_cache_file.Write(
0, static_cast<char*>(proto_storage_.mapping.memory()),
proto_storage_.mapping.size()) == -1) {
table_cache_file.SetLength(0);
proto_storage_ = base::MappedReadOnlyRegion();
LogUMAPersistSuccess(false);
return false;
}
LogUMAPersistSuccess(true);
return true;
}
void FontUniqueNameLookup::ScheduleLoadOrUpdateTable() {
base::PostTaskWithTraits(FROM_HERE,
{base::MayBlock(), base::TaskPriority::BEST_EFFORT,
base::TaskShutdownBehavior::CONTINUE_ON_SHUTDOWN},
base::BindOnce(
[](FontUniqueNameLookup* instance) {
// Error from LoadFromFile() is ignored:
// Loading the cache file could be recovered
// from by rebuilding the font table.
// UpdateTableIfNeeded() checks whether the
// internal base::MappedReadOnlyRegion has a
// size, which it doesn't if the LoadFromFile()
// failed. If it doesn't have a size, the table
// is rebuild by calling UpdateTable().
instance->LoadFromFile();
if (instance->UpdateTableIfNeeded()) {
instance->PersistToFile();
}
instance->proto_storage_ready_.Signal();
instance->PostCallbacks();
},
base::Unretained(this)));
}
base::FilePath FontUniqueNameLookup::TableCacheFilePath() {
return base::FilePath(
cache_directory_.Append(base::FilePath(kProtobufFilename)));
}
std::string FontUniqueNameLookup::GetAndroidBuildFingerprint() const {
return android_build_fingerprint_for_testing_.size()
? android_build_fingerprint_for_testing_
: base::android::BuildInfo::GetInstance()->android_build_fp();
}
std::vector<std::string> FontUniqueNameLookup::GetFontFilePaths() const {
if (font_file_paths_for_testing_.size())
return font_file_paths_for_testing_;
std::vector<std::string> font_files;
for (const char* font_dir_path : kAndroidFontPaths) {
base::FileEnumerator files_enumerator(
base::MakeAbsoluteFilePath(base::FilePath(font_dir_path)), true,
base::FileEnumerator::FILES);
for (base::FilePath name = files_enumerator.Next(); !name.empty();
name = files_enumerator.Next()) {
if (name.Extension() == ".ttf" || name.Extension() == ".ttc" ||
name.Extension() == ".otf") {
font_files.push_back(name.value());
}
}
}
return font_files;
}
FontUniqueNameLookup::CallbackOnTaskRunner::CallbackOnTaskRunner(
scoped_refptr<base::SequencedTaskRunner> runner,
blink::mojom::FontUniqueNameLookup::GetUniqueNameLookupTableCallback
callback)
: task_runner(std::move(runner)), mojo_callback(std::move(callback)) {}
FontUniqueNameLookup::CallbackOnTaskRunner::CallbackOnTaskRunner(
CallbackOnTaskRunner&& other) {
task_runner = std::move(other.task_runner);
mojo_callback = std::move(other.mojo_callback);
other.task_runner = nullptr;
other.mojo_callback =
blink::mojom::FontUniqueNameLookup::GetUniqueNameLookupTableCallback();
}
FontUniqueNameLookup::CallbackOnTaskRunner::~CallbackOnTaskRunner() = default;
void FontUniqueNameLookup::PostCallbacks() {
for (auto& pending_callback : pending_callbacks_) {
pending_callback.task_runner->PostTask(
FROM_HERE, base::BindOnce(std::move(pending_callback.mojo_callback),
DuplicateMemoryRegion()));
}
pending_callbacks_.clear();
}
} // namespace content