Skip to content

Support custom fonts from Android res/font resources - #787

Open
Zellius wants to merge 4 commits into
readium:developfrom
Zellius:support_android_resources
Open

Zellius wants to merge 4 commits into
readium:developfrom
Zellius:support_android_resources

Conversation

@Zellius

@Zellius Zellius commented May 12, 2026 •

Copy link
Copy Markdown

Related issue: #786

Android allows resources to be provided using either the res or assets folders. Classic Android apps provide most resources, such as images, strings, and fonts, using res making them easy to use in XML layouts, views, and composables.

Currently, Readium allows custom fonts to be provided only through the assets folder. This means apps need to store the same font in both res and assets to use it in XML and in Readium.

This PR removes that limitation and allows the font family to be specified using either a single font file or an Android XML font family.

Summary

  • Add support for using Android res/font resources as Readium font families in the EPUB navigator.
  • Support both single font files (.ttf/.otf) and XML font family resources (res/font/*.xml) when building @font-face declarations.

Changes

Core navigator support

  • Added Android resource URL mapping helper to generate WebView compatible resource paths:
    • readium/navigator/src/main/java/org/readium/r2/navigator/epub/resources/ResourcePaths.kt
  • Added font resource parsing utilities:
    • Parse XML definitions and direct font files
    • Extract weight/style metadata per font variant
    • readium/navigator/src/main/java/org/readium/r2/navigator/epub/resources/ResourceFontFamily.kt
  • Added configuration extension to register a FontFamily from Android font resources:
    • Configuration.addFontFamilyDeclaration(...)
    • readium/navigator/src/main/java/org/readium/r2/navigator/epub/resources/FontFamilyDeclarations.kt

WebView serving updates

  • Updated EPUB WebView server asset loading to include Android resources under a dedicated path prefix.

@qnga

qnga commented Jun 3, 2026

Copy link
Copy Markdown
Member

Hi !
Thanks for you PR. Could you rebase your work on develop? I fixed the CORS issue separately as it was urgent. I understand the need for supporting Android resources, I've had the same duplication issue with Compose resources in some app. I'm more on the fence regarding the Android XML fonts as we have our own system to build font families. Why would we need this?

@Zellius
Zellius force-pushed the support_android_resources branch from 81df683 to 8f5469f Compare June 5, 2026 13:55
@Zellius

Zellius commented Jun 5, 2026

Copy link
Copy Markdown
Author

@qnga Hey! :)

Could you rebase your work on develop?

Sure! Thanks for fixing the issue.

Why would we need this?

I agree that Readium already has a good system to build font families through addFontFamilyDeclaration.

In our app Android XML font family resources are the source of truth. We use them across XML layouts and Compose. The app is white-label, with 100+ flavors, and each flavor can override resources such as strings, colors, font families, etc.

This follows Android’s built in approach for defining font families in resources, as described here. The XML file groups the font files together with their style and weight details, and exposes them as a single font resource.

Without XML font family support in Readium we need to duplicate the same font family definition in EpubNavigatorFragment.Configuration. Every font file, weight and style must be declared again manually with addFontFamilyDeclaration. This creates two sources of truth:

  1. Android res/font/*.xml, used by the rest of the app.
  2. Readium font declarations, maintained separately for the EPUB navigator.

Our app could parse these XML files itself and convert them to Readium declarations but then every app with the same need would have to implement its own parser/bridge. Since this is a standard Android resource format and maps directly to Readium’s existing @font-face declarations it seems useful to support it in Readium itself.

This PR does not replace Readium’s font family system. It only adds a bridge from Android res/font resources to Readium’s existing @font-face declaration system, so apps can reuse an existing R.font.some_family without duplicating its XML contents in Kotlin.

@qnga

qnga commented Oct 15, 2026

Copy link
Copy Markdown
Member

Sorry for the delay. I feel like the Android XML font family resources are more or less deprecated as I can't find a way to use them in Jetpack Compose. If I'm not mistaken, I'll feel more comfortable if we could keep all of this simple supporting only simple font resources.

@Zellius

Zellius commented Oct 27, 2026

Copy link
Copy Markdown
Author

Hey! Thanks for the reply.

I agree that XML font family resources will probably become less common in favor of Compose font families. At the same time, storing font files (.ttf, .otf, etc.) in res/font instead of assets is still common practice. Even in Compose we may use Font(R.font.my_font_regular, FontWeight.Normal).

Currently, Readium only supports loading custom app provided fonts from the assets folder. This means I have to duplicate the same font files in both res/font and assets if I want to use them in the app's UI outside Readium as well.

I agree that including an entire XML parser in Readium adds unnecessary complexity.

As an alternative, I could remove the XML parser from this PR and keep only the support for loading fonts from Android resources. The WebViewServer would expose a helper function resourceUrl(resId: Int) similar to the existing assetUrl(path: String). This URL could then be passed to the existing addSource() method.

Internally, WebViewAssetLoader would register an additional WebViewAssetLoader.ResourcesPathHandler, as already implemented in the PR.

Would this approach be acceptable?

For context, I'm currently using a somewhat hacky workaround in my project. I have a custom Container<Resource> that provides FontResource instances, which read the font bytes directly from Android resources. It works, but using WebViewAssetLoader.ResourcesPathHandler would be much simpler and cleaner.

@qnga

qnga commented Nov 3, 2026

Copy link
Copy Markdown
Member

As an alternative, I could remove the XML parser from this PR and keep only the support for loading fonts from Android resources.

Yep, that's what I meant.

@Zellius

Zellius commented Nov 4, 2026

Copy link
Copy Markdown
Author

I removed XML parsing from the PR 👍

// For the custom font Literata.
"fonts/.*",
// For Roboto Mono font files in Android resources
EpubNavigatorFragment.androidResourcePath("font/roboto_mono_.*"),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Too tricky. Let's be both clear and safe with two different lists: servedAssets et servedAndroidResources.

Comment on lines +76 to +92
val resources = requireContext().resources

val robotoMonoRegular = requireNotNull(
EpubNavigatorFragment.androidResourceUrl(resources, R.font.roboto_mono_regular)
)

val robotoMonoItalic = requireNotNull(
EpubNavigatorFragment.androidResourceUrl(resources, R.font.roboto_mono_italic)
)

val robotoMonoBold = requireNotNull(
EpubNavigatorFragment.androidResourceUrl(resources, R.font.roboto_mono_bold)
)

val robotoMonoBoldItalic = requireNotNull(
EpubNavigatorFragment.androidResourceUrl(resources, R.font.roboto_mono_bold_italic)
)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These declarations would be clearer below, with their uses.

* `font/my_font.ttf`. Simple glob patterns such as `font/my_font_.*`
* can be used with [Configuration.servedAssets].
*/
public fun androidResourcePath(path: String): String =

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's remove this (see below).

*
* @return `null` if [resId] does not identify a file-backed resource.
*/
public fun androidResourcePath(resources: Resources, resId: Int): String? =

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if this needs to be public. Returning a URL is best, isn't it?

WebViewAssetLoader.Builder()
.setDomain(ASSETS_HOSTNAME)
.addPathHandler("/$ANDROID_RES_PATH_PREFIX/", WebViewAssetLoader.ResourcesPathHandler(application))
.addPathHandler("/", WebViewAssetLoader.AssetsPathHandler(application))

@qnga qnga Nov 5, 2026 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's be super explicit and add a path prefix for regular assets too (things could get messy if there is an android_res directory in regular assets). Please check the hard-coded asset URLs in the code base.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or maybe a different hostname for resources if that's easier? I don't have a strong opinion on this.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants