Skip to content
This repository was archived by the owner on May 7, 2025. It is now read-only.
This repository was archived by the owner on May 7, 2025. It is now read-only.

FAQ: custom external font faces #429

@danielweck

Description

@danielweck

In the reader's settings, the fontSelection index is zero-based and indicates the chosen item from an array of choices (zero means "default publisher font", other integers mean "custom reading system font"):

/**
*
* @property fontSelection
* @type number
*/
this.fontSelection = 0;

The reader's array of custom fonts is provided at construction time:

this.fonts = options.fonts;

Here is a concrete example of such array, as configured in ReadiumJS (cloud / web reader + Chrome app / extension):

https://github.com/readium/readium-js-viewer/blob/2a41a144ad75cb33058c0684a817aa2fe332f907/src/fonts/fonts.js#L1-L42

Note that although the url field of each JSON font object can be a relative path, in fact the getFontFaces() function at the bottom of the Javascript source file takes a URLprefix parameter to complete the final array of fonts with absolute URLs. In other words, a native app must ensure that each font's HTTP URL resolves to the correct payload, which is typically a CSS file that declares the font faces, for example OpenDyslexic:

https://github.com/readium/readium-js-viewer/tree/2a41a144ad75cb33058c0684a817aa2fe332f907/src/fonts/OpenDyslexic

The fontFamily string of characters in the JSON font object is meant to be used as-is in a CSS font-family statement, and will be applied automatically by the readium-shared-js layout / rendering engine when a the fontSelection setting changes:

/**
* Updates reader view based on the settings specified in settingsData object
*
* @param {Globals.Views.ReaderView.SettingsData} settingsData Settings data
* @fires Globals.Events.SETTINGS_APPLIED
*/
this.updateSettings = function (settingsData) {
//console.debug("UpdateSettings: " + JSON.stringify(settingsData));
_viewerSettings.update(settingsData);
if (_mediaOverlayPlayer) {
_mediaOverlayPlayer.setAutomaticNextSmil(_viewerSettings.mediaOverlaysAutomaticPageTurn ? true : false);
}
if (_currentView && !settingsData.doNotUpdateView) {
var bookMark = _currentView.bookmarkCurrentPage();
if (bookMark && bookMark.idref) {
var wasPlaying = false;
if (_currentView.isReflowable && _currentView.isReflowable()) {
wasPlaying = self.isPlayingMediaOverlay();
if (wasPlaying) {
self.pauseMediaOverlay();
}
}
var spineItem = _spine.getItemById(bookMark.idref);
initViewForItem(spineItem, function (isViewChanged) {
if (!isViewChanged) {
var docWillChange = false;
_currentView.setViewSettings(_viewerSettings, docWillChange);
}
self.once(ReadiumSDK.Events.PAGINATION_CHANGED, function (pageChangeData)
{
var cfi = new BookmarkData(bookMark.idref, bookMark.contentCFI);
self.debugBookmarkData(cfi);
});
self.openSpineItemElementCfi(bookMark.idref, bookMark.contentCFI, self);
if (wasPlaying) {
self.playMediaOverlay();
// setTimeout(function()
// {
// }, 60);
}
Globals.logEvent("SETTINGS_APPLIED 1 (view update)", "EMIT", "reader_view.js");
self.emit(Globals.Events.SETTINGS_APPLIED);
});
return;
}
}
Globals.logEvent("SETTINGS_APPLIED 2 (no view update)", "EMIT", "reader_view.js");
self.emit(Globals.Events.SETTINGS_APPLIED);
};

The actual DOM/CSS logic is implemented in:

/**
*
* @param $epubHtml: The html that is to have font attributes added.
* @param fontSize: The font size that is to be added to the element at all locations.
* @param fontObj: The font Object containing at minimum the URL, and fontFamilyName (In fields url and fontFamily) respectively. Pass in null's on the object's fields to signal no font.
* @param callback: function invoked when "done", which means that if there are asynchronous operations such as font-face loading via injected stylesheets, then the UpdateHtmlFontAttributes() function returns immediately but the caller should wait for the callback function call if fully-loaded font-face *stylesheets* are required on the caller's side (note that the caller's side may still need to detect *actual font loading*, via the FontLoader API or some sort of ResizeSensor to indicate that the updated font-family has been used to render the document).
*/
Helpers.UpdateHtmlFontAttributes = function ($epubHtml, fontSize, fontObj, callback) {
var FONT_FAMILY_ID = "readium_font_family_link";
var docHead = $("head", $epubHtml);
var link = $("#" + FONT_FAMILY_ID, docHead);
const NOTHING = 0, ADD = 1, REMOVE = 2; //Types for css font family.
var changeFontFamily = NOTHING;
var fontLoadCallback = function() {
var perf = false;
// TODO: very slow on Firefox!
// See https://github.com/readium/readium-shared-js/issues/274
if (perf) var time1 = window.performance.now();
if (changeFontFamily != NOTHING) {
var fontFamilyStyle = $("style#readium-fontFamily", docHead);
if (fontFamilyStyle && fontFamilyStyle[0]) {
// REMOVE, or ADD (because we remove before re-adding from scratch)
docHead[0].removeChild(fontFamilyStyle[0]);
}
if (changeFontFamily == ADD) {
var style = $epubHtml[0].ownerDocument.createElement('style');
style.setAttribute("id", "readium-fontFamily");
style.appendChild($epubHtml[0].ownerDocument.createTextNode('html * { font-family: "'+fontObj.fontFamily+'" !important; }')); // this technique works for text-align too (e.g. text-align: justify !important;)
docHead[0].appendChild(style);
//fontFamilyStyle = $(style);
}
}
// The code below does not work because jQuery $element.css() on html.body somehow "resets" the font: CSS directive by removing it entirely (font-family: works with !important, but unfortunately further deep inside the DOM there may be CSS applied with the font: directive, which somehow seems to take precedence! ... as shown in Chrome's developer tools)
// ...thus why we use the above routine instead, to insert a new head>style element
// // var doc = $epubHtml[0].ownerDocument;
// // var body = doc.body;
// var $body = $("body", $epubHtml);
// // $body.css({
// // "font-size" : fontSize + "%",
// // "font-family" : ""
// // });
// $body.css("font-family", "");
// if (changeFontFamily == ADD) {
// var existing = $body.attr("style");
// $body[0].setAttribute("style",
// existing + " ; font-family: '" + fontObj.fontFamily + "' !important ;" + " ; font: regular 100% '" + fontObj.fontFamily + "' !important ;");
// }
var factor = fontSize / 100;
var win = $epubHtml[0].ownerDocument.defaultView;
if (!win) {
console.log("NIL $epubHtml[0].ownerDocument.defaultView");
return;
}
// TODO: is this a complete list? Is there a better way to do this?
//https://github.com/readium/readium-shared-js/issues/336
// Note that font-family is handled differently, using an injected stylesheet with a catch-all selector that pushes an "!important" CSS value in the document's cascade.
var $textblocks = $('p, div, span, h1, h2, h3, h4, h5, h6, li, blockquote, td, pre, dt, dd, code, a', $epubHtml); // excludes section, body etc.
// need to do two passes because it is possible to have nested text blocks.
// If you change the font size of the parent this will then create an inaccurate
// font size for any children.
for (var i = 0; i < $textblocks.length; i++) {
var ele = $textblocks[i];
var fontSizeAttr = ele.getAttribute('data-original-font-size');
if (fontSizeAttr) {
// early exit, original values already set.
break;
}
var style = win.getComputedStyle(ele);
var originalFontSize = parseInt(style.fontSize);
ele.setAttribute('data-original-font-size', originalFontSize);
var originalLineHeight = parseInt(style.lineHeight);
// getComputedStyle will not calculate the line-height if the value is 'normal'. In this case parseInt will return NaN
if (originalLineHeight) {
ele.setAttribute('data-original-line-height', originalLineHeight);
}
// var fontFamilyAttr = ele.getAttribute('data-original-font-family');
// if (!fontFamilyAttr) {
// var originalFontFamily = style.fontFamily;
// if (originalFontFamily) {
// ele.setAttribute('data-original-font-family', originalFontFamily);
// }
// }
}
for (var i = 0; i < $textblocks.length; i++) {
var ele = $textblocks[i];
// TODO: group the 3x potential $(ele).css() calls below to avoid multiple jQuery style mutations
var fontSizeAttr = ele.getAttribute('data-original-font-size');
var originalFontSize = Number(fontSizeAttr);
$(ele).css("font-size", (originalFontSize * factor) + 'px');
var lineHeightAttr = ele.getAttribute('data-original-line-height');
var originalLineHeight = lineHeightAttr ? Number(lineHeightAttr) : 0;
if (originalLineHeight) {
$(ele).css("line-height", (originalLineHeight * factor) + 'px');
}
// var fontFamilyAttr = ele.getAttribute('data-original-font-family');
// switch(changeFontFamily){
// case NOTHING:
// break;
// case ADD:
// $(ele).css("font-family", fontObj.fontFamily);
// break;
// case REMOVE:
// $(ele).css("font-family", fontFamilyAttr);
// break;
// }
}
$epubHtml.css("font-size", fontSize + "%");
if (perf) {
var time2 = window.performance.now();
// Firefox: 80+
// Chrome: 4-10
// Edge: 15-34
// IE: 10-15
// https://readium.firebase.com/?epub=..%2Fepub_content%2Faccessible_epub_3&goto=%7B%22idref%22%3A%22id-id2635343%22%2C%22elementCfi%22%3A%22%2F4%2F2%5Bbuilding_a_better_epub%5D%2F10%2F44%2F6%2C%2F1%3A334%2C%2F1%3A335%22%7D
var diff = time2-time1;
console.log(diff);
// setTimeout(function(){
// alert(diff);
// }, 2000);
}
callback();
};
var fontLoadCallback_ = _.once(fontLoadCallback);
if(fontObj.fontFamily && fontObj.url){
var dataFontFamily = link.length ? link.attr("data-fontfamily") : undefined;
if(!link.length){
changeFontFamily = ADD;
setTimeout(function(){
link = $("<link/>", {
"id" : FONT_FAMILY_ID,
"data-fontfamily" : fontObj.fontFamily,
"rel" : "stylesheet",
"type" : "text/css"
});
docHead.append(link);
link.attr({
"href" : fontObj.url
});
}, 0);
}
else if(dataFontFamily != fontObj.fontFamily){
changeFontFamily = ADD;
link.attr({
"data-fontfamily" : fontObj.fontFamily,
"href" : fontObj.url
});
} else {
changeFontFamily = NOTHING;
}
}
else{
changeFontFamily = REMOVE;
if(link.length) link.remove();
}
if (changeFontFamily == ADD) {
// just in case the link@onload does not trigger, we set a timeout
setTimeout(function(){
fontLoadCallback_();
}, 100);
}
else { // REMOVE, NOTHING
fontLoadCallback_();
}
};

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions