fix: match default font families in headless chromium (#11340)

This commit is contained in:
Yury Semikhatsky 2022-02-11 09:06:17 -08:00 committed by GitHub
parent d8db785c0a
commit abd7084bcc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 347 additions and 24 deletions

View file

@ -40,7 +40,6 @@ export class CRBrowser extends Browser {
_backgroundPages = new Map<string, CRPage>();
_serviceWorkers = new Map<string, CRServiceWorker>();
_devtools?: CRDevTools;
_isMac = false;
private _version = '';
private _tracingRecording = false;
@ -49,6 +48,8 @@ export class CRBrowser extends Browser {
private _userAgent: string = '';
static async connect(transport: ConnectionTransport, options: BrowserOptions, devtools?: CRDevTools): Promise<CRBrowser> {
// Make a copy in case we need to update `headful` property below.
options = { ...options };
const connection = new CRConnection(transport, options.protocolLogger, options.browserLogsCollector);
const browser = new CRBrowser(connection, options);
browser._devtools = devtools;
@ -57,9 +58,11 @@ export class CRBrowser extends Browser {
await (options as any).__testHookOnConnectToBrowser();
const version = await session.send('Browser.getVersion');
browser._isMac = version.userAgent.includes('Macintosh');
browser._version = version.product.substring(version.product.indexOf('/') + 1);
browser._userAgent = version.userAgent;
// We don't trust the option as it may lie in case of connectOverCDP where remote browser
// may have been launched with different options.
browser.options.headful = !version.userAgent.includes('Headless');
if (!options.persistent) {
await session.send('Target.setAutoAttach', { autoAttach: true, waitForDebuggerOnStart: true, flatten: true });
return browser;
@ -123,6 +126,14 @@ export class CRBrowser extends Browser {
return this._userAgent;
}
_platform(): 'mac' | 'linux' | 'win' {
if (this._userAgent.includes('Windows'))
return 'win';
if (this._userAgent.includes('Macintosh'))
return 'mac';
return 'linux';
}
isClank(): boolean {
return this.options.name === 'clank';
}

View file

@ -15,32 +15,32 @@
* limitations under the License.
*/
import path from 'path';
import { eventsHelper, RegisteredListener } from '../../utils/eventsHelper';
import { registry } from '../../utils/registry';
import { rewriteErrorMessage } from '../../utils/stackTrace';
import { assert, createGuid, headersArrayToObject } from '../../utils/utils';
import * as dialog from '../dialog';
import * as dom from '../dom';
import * as frames from '../frames';
import { helper } from '../helper';
import { eventsHelper, RegisteredListener } from '../../utils/eventsHelper';
import * as network from '../network';
import { CRSession, CRConnection, CRSessionEvents } from './crConnection';
import { CRExecutionContext } from './crExecutionContext';
import { CRNetworkManager } from './crNetworkManager';
import { Page, Worker, PageBinding } from '../page';
import { Protocol } from './protocol';
import { toConsoleMessageLocation, exceptionToError, releaseObject } from './crProtocolHelper';
import * as dialog from '../dialog';
import { PageDelegate } from '../page';
import path from 'path';
import { RawMouseImpl, RawKeyboardImpl, RawTouchscreenImpl } from './crInput';
import { getAccessibilityTree } from './crAccessibility';
import { CRCoverage } from './crCoverage';
import { CRPDF } from './crPdf';
import { CRBrowserContext } from './crBrowser';
import * as types from '../types';
import { rewriteErrorMessage } from '../../utils/stackTrace';
import { assert, headersArrayToObject, createGuid } from '../../utils/utils';
import { VideoRecorder } from './videoRecorder';
import { Page, PageBinding, PageDelegate, Worker } from '../page';
import { Progress } from '../progress';
import * as types from '../types';
import { getAccessibilityTree } from './crAccessibility';
import { CRBrowserContext } from './crBrowser';
import { CRConnection, CRSession, CRSessionEvents } from './crConnection';
import { CRCoverage } from './crCoverage';
import { DragManager } from './crDragDrop';
import { registry } from '../../utils/registry';
import { CRExecutionContext } from './crExecutionContext';
import { RawKeyboardImpl, RawMouseImpl, RawTouchscreenImpl } from './crInput';
import { CRNetworkManager } from './crNetworkManager';
import { CRPDF } from './crPdf';
import { exceptionToError, releaseObject, toConsoleMessageLocation } from './crProtocolHelper';
import { platformToFontFamilies } from './defaultFontFamilies';
import { Protocol } from './protocol';
import { VideoRecorder } from './videoRecorder';
const UTILITY_WORLD_NAME = '__playwright_utility_world__';
@ -79,7 +79,7 @@ export class CRPage implements PageDelegate {
this._opener = opener;
this._isBackgroundPage = isBackgroundPage;
const dragManager = new DragManager(this);
this.rawKeyboard = new RawKeyboardImpl(client, browserContext._browser._isMac, dragManager);
this.rawKeyboard = new RawKeyboardImpl(client, browserContext._browser._platform() === 'mac', dragManager);
this.rawMouse = new RawMouseImpl(this, client, dragManager);
this.rawTouchscreen = new RawTouchscreenImpl(client);
this._pdf = new CRPDF(client);
@ -512,6 +512,8 @@ class FrameSession {
promises.push(emulateLocale(this._client, options.locale));
if (options.timezoneId)
promises.push(emulateTimezone(this._client, options.timezoneId));
if (!this._crPage._browserContext._browser.options.headful)
promises.push(this._setDefaultFontFamilies(this._client));
promises.push(this._updateGeolocation(true));
promises.push(this._updateExtraHTTPHeaders(true));
promises.push(this._updateRequestInterception());
@ -1017,6 +1019,11 @@ class FrameSession {
await this._client.send('Emulation.setEmulatedMedia', { media: this._page._state.mediaType || '', features });
}
private async _setDefaultFontFamilies(session: CRSession) {
const fontFamilies = platformToFontFamilies[this._crPage._browserContext._browser._platform()];
await session.send('Page.setFontFamilies', fontFamilies);
}
async _updateRequestInterception(): Promise<void> {
await this._networkManager.setRequestInterception(this._page._needsRequestInterception());
}

View file

@ -0,0 +1,157 @@
/**
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Protocol } from './protocol';
// DO NOT EDIT: this map is generated from Chromium source code by utils/generate_chromium_default_font_families.js
export const platformToFontFamilies: { [key in 'linux'|'mac'|'win']: Protocol.Page.setFontFamiliesParameters } = {
'linux': {
'fontFamilies': {
'standard': 'Times New Roman',
'fixed': 'Monospace',
'serif': 'Times New Roman',
'sansSerif': 'Arial',
'cursive': 'Comic Sans MS',
'fantasy': 'Impact',
'pictograph': 'Times New Roman'
}
},
'mac': {
'fontFamilies': {
'standard': 'Times',
'fixed': 'Courier',
'serif': 'Times',
'sansSerif': 'Helvetica',
'cursive': 'Apple Chancery',
'fantasy': 'Papyrus',
'pictograph': 'Apple Color Emoji'
},
'forScripts': [
{
'script': 'jpan',
'fontFamilies': {
'standard': 'Hiragino Kaku Gothic ProN',
'fixed': 'Osaka-Mono',
'serif': 'Hiragino Mincho ProN',
'sansSerif': 'Hiragino Kaku Gothic ProN'
}
},
{
'script': 'hang',
'fontFamilies': {
'standard': 'Apple SD Gothic Neo',
'serif': 'AppleMyungjo',
'sansSerif': 'Apple SD Gothic Neo'
}
},
{
'script': 'hans',
'fontFamilies': {
'standard': ',PingFang SC,STHeiti',
'serif': 'Songti SC',
'sansSerif': ',PingFang SC,STHeiti',
'cursive': 'Kaiti SC'
}
},
{
'script': 'hant',
'fontFamilies': {
'standard': ',PingFang TC,Heiti TC',
'serif': 'Songti TC',
'sansSerif': ',PingFang TC,Heiti TC',
'cursive': 'Kaiti TC'
}
}
]
},
'win': {
'fontFamilies': {
'standard': 'Times New Roman',
'fixed': 'Consolas',
'serif': 'Times New Roman',
'sansSerif': 'Arial',
'cursive': 'Comic Sans MS',
'fantasy': 'Impact',
'pictograph': 'Segoe UI Symbol'
},
'forScripts': [
{
'script': 'cyrl',
'fontFamilies': {
'standard': 'Times New Roman',
'fixed': 'Courier New',
'serif': 'Times New Roman',
'sansSerif': 'Arial'
}
},
{
'script': 'arab',
'fontFamilies': {
'fixed': 'Courier New',
'sansSerif': 'Segoe UI'
}
},
{
'script': 'grek',
'fontFamilies': {
'standard': 'Times New Roman',
'fixed': 'Courier New',
'serif': 'Times New Roman',
'sansSerif': 'Arial'
}
},
{
'script': 'jpan',
'fontFamilies': {
'standard': ',Meiryo,Yu Gothic',
'fixed': 'MS Gothic',
'serif': ',Yu Mincho,MS PMincho',
'sansSerif': ',Meiryo,Yu Gothic'
}
},
{
'script': 'hang',
'fontFamilies': {
'standard': 'Malgun Gothic',
'fixed': 'Gulimche',
'serif': 'Batang',
'sansSerif': 'Malgun Gothic',
'cursive': 'Gungsuh'
}
},
{
'script': 'hans',
'fontFamilies': {
'standard': 'Microsoft YaHei',
'fixed': 'NSimsun',
'serif': 'Simsun',
'sansSerif': 'Microsoft YaHei',
'cursive': 'KaiTi'
}
},
{
'script': 'hant',
'fontFamilies': {
'standard': 'Microsoft JhengHei',
'fixed': 'MingLiU',
'serif': 'PMingLiU',
'sansSerif': 'Microsoft JhengHei',
'cursive': 'DFKai-SB'
}
}
]
}
};

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,117 @@
/**
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// @ts-check
const xml2js = require('xml2js');
const fs = require('fs');
const path = require('path');
const { argv } = require('process');
// From https://source.chromium.org/chromium/chromium/src/+/main:chrome/browser/ui/prefs/prefs_tab_helper.cc;l=130;drc=62b77bef90de54f0136b51935fa2d5814a1b4da9
// and https://source.chromium.org/chromium/chromium/src/+/main:third_party/blink/renderer/platform/text/locale_to_script_mapping.cc;l=44;drc=befcb6de95fb8c88c162ce1f64111f6c17351b13
// note that some suffixes like _JAPANESE, _KOREAN don't have matching icu codes.
const codeToScriptName = new Map([
['ARABIC', 'arab'],
['CYRILLIC', 'cyrl'],
['GREEK', 'grek'],
['JAPANESE', 'jpan'],
['KOREAN', 'hang'],
['SIMPLIFIED_HAN', 'hans'],
['TRADITIONAL_HAN', 'hant'],
]);
const idToProtocol = new Map([
['IDS_STANDARD_FONT_FAMILY', 'standard'],
['IDS_SANS_SERIF_FONT_FAMILY','sansSerif'],
['IDS_SERIF_FONT_FAMILY', 'serif'],
['IDS_CURSIVE_FONT_FAMILY', 'cursive'],
['IDS_FANTASY_FONT_FAMILY', 'fantasy'],
['IDS_FIXED_FONT_FAMILY', 'fixed'],
['IDS_PICTOGRAPH_FONT_FAMILY', 'pictograph']
]);
class ScriptFontFamilies {
scriptToFontFamilies = new Map();
setFont(scriptName, familyName, value) {
let fontFamilies = this.scriptToFontFamilies.get(scriptName);
if (!fontFamilies) {
fontFamilies = {};
this.scriptToFontFamilies.set(scriptName, fontFamilies);
}
fontFamilies[familyName] = value;
}
toJSON() {
const forScripts = Array.from(this.scriptToFontFamilies.entries()).filter(([name, _]) => !!name).map(([script, fontFamilies]) => ({ script, fontFamilies }));
return {
fontFamilies: this.scriptToFontFamilies.get(''),
forScripts: forScripts.length ? forScripts : undefined
};
}
}
if (argv.length < 3)
throw new Error('Expected path to "chromium/src" checkout as first argument')
// Upstream files location is https://chromium.googlesource.com/chromium/src/+/main/chrome/app/resources/locale_settings_linux.grd
const resourceDir = path.join(argv[2], 'chrome/app/resources/');
if (!fs.existsSync(resourceDir))
throw new Error(`Path ${resourceDir} does not exist`);
function parseXML(xml) {
let result;
xml2js.parseString(xml, {trim: true}, (err, r) => result = r);
return result;
}
const result = {};
for (const platform of ['linux', 'mac', 'win']) {
const f = path.join(resourceDir, `locale_settings_${platform}.grd`);
const xmlDataStr = fs.readFileSync(f);
let jsonObj = parseXML(xmlDataStr);
if (!jsonObj)
throw new Error('Failed to parse ' + f);
const fontFamilies = new ScriptFontFamilies();
const defaults = jsonObj.grit.release[0].messages[0].message;
defaults.forEach(e => {
const name = e['$']['name'];
let scriptName = '';
let familyName;
for (const id of idToProtocol.keys()) {
if (!name.startsWith(id))
continue;
familyName = idToProtocol.get(id);
if (name !== id) {
const suffix = name.substring(id.length + 1);
// We don't support this, see https://source.chromium.org/chromium/chromium/src/+/main:chrome/browser/ui/prefs/prefs_tab_helper.cc;l=384-390;drc=62b77bef90de54f0136b51935fa2d5814a1b4da9
if (suffix === 'ALT_WIN')
continue;
scriptName = codeToScriptName.get(suffix);
if (!scriptName)
throw new Error('NO Script name for: ' + suffix);
}
break;
}
// Skip things like IDS_NTP_FONT_FAMILY, IDS_MINIMUM_FONT_SIZE etc.
if (!familyName)
return;
fontFamilies.setFont(scriptName, familyName, e['_'])
});
result[platform] = fontFamilies.toJSON();
}
console.log(JSON.stringify(result, null, 2).replaceAll('"', `'`));