
Add the ability for the PDF Viewer to tell PDFium about the fonts on the system on Windows. This allows PDFium to make better decisions when mapping fonts. Bug: 381126164 Low-Coverage-Reason: COVERAGE_UNDERREPORTED see patchset 3 try jobs. Change-Id: Ia28988b8e678ac1e71dddccea7600534b2ea2607 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/6075232 Reviewed-by: Alex Gough <ajgo@chromium.org> Commit-Queue: Lei Zhang <thestig@chromium.org> Cr-Commit-Position: refs/heads/main@{#1393977}
411 lines
15 KiB
C++
411 lines
15 KiB
C++
// 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 "pdf/pdfium/pdfium_font_win.h"
|
|
|
|
#include <optional>
|
|
#include <string>
|
|
#include <string_view>
|
|
#include <utility>
|
|
|
|
#include "base/check_op.h"
|
|
#include "base/containers/contains.h"
|
|
#include "base/containers/fixed_flat_map.h"
|
|
#include "base/containers/flat_map.h"
|
|
#include "base/feature_list.h"
|
|
#include "base/logging.h"
|
|
#include "base/no_destructor.h"
|
|
#include "base/sequence_checker.h"
|
|
#include "base/strings/string_number_conversions.h"
|
|
#include "base/trace_event/trace_event.h"
|
|
#include "pdf/pdfium/pdfium_engine.h"
|
|
#include "pdf/pdfium/pdfium_font_helpers.h"
|
|
#include "skia/ext/font_utils.h"
|
|
#include "third_party/blink/public/platform/web_font_description.h"
|
|
#include "third_party/pdfium/public/fpdf_sysfontinfo.h"
|
|
#include "third_party/re2/src/re2/re2.h"
|
|
#include "third_party/skia/include/core/SkFontMgr.h"
|
|
#include "third_party/skia/include/core/SkFontStyle.h"
|
|
#include "third_party/skia/include/core/SkStream.h"
|
|
#include "third_party/skia/include/core/SkTypeface.h"
|
|
|
|
namespace chrome_pdf {
|
|
|
|
namespace {
|
|
|
|
constexpr auto kBase14Substs =
|
|
base::MakeFixedFlatMap<std::string_view, std::string_view>({
|
|
// PDF Fonts
|
|
{"Courier", "Courier New"},
|
|
{"Courier-Bold", "Courier New Bold"},
|
|
{"Courier-BoldOblique", "Courier New Bold Italic"},
|
|
{"Courier-Oblique", "Courier New Italic"},
|
|
{"Helvetica", "Arial"},
|
|
{"Helvetica-Bold", "Arial Bold"},
|
|
{"Helvetica-BoldOblique", "Arial Bold Italic"},
|
|
{"Helvetica-Oblique", "Arial Italic"},
|
|
{"Times-Roman", "Times New Roman"},
|
|
{"Times-Bold", "Times New Roman Bold"},
|
|
{"Times-BoldItalic", "Times New Roman Bold Italic"},
|
|
{"Times-Italic", "Times New Roman Italic"},
|
|
});
|
|
|
|
// kBase14Substs from cfx_folderfontinfo.
|
|
std::string GetSubstFont(const std::string& face) {
|
|
auto iter = kBase14Substs.find(face);
|
|
if (iter != kBase14Substs.end()) {
|
|
return std::string(iter->second);
|
|
}
|
|
return face;
|
|
}
|
|
|
|
// Kill switch in case this goes horribly wrong.
|
|
// TODO(crbug.com/381126164): Remove after this lands safely in a Stable
|
|
// release.
|
|
BASE_FEATURE(kPdfEnumFontsWin,
|
|
"PdfEnumFontsWin",
|
|
base::FEATURE_ENABLED_BY_DEFAULT);
|
|
|
|
// Maps font description and charset to `FontId` as requested by PDFium, with
|
|
// `FontId` as an opaque type that PDFium works with. Based on the `FontId`,
|
|
// PDFium can read from the font files using GetFontData(). Properly frees the
|
|
// underlying resource type when PDFium is done with the mapped font.
|
|
class SkiaFontMapper {
|
|
public:
|
|
// Defined as the type most convenient for use with PDFium's
|
|
// `FPDF_SYSFONTINFO` functions.
|
|
using FontId = void*;
|
|
|
|
SkiaFontMapper() : manager_(skia::DefaultFontMgr()) {}
|
|
|
|
~SkiaFontMapper() = delete;
|
|
|
|
void EnumFonts(FPDF_SYSFONTINFO* sysfontinfo, void* mapper) {
|
|
if (!base::FeatureList::IsEnabled(kPdfEnumFontsWin)) {
|
|
return;
|
|
}
|
|
|
|
const int count = manager_->countFamilies();
|
|
for (int i = 0; i < count; ++i) {
|
|
SkString family;
|
|
manager_->getFamilyName(i, &family);
|
|
// Skia does not make any guarantees about whether `family` can be empty
|
|
// or not.
|
|
// PDFium does not check if FPDF_AddInstalledFont() got an empty string.
|
|
// Do an explicit check here to make sure the two sides play nicely
|
|
// together.
|
|
if (!family.isEmpty()) {
|
|
// It may be better to pick a more accurate character set value, but
|
|
// this is good enough for now.
|
|
FPDF_AddInstalledFont(mapper, family.c_str(), FXFONT_DEFAULT_CHARSET);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Returns a handle to the font mapped based on `desc`, for use
|
|
// as the `font_id` in GetFontData() and DeleteFont() below. Returns nullptr
|
|
// on failure.
|
|
FontId MapFont(int weight,
|
|
int italic,
|
|
int charset,
|
|
int pitch,
|
|
const char* face) {
|
|
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
|
|
TRACE_EVENT2("fonts", "PdfiumMapFont", "face", std::string(face), "charset",
|
|
charset);
|
|
|
|
auto typeface = MapTypeface(weight, italic, charset, pitch, face);
|
|
if (typeface) {
|
|
FontId id = reinterpret_cast<FontId>(typeface->uniqueID());
|
|
id_to_typeface_.try_emplace(id, std::move(typeface));
|
|
return id;
|
|
}
|
|
|
|
LOG(WARNING) << "Failed to lookup face `" << base::HexEncode(face)
|
|
<< "` for charset " << charset << ", weight " << weight;
|
|
return nullptr;
|
|
}
|
|
|
|
// Releases the font file that `font_id` points to. Note that skia's font
|
|
// manager might retain its own cached resources.
|
|
void DeleteFont(FontId font_id) {
|
|
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
|
|
id_to_typeface_.erase(font_id);
|
|
}
|
|
|
|
// Reads data from the `font_id` handle for `table` into a `buffer` of
|
|
// `buf_size`. Returns the amount of data read on success, or 0 on failure.
|
|
// If `buffer` is null, then just return the required size for the buffer.
|
|
// See content::GetFontTable() for information on the `table_tag` parameter.
|
|
unsigned long GetFontData(FontId font_id,
|
|
unsigned int table_tag,
|
|
unsigned char* buffer,
|
|
unsigned long buf_size) const {
|
|
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
|
|
|
|
// This class creates font_id so it will always cast safely to SkTypefaceID.
|
|
auto stored_typeface = id_to_typeface_.find(font_id);
|
|
if (stored_typeface == id_to_typeface_.end()) {
|
|
return 0;
|
|
}
|
|
|
|
sk_sp<SkTypeface> typeface = stored_typeface->second;
|
|
|
|
// PDFium asks for 0 and 'ttcf' tags. These are not supported by DirectWrite
|
|
// backed Skia fonts so this adapter must do something sensible. Return the
|
|
// full font data for 0, and skip 'ttcf' allowing getTableSize(ttcf) to
|
|
// naturally fail.
|
|
if (table_tag == 0) {
|
|
std::unique_ptr<SkStreamAsset> stream = typeface->openStream(nullptr);
|
|
if (!buffer || buf_size < stream->getLength()) {
|
|
return stream->getLength();
|
|
}
|
|
return stream->read(buffer, buf_size);
|
|
}
|
|
|
|
if (!buffer) {
|
|
return typeface->getTableSize(table_tag);
|
|
}
|
|
|
|
return typeface->getTableData(table_tag, /*offset=*/0,
|
|
/*size=*/buf_size, buffer);
|
|
}
|
|
|
|
private:
|
|
// Lookup a typeface, taking various fallbacks if fonts are not available.
|
|
sk_sp<SkTypeface> MapTypeface(int weight,
|
|
int italic,
|
|
int charset,
|
|
int pitch,
|
|
const std::string& face) {
|
|
// Lookup via skia manager directly.
|
|
SkFontStyle style(weight, SkFontStyle::Width::kNormal_Width,
|
|
italic > 0 ? SkFontStyle::Slant::kItalic_Slant
|
|
: SkFontStyle::Slant::kUpright_Slant);
|
|
|
|
// Force name substitution for default PDF fonts.
|
|
std::string subst_face = GetSubstFont(face);
|
|
|
|
auto typeface = manager_->matchFamilyStyle(subst_face.c_str(), style);
|
|
if (typeface) {
|
|
return typeface;
|
|
}
|
|
|
|
// Try pdf->blink mappings, which does its own substitution.
|
|
std::optional<blink::WebFontDescription> desc =
|
|
PdfFontToBlinkFontMapping(weight, italic, charset, pitch, face.c_str());
|
|
if (desc) {
|
|
typeface = manager_->matchFamilyStyle(desc->family.Utf8().c_str(), style);
|
|
if (typeface) {
|
|
return typeface;
|
|
}
|
|
}
|
|
|
|
// Nothing was found (e.g. an optional Windows font is not installed),
|
|
// then try to map the name to a fallback.
|
|
auto fallback = GetFallbackFace(subst_face, charset, weight, italic);
|
|
if (fallback) {
|
|
typeface = manager_->matchFamilyStyle(fallback->c_str(), style);
|
|
if (typeface) {
|
|
return typeface;
|
|
}
|
|
}
|
|
|
|
// Finally, try some hacks that fix edge cases & mis-spellings.
|
|
return FinalFixups(subst_face, style);
|
|
}
|
|
|
|
bool HasFamily(const char* family) {
|
|
auto style_set = manager_->matchFamily(family);
|
|
bool has_family = style_set->count() > 0;
|
|
return has_family;
|
|
}
|
|
|
|
std::optional<std::string> GetShiftJISPreference(const std::string& face,
|
|
int weight,
|
|
int pitch_family) {
|
|
if (base::Contains(face, "Gothic") ||
|
|
base::Contains(face, "\x83\x53\x83\x56\x83\x62\x83\x4e")) {
|
|
if (base::Contains(face, "UI Gothic")) {
|
|
return "MS UI Gothic";
|
|
} else if (base::Contains(face, "PGothic") ||
|
|
base::Contains(face,
|
|
"\x82\x6f\x83\x53\x83\x56\x83\x62\x83\x4e") ||
|
|
base::Contains(face, "HGSGothicM") ||
|
|
base::Contains(face, "HGMaruGothicMPRO")) {
|
|
return "MS PGothic";
|
|
}
|
|
return "MS Gothic";
|
|
}
|
|
if (base::Contains(face, "Mincho") ||
|
|
base::Contains(face, "\x96\xbe\x92\xa9")) {
|
|
if (base::Contains(face, "PMincho") ||
|
|
base::Contains(face, "\x82\x6f\x96\xbe\x92\xa9")) {
|
|
return std::string(HasFamily("MS PMincho") ? "MS PMincho"
|
|
: "MS PGothic");
|
|
}
|
|
return std::string(HasFamily("MS Mincho") ? "MS Mincho" : "MS Gothic");
|
|
}
|
|
if (!(pitch_family & FXFONT_FF_ROMAN) && weight > 400) {
|
|
return "MS PGothic";
|
|
}
|
|
return "MS Gothic";
|
|
}
|
|
|
|
std::optional<std::string> GetGBPreference(const std::string& face,
|
|
int weight,
|
|
int pitch_family) {
|
|
// KaiTi and SimHei are Windows supplemental fonts so assume they were not
|
|
// found by skia.
|
|
if (base::Contains(face, "KaiTi") || base::Contains(face, "\xbf\xac")) {
|
|
return "SimSun";
|
|
} else if (base::Contains(face, "FangSong") ||
|
|
base::Contains(face, "\xb7\xc2\xcb\xce")) {
|
|
return "SimSun";
|
|
} else if (base::Contains(face, "SimSun") ||
|
|
base::Contains(face, "\xcb\xce")) {
|
|
return "SimSun";
|
|
} else if (base::Contains(face, "SimHei") ||
|
|
base::Contains(face, "\xba\xda")) {
|
|
return "SimHei";
|
|
} else if (!(pitch_family & FXFONT_FF_ROMAN) && weight > 550) {
|
|
return "SimHei";
|
|
}
|
|
return "SimSun";
|
|
}
|
|
|
|
std::optional<std::string> GetHangeulPreference(const std::string& face,
|
|
int weight,
|
|
int pitch_family) {
|
|
// Gulim is a supplemental font.
|
|
if (HasFamily("Gulim")) {
|
|
return "Gulim";
|
|
}
|
|
return "Malgun Gothic";
|
|
}
|
|
|
|
std::optional<std::string> GetFallbackFace(const std::string& face,
|
|
int charset,
|
|
int weight,
|
|
int pitch_family) {
|
|
switch (charset) {
|
|
case FXFONT_SHIFTJIS_CHARSET:
|
|
return GetShiftJISPreference(face, weight, pitch_family);
|
|
case FXFONT_GB2312_CHARSET:
|
|
return GetGBPreference(face, weight, pitch_family);
|
|
case FXFONT_HANGEUL_CHARSET:
|
|
return GetHangeulPreference(face, weight, pitch_family);
|
|
case FXFONT_CHINESEBIG5_CHARSET:
|
|
if (base::Contains(face, "MSung")) {
|
|
// Monospace.
|
|
return "Microsoft YaHei";
|
|
}
|
|
// Proportional.
|
|
return "Microsoft JHengHei";
|
|
default:
|
|
return std::nullopt;
|
|
}
|
|
}
|
|
|
|
// Put any last-gasp hacks into this method.
|
|
sk_sp<SkTypeface> FinalFixups(const std::string& face,
|
|
const SkFontStyle& style) {
|
|
// Some fonts are specified with weights that Skia can't provide.
|
|
// pdf.js/tests/issue5801.pdf specifies ArialBlack but a weight of 390.
|
|
// Commonly seen patterns: `ArialBlack` `Arial Black` & `Arial-Black`.
|
|
if (base::StartsWith(face, "Arial")) {
|
|
if (base::EndsWith(face, "Black")) {
|
|
SkFontStyle black = SkFontStyle(SkFontStyle::Weight::kBlack_Weight,
|
|
style.width(), style.slant());
|
|
return manager_->matchFamilyStyle("Arial", black);
|
|
}
|
|
if (base::EndsWith(face, "Narrow")) {
|
|
SkFontStyle narrow = SkFontStyle(SkFontStyle::Weight::kThin_Weight,
|
|
style.width(), style.slant());
|
|
return manager_->matchFamilyStyle("Arial", narrow);
|
|
}
|
|
}
|
|
// Some fonts are specified without spaces in their name e.g. `ComicSansMS`.
|
|
std::string with_spaces(face);
|
|
// s/{lower case letter}{uppercase letter}/l u/g.
|
|
if (re2::RE2::GlobalReplace(&with_spaces, "(\\p{Ll})(\\p{Lu})", "\\1 \\2") >
|
|
0) {
|
|
return manager_->matchFamilyStyle(with_spaces.c_str(), style);
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
sk_sp<SkFontMgr> const manager_;
|
|
base::flat_map<FontId, sk_sp<SkTypeface>> id_to_typeface_;
|
|
SEQUENCE_CHECKER(sequence_checker_);
|
|
};
|
|
|
|
SkiaFontMapper& GetSkiaFontMapper() {
|
|
static base::NoDestructor<SkiaFontMapper> mapper;
|
|
return *mapper;
|
|
}
|
|
|
|
void EnumFonts(FPDF_SYSFONTINFO* sysfontinfo, void* mapper) {
|
|
// Exit early if PDFium was specifically configured in `kNoMapping` mode.
|
|
if (PDFiumEngine::GetFontMappingMode() != FontMappingMode::kBlink) {
|
|
CHECK_EQ(PDFiumEngine::GetFontMappingMode(), FontMappingMode::kNoMapping);
|
|
return;
|
|
}
|
|
|
|
GetSkiaFontMapper().EnumFonts(sysfontinfo, mapper);
|
|
}
|
|
|
|
// Note: `exact` is obsolete.
|
|
void* MapFont(FPDF_SYSFONTINFO*,
|
|
int weight,
|
|
int italic,
|
|
int charset,
|
|
int pitch,
|
|
const char* face,
|
|
int* exact) {
|
|
// Exit early if PDFium was specifically configured in `kNoMapping` mode.
|
|
if (PDFiumEngine::GetFontMappingMode() != FontMappingMode::kBlink) {
|
|
CHECK_EQ(PDFiumEngine::GetFontMappingMode(), FontMappingMode::kNoMapping);
|
|
return nullptr;
|
|
}
|
|
|
|
return GetSkiaFontMapper().MapFont(weight, italic, charset, pitch, face);
|
|
}
|
|
|
|
unsigned long GetFontData(FPDF_SYSFONTINFO*,
|
|
void* font_id,
|
|
unsigned int table,
|
|
unsigned char* buffer,
|
|
unsigned long buf_size) {
|
|
CHECK_EQ(PDFiumEngine::GetFontMappingMode(), FontMappingMode::kBlink);
|
|
return GetSkiaFontMapper().GetFontData(font_id, table, buffer, buf_size);
|
|
}
|
|
|
|
void DeleteFont(FPDF_SYSFONTINFO*, void* font_id) {
|
|
CHECK_EQ(PDFiumEngine::GetFontMappingMode(), FontMappingMode::kBlink);
|
|
GetSkiaFontMapper().DeleteFont(font_id);
|
|
}
|
|
|
|
FPDF_SYSFONTINFO g_font_info = {.version = 1,
|
|
.Release = nullptr,
|
|
.EnumFonts = EnumFonts,
|
|
.MapFont = MapFont,
|
|
.GetFont = nullptr,
|
|
.GetFontData = GetFontData,
|
|
.GetFaceName = nullptr,
|
|
.GetFontCharset = nullptr,
|
|
.DeleteFont = DeleteFont};
|
|
|
|
} // namespace
|
|
|
|
void InitializeWindowsFontMapper() {
|
|
FPDF_SetSystemFontInfo(&g_font_info);
|
|
}
|
|
|
|
FPDF_SYSFONTINFO* GetSkiaFontMapperForTesting() {
|
|
return &g_font_info;
|
|
}
|
|
|
|
} // namespace chrome_pdf
|