Skip to content

Commit 81f5835

Browse files
authored
Merge pull request #10539 from Snuffleupagus/fallback-disableFontFace-v2
[api-minor] Fallback to the built-in font renderer when font loading fails
2 parents a045a00 + b6d090c commit 81f5835

File tree

9 files changed

+165
-107
lines changed

9 files changed

+165
-107
lines changed

src/core/document.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -666,6 +666,10 @@ class PDFDocument {
666666
});
667667
}
668668

669+
fontFallback(id, handler) {
670+
return this.catalog.fontFallback(id, handler);
671+
}
672+
669673
cleanup() {
670674
return this.catalog.cleanup();
671675
}

src/core/evaluator.js

Lines changed: 53 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -610,37 +610,18 @@ var PartialEvaluator = (function PartialEvaluatorClosure() {
610610
});
611611
},
612612

613-
handleText: function PartialEvaluator_handleText(chars, state) {
614-
var font = state.font;
615-
var glyphs = font.charsToGlyphs(chars);
616-
var isAddToPathSet = !!(state.textRenderingMode &
617-
TextRenderingMode.ADD_TO_PATH_FLAG);
618-
if (font.data && (isAddToPathSet || this.options.disableFontFace ||
619-
state.fillColorSpace.name === 'Pattern')) {
620-
var buildPath = (fontChar) => {
621-
if (!font.renderer.hasBuiltPath(fontChar)) {
622-
var path = font.renderer.getPathJs(fontChar);
623-
this.handler.send('commonobj', [
624-
font.loadedName + '_path_' + fontChar,
625-
'FontPath',
626-
path
627-
]);
628-
}
629-
};
630-
631-
for (var i = 0, ii = glyphs.length; i < ii; i++) {
632-
var glyph = glyphs[i];
633-
buildPath(glyph.fontChar);
613+
handleText(chars, state) {
614+
const font = state.font;
615+
const glyphs = font.charsToGlyphs(chars);
634616

635-
// If the glyph has an accent we need to build a path for its
636-
// fontChar too, otherwise CanvasGraphics_paintChar will fail.
637-
var accent = glyph.accent;
638-
if (accent && accent.fontChar) {
639-
buildPath(accent.fontChar);
640-
}
617+
if (font.data) {
618+
const isAddToPathSet = !!(state.textRenderingMode &
619+
TextRenderingMode.ADD_TO_PATH_FLAG);
620+
if (isAddToPathSet || state.fillColorSpace.name === 'Pattern' ||
621+
font.disableFontFace || this.options.disableFontFace) {
622+
PartialEvaluator.buildFontPaths(font, glyphs, this.handler);
641623
}
642624
}
643-
644625
return glyphs;
645626
},
646627

@@ -2623,6 +2604,30 @@ var PartialEvaluator = (function PartialEvaluatorClosure() {
26232604
},
26242605
};
26252606

2607+
PartialEvaluator.buildFontPaths = function(font, glyphs, handler) {
2608+
function buildPath(fontChar) {
2609+
if (font.renderer.hasBuiltPath(fontChar)) {
2610+
return;
2611+
}
2612+
handler.send('commonobj', [
2613+
`${font.loadedName}_path_${fontChar}`,
2614+
'FontPath',
2615+
font.renderer.getPathJs(fontChar),
2616+
]);
2617+
}
2618+
2619+
for (const glyph of glyphs) {
2620+
buildPath(glyph.fontChar);
2621+
2622+
// If the glyph has an accent we need to build a path for its
2623+
// fontChar too, otherwise CanvasGraphics_paintChar will fail.
2624+
const accent = glyph.accent;
2625+
if (accent && accent.fontChar) {
2626+
buildPath(accent.fontChar);
2627+
}
2628+
}
2629+
};
2630+
26262631
return PartialEvaluator;
26272632
})();
26282633

@@ -2639,14 +2644,31 @@ var TranslatedFont = (function TranslatedFontClosure() {
26392644
if (this.sent) {
26402645
return;
26412646
}
2642-
var fontData = this.font.exportData();
2647+
this.sent = true;
2648+
26432649
handler.send('commonobj', [
26442650
this.loadedName,
26452651
'Font',
2646-
fontData
2652+
this.font.exportData(),
26472653
]);
2648-
this.sent = true;
26492654
},
2655+
2656+
fallback(handler) {
2657+
if (!this.font.data) {
2658+
return;
2659+
}
2660+
// When font loading failed, fall back to the built-in font renderer.
2661+
this.font.disableFontFace = true;
2662+
// An arbitrary number of text rendering operators could have been
2663+
// encountered between the point in time when the 'Font' message was sent
2664+
// to the main-thread, and the point in time when the 'FontFallback'
2665+
// message was received on the worker-thread.
2666+
// To ensure that all 'FontPath's are available on the main-thread, when
2667+
// font loading failed, attempt to resend *all* previously parsed glyphs.
2668+
const glyphs = this.font.glyphCacheValues;
2669+
PartialEvaluator.buildFontPaths(this.font, glyphs, handler);
2670+
},
2671+
26502672
loadType3Data(evaluator, resources, parentOperatorList, task) {
26512673
if (!this.font.isType3Font) {
26522674
throw new Error('Must be a Type3 font.');

src/core/fonts.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1159,6 +1159,8 @@ var Font = (function FontClosure() {
11591159
font: null,
11601160
mimetype: null,
11611161
encoding: null,
1162+
disableFontFace: false,
1163+
11621164
get renderer() {
11631165
var renderer = FontRendererFactory.create(this, SEAC_ANALYSIS_ENABLED);
11641166
return shadow(this, 'renderer', renderer);
@@ -2944,6 +2946,10 @@ var Font = (function FontClosure() {
29442946
// Enter the translated string into the cache
29452947
return (charsCache[charsCacheKey] = glyphs);
29462948
},
2949+
2950+
get glyphCacheValues() {
2951+
return Object.values(this.glyphCache);
2952+
},
29472953
};
29482954

29492955
return Font;

src/core/obj.js

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -490,6 +490,22 @@ class Catalog {
490490
return shadow(this, 'javaScript', javaScript);
491491
}
492492

493+
fontFallback(id, handler) {
494+
const promises = [];
495+
this.fontCache.forEach(function(promise) {
496+
promises.push(promise);
497+
});
498+
499+
return Promise.all(promises).then((translatedFonts) => {
500+
for (const translatedFont of translatedFonts) {
501+
if (translatedFont.loadedName === id) {
502+
translatedFont.fallback(handler);
503+
return;
504+
}
505+
}
506+
});
507+
}
508+
493509
cleanup() {
494510
this.pageKidsCountCache.clear();
495511

src/core/pdf_manager.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,10 @@ class BasePdfManager {
6868
return this.pdfDocument.getPage(pageIndex);
6969
}
7070

71+
fontFallback(id, handler) {
72+
return this.pdfDocument.fontFallback(id, handler);
73+
}
74+
7175
cleanup() {
7276
return this.pdfDocument.cleanup();
7377
}

src/core/worker.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -667,6 +667,10 @@ var WorkerMessageHandler = {
667667
});
668668
});
669669

670+
handler.on('FontFallback', function(data) {
671+
return pdfManager.fontFallback(data.id, handler);
672+
});
673+
670674
handler.on('Cleanup', function wphCleanup(data) {
671675
return pdfManager.cleanup();
672676
});

src/display/api.js

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1676,7 +1676,10 @@ class WorkerTransport {
16761676
this.messageHandler = messageHandler;
16771677
this.loadingTask = loadingTask;
16781678
this.commonObjs = new PDFObjects();
1679-
this.fontLoader = new FontLoader(loadingTask.docId);
1679+
this.fontLoader = new FontLoader({
1680+
docId: loadingTask.docId,
1681+
onUnsupportedFeature: this._onUnsupportedFeature.bind(this),
1682+
});
16801683
this._params = params;
16811684
this.CMapReaderFactory = new params.CMapReaderFactory({
16821685
baseUrl: params.cMapUrl,
@@ -1944,11 +1947,16 @@ class WorkerTransport {
19441947
onUnsupportedFeature: this._onUnsupportedFeature.bind(this),
19451948
fontRegistry,
19461949
});
1947-
const fontReady = (fontObjs) => {
1948-
this.commonObjs.resolve(id, font);
1949-
};
19501950

1951-
this.fontLoader.bind([font], fontReady);
1951+
this.fontLoader.bind(font).then(() => {
1952+
this.commonObjs.resolve(id, font);
1953+
}, (reason) => {
1954+
messageHandler.sendWithPromise('FontFallback', {
1955+
id,
1956+
}).finally(() => {
1957+
this.commonObjs.resolve(id, font);
1958+
});
1959+
});
19521960
break;
19531961
case 'FontPath':
19541962
this.commonObjs.resolve(id, exportedData);

src/display/font_loader.js

Lines changed: 62 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -19,18 +19,15 @@ import {
1919
} from '../shared/util';
2020

2121
class BaseFontLoader {
22-
constructor(docId) {
22+
constructor({ docId, onUnsupportedFeature, }) {
2323
if (this.constructor === BaseFontLoader) {
2424
unreachable('Cannot initialize BaseFontLoader.');
2525
}
2626
this.docId = docId;
27+
this._onUnsupportedFeature = onUnsupportedFeature;
2728

2829
this.nativeFontFaces = [];
2930
this.styleElement = null;
30-
this.loadingContext = {
31-
requests: [],
32-
nextRequestId: 0,
33-
};
3431
}
3532

3633
addNativeFontFace(nativeFontFace) {
@@ -64,72 +61,48 @@ class BaseFontLoader {
6461
}
6562
}
6663

67-
bind(fonts, callback) {
68-
const rules = [];
69-
const fontsToLoad = [];
70-
const fontLoadPromises = [];
71-
const getNativeFontPromise = function(nativeFontFace) {
72-
// Return a promise that is always fulfilled, even when the font fails to
73-
// load.
74-
return nativeFontFace.loaded.catch(function(reason) {
75-
warn(`Failed to load font "${nativeFontFace.family}": ${reason}`);
76-
});
77-
};
78-
79-
for (const font of fonts) {
80-
// Add the font to the DOM only once; skip if the font is already loaded.
81-
if (font.attached || font.missingFile) {
82-
continue;
83-
}
84-
font.attached = true;
85-
86-
if (this.isFontLoadingAPISupported) {
87-
const nativeFontFace = font.createNativeFontFace();
88-
if (nativeFontFace) {
89-
this.addNativeFontFace(nativeFontFace);
90-
fontLoadPromises.push(getNativeFontPromise(nativeFontFace));
91-
}
92-
} else {
93-
const rule = font.createFontFaceRule();
94-
if (rule) {
95-
this.insertRule(rule);
96-
rules.push(rule);
97-
fontsToLoad.push(font);
98-
}
99-
}
64+
async bind(font) {
65+
// Add the font to the DOM only once; skip if the font is already loaded.
66+
if (font.attached || font.missingFile) {
67+
return;
10068
}
69+
font.attached = true;
10170

102-
const request = this._queueLoadingCallback(callback);
10371
if (this.isFontLoadingAPISupported) {
104-
Promise.all(fontLoadPromises).then(request.complete);
105-
} else if (rules.length > 0 && !this.isSyncFontLoadingSupported) {
106-
this._prepareFontLoadEvent(rules, fontsToLoad, request);
107-
} else {
108-
request.complete();
72+
const nativeFontFace = font.createNativeFontFace();
73+
if (nativeFontFace) {
74+
this.addNativeFontFace(nativeFontFace);
75+
try {
76+
await nativeFontFace.loaded;
77+
} catch (ex) {
78+
this._onUnsupportedFeature({ featureId: UNSUPPORTED_FEATURES.font, });
79+
warn(`Failed to load font '${nativeFontFace.family}': '${ex}'.`);
80+
81+
// When font loading failed, fall back to the built-in font renderer.
82+
font.disableFontFace = true;
83+
throw ex;
84+
}
85+
}
86+
return; // The font was, asynchronously, loaded.
10987
}
110-
}
11188

112-
_queueLoadingCallback(callback) {
113-
function completeRequest() {
114-
assert(!request.done, 'completeRequest() cannot be called twice.');
115-
request.done = true;
89+
// !this.isFontLoadingAPISupported
90+
const rule = font.createFontFaceRule();
91+
if (rule) {
92+
this.insertRule(rule);
11693

117-
// Sending all completed requests in order of how they were queued.
118-
while (context.requests.length > 0 && context.requests[0].done) {
119-
const otherRequest = context.requests.shift();
120-
setTimeout(otherRequest.callback, 0);
94+
if (this.isSyncFontLoadingSupported) {
95+
return; // The font was, synchronously, loaded.
12196
}
97+
return new Promise((resolve) => {
98+
const request = this._queueLoadingCallback(resolve);
99+
this._prepareFontLoadEvent([rule], [font], request);
100+
});
122101
}
102+
}
123103

124-
const context = this.loadingContext;
125-
const request = {
126-
id: `pdfjs-font-loading-${context.nextRequestId++}`,
127-
done: false,
128-
complete: completeRequest,
129-
callback,
130-
};
131-
context.requests.push(request);
132-
return request;
104+
_queueLoadingCallback(callback) {
105+
unreachable('Abstract method `_queueLoadingCallback`.');
133106
}
134107

135108
get isFontLoadingAPISupported() {
@@ -168,6 +141,10 @@ FontLoader = class MozcentralFontLoader extends BaseFontLoader {
168141
FontLoader = class GenericFontLoader extends BaseFontLoader {
169142
constructor(docId) {
170143
super(docId);
144+
this.loadingContext = {
145+
requests: [],
146+
nextRequestId: 0,
147+
};
171148
this.loadTestFontId = 0;
172149
}
173150

@@ -205,6 +182,29 @@ FontLoader = class GenericFontLoader extends BaseFontLoader {
205182
return shadow(this, 'isSyncFontLoadingSupported', supported);
206183
}
207184

185+
_queueLoadingCallback(callback) {
186+
function completeRequest() {
187+
assert(!request.done, 'completeRequest() cannot be called twice.');
188+
request.done = true;
189+
190+
// Sending all completed requests in order of how they were queued.
191+
while (context.requests.length > 0 && context.requests[0].done) {
192+
const otherRequest = context.requests.shift();
193+
setTimeout(otherRequest.callback, 0);
194+
}
195+
}
196+
197+
const context = this.loadingContext;
198+
const request = {
199+
id: `pdfjs-font-loading-${context.nextRequestId++}`,
200+
done: false,
201+
complete: completeRequest,
202+
callback,
203+
};
204+
context.requests.push(request);
205+
return request;
206+
}
207+
208208
get _loadTestFont() {
209209
const getLoadTestFont = function() {
210210
// This is a CFF font with 1 glyph for '.' that fills its entire width and

0 commit comments

Comments
 (0)