blob: 844eb92f95eaf5f2406eb60a8cccb78a5c8484b3 [file] [log] [blame]
'use strict';
const TEST_SIZE_CATEGORY = {
// Fonts with file smaller than 1MiB.
small: 'small',
// Fonts with file between 1 and 20MiB.
medium: 'medium',
// Fonts with file larger than 20MiB.
large: 'large',
};
const MAC_FONTS = [
{
postscriptName: 'Monaco',
fullName: 'Monaco',
family: 'Monaco',
style: 'Regular',
label: TEST_SIZE_CATEGORY.small,
expectedTables: [
// Tables related to TrueType.
'cvt ',
'glyf',
'loca',
'prep',
'gasp',
],
},
{
postscriptName: 'Menlo-Regular',
fullName: 'Menlo Regular',
family: 'Menlo',
style: 'Regular',
label: TEST_SIZE_CATEGORY.medium,
expectedTables: [
'cvt ',
'glyf',
'loca',
'prep',
],
},
{
postscriptName: 'Menlo-Bold',
fullName: 'Menlo Bold',
family: 'Menlo',
style: 'Bold',
label: TEST_SIZE_CATEGORY.medium,
expectedTables: [
'cvt ',
'glyf',
'loca',
'prep',
],
},
{
postscriptName: 'Menlo-BoldItalic',
fullName: 'Menlo Bold Italic',
family: 'Menlo',
style: 'Bold Italic',
label: TEST_SIZE_CATEGORY.medium,
expectedTables: [
'cvt ',
'glyf',
'loca',
'prep',
],
},
// Indic.
{
postscriptName: 'GujaratiMT',
fullName: 'Gujarati MT',
family: 'Gujarati MT',
style: 'Regular',
label: TEST_SIZE_CATEGORY.small,
expectedTables: [
'cvt ',
'glyf',
'loca',
'prep',
],
},
{
postscriptName: 'GujaratiMT-Bold',
fullName: 'Gujarati MT Bold',
family: 'Gujarati MT',
style: 'Bold',
label: TEST_SIZE_CATEGORY.small,
expectedTables: [
'cvt ',
'glyf',
'loca',
'prep',
],
},
{
postscriptName: 'DevanagariMT',
fullName: 'Devanagari MT',
family: 'Devanagari MT',
style: 'Regular',
label: TEST_SIZE_CATEGORY.small,
expectedTables: [
'cvt ',
'glyf',
'loca',
'prep',
],
},
{
postscriptName: 'DevanagariMT-Bold',
fullName: 'Devanagari MT Bold',
family: 'Devanagari MT',
style: 'Bold',
label: TEST_SIZE_CATEGORY.small,
expectedTables: [
'cvt ',
'glyf',
'loca',
'prep',
],
},
// Japanese.
{
postscriptName: 'HiraMinProN-W3',
fullName: 'Hiragino Mincho ProN W3',
family: 'Hiragino Mincho ProN',
style: 'Regular',
label: TEST_SIZE_CATEGORY.medium,
expectedTables: [
'CFF ',
'VORG',
],
},
{
postscriptName: 'HiraMinProN-W6',
fullName: 'Hiragino Mincho ProN W6',
family: 'Hiragino Mincho ProN',
style: 'Regular',
label: TEST_SIZE_CATEGORY.medium,
expectedTables: [
'CFF ',
'VORG',
],
},
// Korean.
{
postscriptName: 'AppleGothic',
fullName: 'AppleGothic Regular',
family: 'AppleGothic',
style: 'Regular',
label: TEST_SIZE_CATEGORY.medium,
expectedTables: [
'cvt ',
'glyf',
'loca',
],
},
{
postscriptName: 'AppleMyungjo',
fullName: 'AppleMyungjo Regular',
family: 'AppleMyungjo',
style: 'Regular',
label: TEST_SIZE_CATEGORY.medium,
expectedTables: [
'cvt ',
'glyf',
'loca',
],
},
// Chinese.
{
postscriptName: 'STHeitiTC-Light',
fullName: 'Heiti TC Light',
family: 'Heiti TC',
style: 'Light',
label: TEST_SIZE_CATEGORY.large,
expectedTables: [
'cvt ',
'glyf',
'loca',
'prep',
],
},
{
postscriptName: 'STHeitiTC-Medium',
fullName: 'Heiti TC Medium',
family: 'Heiti TC',
style: 'Medium',
label: TEST_SIZE_CATEGORY.large,
expectedTables: [
'cvt ',
'glyf',
'loca',
'prep',
],
},
// Bitmap.
{
postscriptName: 'AppleColorEmoji',
fullName: 'Apple Color Emoji',
family: 'Apple Color Emoji',
style: 'Regular',
label: TEST_SIZE_CATEGORY.large,
expectedTables: [
'glyf',
'loca',
// Tables related to Bitmap Glyphs.
'sbix',
],
},
];
const WIN_FONTS = [
{
postscriptName: 'Verdana',
fullName: 'Verdana',
family: 'Verdana',
style: 'Regular',
label: TEST_SIZE_CATEGORY.small,
expectedTables: [
// Tables related to TrueType.
'cvt ',
'glyf',
'loca',
'prep',
'gasp',
],
},
{
postscriptName: 'Verdana-Bold',
fullName: 'Verdana Bold',
family: 'Verdana',
style: 'Bold',
label: TEST_SIZE_CATEGORY.small,
expectedTables: [
// Tables related to TrueType.
'cvt ',
'glyf',
'loca',
'prep',
'gasp',
],
},
{
postscriptName: 'Verdana-Italic',
fullName: 'Verdana Italic',
family: 'Verdana',
style: 'Italic',
label: TEST_SIZE_CATEGORY.small,
expectedTables: [
// Tables related to TrueType.
'cvt ',
'glyf',
'loca',
'prep',
'gasp',
],
},
// Korean.
{
postscriptName: 'MalgunGothicBold',
fullName: 'Malgun Gothic Bold',
family: 'Malgun Gothic',
style: 'Bold',
label: TEST_SIZE_CATEGORY.medium,
expectedTables: [
// Tables related to TrueType.
'cvt ',
'glyf',
'loca',
'prep',
'gasp',
],
},
// Chinese.
{
postscriptName: 'MicrosoftYaHei',
fullName: 'Microsoft YaHei',
family: 'Microsoft YaHei',
style: 'Regular',
label: TEST_SIZE_CATEGORY.medium,
},
{
postscriptName: 'MicrosoftYaHei-Bold',
fullName: 'Microsoft YaHei Bold',
family: 'Microsoft YaHei',
style: 'Bold',
label: TEST_SIZE_CATEGORY.medium,
},
];
const LINUX_FONTS = [
{
postscriptName: 'Ahem',
fullName: 'Ahem',
family: 'Ahem',
style: 'Regular',
label: TEST_SIZE_CATEGORY.small,
expectedTables: [
// Tables related to TrueType.
'cvt ',
'glyf',
'loca',
'prep',
'gasp',
],
},
];
// The OpenType spec mentions that the follow tables are required for a font to
// function correctly. We'll have all the tables listed except for OS/2, which
// is not present in all fonts on Mac OS.
// https://docs.microsoft.com/en-us/typography/opentype/spec/otff#font-tables
const BASE_TABLES = [
'cmap',
'head',
'hhea',
'hmtx',
'maxp',
'name',
'post',
];
function getEnumerationTestSet(options) {
options = Object.assign({
labelFilter: [],
}, options);
// Verify (by font family) that some standard fonts have been returned.
let platform;
if (navigator.platform.indexOf("Win") !== -1) {
platform = 'win';
} else if (navigator.platform.indexOf("Mac") !== -1) {
platform = 'mac';
} else if (navigator.platform.indexOf("Linux") !== -1) {
platform = 'linux';
} else {
platform = 'generic';
}
assert_not_equals(platform, 'generic', 'Platform must be detected.');
let output = [];
if (platform === 'mac') {
output = MAC_FONTS;
} else if (platform === 'win') {
output = WIN_FONTS;
} else if (platform === 'linux') {
// Also includes ChromeOS, on which navigator.platform starts with 'Linux'.
output = LINUX_FONTS;
}
if (options.labelFilter.length && output.length) {
const labelFilter = new Set(options.labelFilter);
output = output.filter(f => labelFilter.has(f.label));
}
return output;
}
function getMoreExpectedTables(expectations) {
const output = {};
for (const f of expectations) {
if (f.expectedTables) {
output[f.postscriptName] = f.expectedTables;
}
}
return output;
}
async function filterEnumeration(fonts, expectedFonts) {
const nameSet = new Set();
for (const e of expectedFonts) {
nameSet.add(e.postscriptName);
}
const output = [];
for (const f of fonts) {
if (nameSet.has(f.postscriptName)) {
output.push(f);
}
}
const numGot = output.length;
const numExpected = Object.keys(expectedFonts).length;
assert_equals(numGot, numExpected, `Got ${numGot} fonts, expected ${numExpected}.`);
return output;
}
function assert_fonts_exist(availableFonts, expectedFonts) {
const postscriptNameSet = new Set();
const fullNameSet = new Set();
const familySet = new Set();
const styleSet = new Set();
for (const f of expectedFonts) {
postscriptNameSet.add(f.postscriptName);
fullNameSet.add(f.fullName);
familySet.add(f.family);
styleSet.add(f.style);
}
for (const f of availableFonts) {
postscriptNameSet.delete(f.postscriptName);
fullNameSet.delete(f.fullName);
familySet.delete(f.family);
styleSet.delete(f.style);
}
assert_equals(postscriptNameSet.size, 0,
`Missing Postscript names: ${setToString(postscriptNameSet)}.`);
assert_equals(fullNameSet.size, 0,
`Missing Full names: ${setToString(fullNameSet)}.`);
assert_equals(familySet.size, 0,
`Missing Families: ${setToString(familySet)}.`);
assert_equals(styleSet.size, 0, `Missing Styles: ${setToString(styleSet)}.`);
}
function assert_postscript_name_exists(
availableFonts, postscriptNameSelection) {
const postscriptNameSet = new Set(postscriptNameSelection);
const foundFonts = [];
for (const f of availableFonts) {
if (postscriptNameSet.has(f.postscriptName)) {
foundFonts.push(f.postscriptName);
}
}
assert_equals(
foundFonts.length, postscriptNameSelection.length,
`Expected to only find selected fonts ${
JSON.stringify(postscriptNameSelection)}. Instead found: ${
JSON.stringify(foundFonts)}`);
}
function assert_font_has_tables(name, tables, expectedTables) {
for (const t of expectedTables) {
assert_equals(t.length, 4,
"Table names are always 4 characters long.");
assert_true(tables.has(t),
`Font ${name} did not have required table ${t}.`);
assert_greater_than(tables.get(t).size, 0,
`Font ${name} has table ${t} of size 0.`);
}
}
function setToString(set) {
const items = Array.from(set);
return JSON.stringify(items);
}
async function parseFontData(fontBlob) {
const fontInfo = {
errors: [],
numTables: 0,
};
const versionTag = await getTag(fontBlob, 0);
fontInfo.version = sfntVersionInfo(versionTag);
if (fontInfo.version === 'UNKNOWN') {
fontInfo.errors.push(`versionTag: "${versionTag}"`);
}
const numTables = await getUint16(fontBlob, 4);
[fontInfo.tables, fontInfo.tableMeta] = await getTableData(fontBlob, numTables);
return fontInfo;
}
async function getTableData(fontBlob, numTables) {
const dataMap = new Map();
const metaMap = new Map();
let blobOffset = 12;
for (let i = 0; i < numTables; i++) {
const tag = await getTag(fontBlob, blobOffset);
const checksum = await getUint32(fontBlob, blobOffset + 4);
const offset = await getUint32(fontBlob, blobOffset + 8);
const size = await getUint32(fontBlob, blobOffset + 12);
const tableBlob = fontBlob.slice(offset, offset + size);
dataMap.set(tag, tableBlob);
metaMap.set(tag, {checksum, offset, size});
blobOffset += 16;
}
return [dataMap, metaMap];
}
function sfntVersionInfo(version) {
// Spec: https://docs.microsoft.com/en-us/typography/opentype/spec/otff#organization-of-an-opentype-font
switch (version) {
case '\x00\x01\x00\x00':
case 'true':
case 'typ1':
return 'truetype';
case 'OTTO':
return 'cff';
default:
return 'UNKNOWN';
}
}
async function getTag(blob, offset) {
return (new TextDecoder).decode(
await blob.slice(offset, offset + 4).arrayBuffer());
}
async function getUint16(blob, offset) {
const slice = blob.slice(offset, offset + 2);
const buf = await slice.arrayBuffer();
const dataView = new DataView(buf);
return dataView.getUint16(0);
}
async function getUint32(blob, offset) {
const slice = blob.slice(offset, offset + 4);
const buf = await slice.arrayBuffer();
const dataView = new DataView(buf);
return dataView.getUint32(0);
}
function promiseDocumentReady() {
return new Promise(resolve => {
if (document.readyState === 'complete') {
resolve();
}
window.addEventListener('load', () => {
resolve();
}, {once: true});
});
}
function isPlatformSupported() {
if (navigator.platform.indexOf('Mac') != -1 ||
navigator.platform.indexOf('Win') != -1 ||
navigator.platform.indexOf('Linux') != -1) {
return true;
}
return false;
}
async function simulateUserActivation() {
await promiseDocumentReady();
return new Promise(resolve => {
const button = document.createElement('button');
button.textContent = 'Click to enumerate fonts';
button.style.fontSize = '40px';
button.onclick = () => {
document.body.removeChild(button);
resolve();
};
document.body.appendChild(button);
test_driver.click(button);
});
}
function font_access_test(test_function, name, properties) {
return promise_test(async (t) => {
if (!isPlatformSupported()) {
await promise_rejects_dom(
t, 'NotSupportedError', navigator.fonts.query());
return;
}
await test_driver.set_permission({name: 'font-access'}, 'granted');
await simulateUserActivation();
await test_function(t, name, properties);
});
}