mirror of
https://github.com/gotson/komga.git
synced 2025-04-04 22:33:31 +08:00
feat(webui): support additional fonts
added embedded font OpenDyslexic additional fonts can be added in the configuration directory under ./fonts/{fontFamily}/ supported files are woff/woff2/ttf/otf Closes: #1836
This commit is contained in:
parent
42047cdafb
commit
201c066fc4
@ -756,8 +756,10 @@
|
||||
"epubreader": {
|
||||
"current_chapter": "Current chapter",
|
||||
"page_of": "Page {page} of {count}",
|
||||
"publisher_font": "Publisher",
|
||||
"settings": {
|
||||
"column_count": "Column count",
|
||||
"font_family": "Font",
|
||||
"layout": "Layout",
|
||||
"layout_paginated": "Paginated",
|
||||
"layout_scroll": "Scroll",
|
||||
|
@ -32,6 +32,7 @@ import komgaHistory from './plugins/komga-history.plugin'
|
||||
import komgaAnnouncements from './plugins/komga-announcements.plugin'
|
||||
import komgaReleases from './plugins/komga-releases.plugin'
|
||||
import komgaSettings from './plugins/komga-settings.plugin'
|
||||
import komgaFonts from './plugins/komga-fonts.plugin'
|
||||
import vuetify from './plugins/vuetify'
|
||||
import logger from './plugins/logger.plugin'
|
||||
import './public-path'
|
||||
@ -80,6 +81,7 @@ Vue.use(komgaHistory, {http: Vue.prototype.$http})
|
||||
Vue.use(komgaAnnouncements, {http: Vue.prototype.$http})
|
||||
Vue.use(komgaReleases, {http: Vue.prototype.$http})
|
||||
Vue.use(komgaSettings, {http: Vue.prototype.$http})
|
||||
Vue.use(komgaFonts, {http: Vue.prototype.$http})
|
||||
|
||||
Vue.config.productionTip = false
|
||||
|
||||
|
17
komga-webui/src/plugins/komga-fonts.plugin.ts
Normal file
17
komga-webui/src/plugins/komga-fonts.plugin.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import {AxiosInstance} from 'axios'
|
||||
import _Vue from 'vue'
|
||||
import KomgaFontsService from '@/services/komga-fonts.service'
|
||||
|
||||
export default {
|
||||
install(
|
||||
Vue: typeof _Vue,
|
||||
{http}: { http: AxiosInstance }) {
|
||||
Vue.prototype.$komgaFonts = new KomgaFontsService(http)
|
||||
},
|
||||
}
|
||||
|
||||
declare module 'vue/types/vue' {
|
||||
interface Vue {
|
||||
$komgaFonts: KomgaFontsService;
|
||||
}
|
||||
}
|
23
komga-webui/src/services/komga-fonts.service.ts
Normal file
23
komga-webui/src/services/komga-fonts.service.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import {AxiosInstance} from 'axios'
|
||||
|
||||
const API_FONTS = '/api/v1/fonts'
|
||||
|
||||
export default class KomgaFontsService {
|
||||
private http: AxiosInstance
|
||||
|
||||
constructor(http: AxiosInstance) {
|
||||
this.http = http
|
||||
}
|
||||
|
||||
async getFamilies(): Promise<string[]> {
|
||||
try {
|
||||
return (await this.http.get(`${API_FONTS}/families`)).data
|
||||
} catch (e) {
|
||||
let msg = 'An error occurred while trying to retrieve font families'
|
||||
if (e.response.data.message) {
|
||||
msg += `: ${e.response.data.message}`
|
||||
}
|
||||
throw new Error(msg)
|
||||
}
|
||||
}
|
||||
}
|
@ -213,6 +213,14 @@
|
||||
|
||||
<v-subheader class="font-weight-black text-h6">{{ $t('bookreader.settings.display') }}</v-subheader>
|
||||
|
||||
<v-list-item v-if="fontFamilies.length > 1">
|
||||
<settings-select
|
||||
:items="fontFamilies"
|
||||
v-model="fontFamily"
|
||||
:label="$t('epubreader.settings.font_family')"
|
||||
/>
|
||||
</v-list-item>
|
||||
|
||||
<v-list-item>
|
||||
<v-list-item-title>{{ $t('epubreader.settings.viewing_theme') }}</v-list-item-title>
|
||||
<v-btn
|
||||
@ -299,7 +307,7 @@
|
||||
<script lang="ts">
|
||||
import Vue from 'vue'
|
||||
import D2Reader, {Locator} from '@d-i-t-a/reader'
|
||||
import {bookManifestUrl, bookPositionsUrl} from '@/functions/urls'
|
||||
import urls, {bookManifestUrl, bookPositionsUrl} from '@/functions/urls'
|
||||
import {BookDto} from '@/types/komga-books'
|
||||
import {getBookTitleCompact} from '@/functions/book-title'
|
||||
import {SeriesDto} from '@/types/komga-series'
|
||||
@ -382,6 +390,12 @@ export default Vue.extend({
|
||||
{text: this.$t('enums.epubreader.column_count.one').toString(), value: '1'},
|
||||
{text: this.$t('enums.epubreader.column_count.two').toString(), value: '2'},
|
||||
],
|
||||
fontFamilyDefault: [{
|
||||
text: this.$t('epubreader.publisher_font'),
|
||||
value: 'Original',
|
||||
}],
|
||||
fontFamiliesAdditional: [] as string[],
|
||||
fontFamilies: [] as any[],
|
||||
settings: {
|
||||
// R2D2BC
|
||||
appearance: 'readium-default-on',
|
||||
@ -393,6 +407,7 @@ export default Vue.extend({
|
||||
fixedLayoutMargin: 0,
|
||||
fixedLayoutShadow: false,
|
||||
direction: 'auto',
|
||||
fontFamily: 'Original',
|
||||
// Epub Reader
|
||||
alwaysFullscreen: false,
|
||||
navigationClick: true,
|
||||
@ -439,10 +454,13 @@ export default Vue.extend({
|
||||
screenfull.exit()
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
async mounted() {
|
||||
Object.assign(this.settings, this.$store.state.persistedState.epubreader)
|
||||
this.settings.alwaysFullscreen = this.$store.state.persistedState.webreader.alwaysFullscreen
|
||||
|
||||
this.fontFamiliesAdditional = await this.$komgaFonts.getFamilies()
|
||||
this.fontFamilies = [...this.fontFamilyDefault, ...this.fontFamiliesAdditional]
|
||||
|
||||
this.setup(this.bookId)
|
||||
},
|
||||
props: {
|
||||
@ -611,6 +629,16 @@ export default Vue.extend({
|
||||
this.$store.commit('setEpubreaderSettings', this.settings)
|
||||
},
|
||||
},
|
||||
fontFamily: {
|
||||
get: function (): string {
|
||||
return this.settings.fontFamily ?? 'Original'
|
||||
},
|
||||
set: function (value: string): void {
|
||||
this.settings.fontFamily = value
|
||||
this.d2Reader.applyUserSettings({fontFamily: value})
|
||||
this.$store.commit('setEpubreaderSettings', this.settings)
|
||||
},
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
previousBook() {
|
||||
@ -721,6 +749,12 @@ export default Vue.extend({
|
||||
// parse query params to get incognito mode
|
||||
this.incognito = !!(this.$route.query.incognito && this.$route.query.incognito.toString().toLowerCase() === 'true')
|
||||
|
||||
const fontFamiliesInjectables = this.fontFamiliesAdditional.map(x => ({
|
||||
type: 'style',
|
||||
url: new URL(`${urls.origin}api/v1/fonts/resource/${x}/css`, import.meta.url).toString(),
|
||||
fontFamily: x,
|
||||
}))
|
||||
|
||||
this.d2Reader = await D2Reader.load({
|
||||
url: new URL(bookManifestUrl(this.bookId)),
|
||||
userSettings: this.settings,
|
||||
@ -747,6 +781,7 @@ export default Vue.extend({
|
||||
{type: 'style', url: new URL('../styles/r2d2bc/popup.css.resource', import.meta.url).toString()},
|
||||
{type: 'style', url: new URL('../styles/r2d2bc/popover.css.resource', import.meta.url).toString()},
|
||||
{type: 'style', url: new URL('../styles/r2d2bc/style.css.resource', import.meta.url).toString()},
|
||||
...fontFamiliesInjectables,
|
||||
],
|
||||
requestConfig: {
|
||||
credentials: 'include',
|
||||
|
@ -47,6 +47,8 @@ class KomgaProperties {
|
||||
|
||||
var kobo = Kobo()
|
||||
|
||||
val fonts = Fonts()
|
||||
|
||||
class Cors {
|
||||
var allowedOrigins: List<String> = emptyList()
|
||||
}
|
||||
@ -72,6 +74,11 @@ class KomgaProperties {
|
||||
var pragmas: Map<String, String> = emptyMap()
|
||||
}
|
||||
|
||||
class Fonts {
|
||||
@get:NotBlank
|
||||
var dataDirectory: String = ""
|
||||
}
|
||||
|
||||
class Lucene {
|
||||
@get:NotBlank
|
||||
var dataDirectory: String = ""
|
||||
|
@ -90,6 +90,8 @@ class SecurityConfiguration(
|
||||
"/api/v1/oauth2/providers",
|
||||
// epub resources - fonts are always requested anonymously, so we check for authorization within the controller method directly
|
||||
"/api/v1/books/{bookId}/resource/**",
|
||||
// dynamic fonts
|
||||
"/api/v1/fonts/resource/**",
|
||||
// OPDS authentication document
|
||||
"/opds/v2/auth",
|
||||
// KOReader user creation
|
||||
|
@ -0,0 +1,165 @@
|
||||
package org.gotson.komga.interfaces.api.rest
|
||||
|
||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||
import org.gotson.komga.infrastructure.configuration.KomgaProperties
|
||||
import org.gotson.komga.language.contains
|
||||
import org.springframework.core.io.ByteArrayResource
|
||||
import org.springframework.core.io.FileSystemResource
|
||||
import org.springframework.core.io.Resource
|
||||
import org.springframework.core.io.support.PathMatchingResourcePatternResolver
|
||||
import org.springframework.http.ContentDisposition
|
||||
import org.springframework.http.HttpStatus
|
||||
import org.springframework.http.MediaType
|
||||
import org.springframework.http.ResponseEntity
|
||||
import org.springframework.web.bind.annotation.GetMapping
|
||||
import org.springframework.web.bind.annotation.PathVariable
|
||||
import org.springframework.web.bind.annotation.RequestMapping
|
||||
import org.springframework.web.bind.annotation.RestController
|
||||
import org.springframework.web.server.ResponseStatusException
|
||||
import kotlin.io.path.Path
|
||||
import kotlin.io.path.extension
|
||||
import kotlin.io.path.isDirectory
|
||||
import kotlin.io.path.isReadable
|
||||
import kotlin.io.path.isRegularFile
|
||||
import kotlin.io.path.listDirectoryEntries
|
||||
import kotlin.io.path.name
|
||||
import kotlin.io.path.toPath
|
||||
|
||||
private val logger = KotlinLogging.logger {}
|
||||
|
||||
@RestController
|
||||
@RequestMapping(value = ["api/v1/fonts"], produces = [MediaType.APPLICATION_JSON_VALUE])
|
||||
class FontsController(
|
||||
komgaProperties: KomgaProperties,
|
||||
) {
|
||||
private val supportedExtensions = listOf("woff", "woff2", "ttf", "otf")
|
||||
private final val fonts: Map<String, List<Resource>>
|
||||
|
||||
init {
|
||||
val resolver = PathMatchingResourcePatternResolver()
|
||||
val fontsEmbedded =
|
||||
try {
|
||||
resolver
|
||||
.getResources("/embeddedFonts/**/*.*")
|
||||
.filterNot { it.filename == null }
|
||||
.filter { supportedExtensions.contains(it.uri.toPath().extension, true) }
|
||||
.groupBy {
|
||||
it.uri
|
||||
.toPath()
|
||||
.parent.name
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
logger.error(e) { "Could not load embedded fonts" }
|
||||
emptyMap()
|
||||
}
|
||||
|
||||
val fontsDir = Path(komgaProperties.fonts.dataDirectory)
|
||||
val fontsAdditional =
|
||||
try {
|
||||
if (fontsDir.isDirectory() && fontsDir.isReadable()) {
|
||||
fontsDir
|
||||
.listDirectoryEntries()
|
||||
.filter { it.isDirectory() }
|
||||
.associate { dir ->
|
||||
dir.name to
|
||||
dir
|
||||
.listDirectoryEntries()
|
||||
.filter { it.isRegularFile() }
|
||||
.filter { it.isReadable() }
|
||||
.filter { supportedExtensions.contains(it.extension, true) }
|
||||
.map { FileSystemResource(it) }
|
||||
}
|
||||
} else {
|
||||
emptyMap()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
logger.error(e) { "Could not load additional fonts" }
|
||||
emptyMap()
|
||||
}
|
||||
|
||||
fonts = fontsEmbedded + fontsAdditional
|
||||
|
||||
logger.info { "Fonts embedded: $fontsEmbedded" }
|
||||
logger.info { "Fonts discovered: $fontsAdditional" }
|
||||
}
|
||||
|
||||
@GetMapping("families")
|
||||
fun listFonts(): Set<String> = fonts.keys
|
||||
|
||||
@GetMapping("resource/{fontFamily}/{fontFile}")
|
||||
fun getFontFile(
|
||||
@PathVariable fontFamily: String,
|
||||
@PathVariable fontFile: String,
|
||||
): ResponseEntity<Resource> {
|
||||
fonts[fontFamily]?.let { resources ->
|
||||
val resource = resources.firstOrNull { it.uri.toPath().name == fontFile } ?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
|
||||
return ResponseEntity
|
||||
.ok()
|
||||
.headers {
|
||||
it.contentDisposition =
|
||||
ContentDisposition
|
||||
.attachment()
|
||||
.filename(fontFile)
|
||||
.build()
|
||||
}.contentType(MediaType.APPLICATION_OCTET_STREAM)
|
||||
.body(resource)
|
||||
} ?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
|
||||
}
|
||||
|
||||
@GetMapping("resource/{fontFamily}/css", produces = ["text/css"])
|
||||
fun getFontFamilyAsCss(
|
||||
@PathVariable fontFamily: String,
|
||||
): ResponseEntity<Resource> {
|
||||
fonts[fontFamily]?.let { files ->
|
||||
val groups = files.groupBy { getFontCharacteristics(it.uri.toPath().name) }
|
||||
|
||||
val css =
|
||||
groups
|
||||
.map { (styleWeight, resources) -> buildFontFaceBlock(fontFamily, styleWeight, resources) }
|
||||
.joinToString(separator = "\n")
|
||||
|
||||
return ResponseEntity
|
||||
.ok()
|
||||
.headers {
|
||||
it.contentDisposition =
|
||||
ContentDisposition
|
||||
.attachment()
|
||||
.filename("$fontFamily.css")
|
||||
.build()
|
||||
}.body(ByteArrayResource(css.toByteArray()))
|
||||
} ?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
|
||||
}
|
||||
|
||||
private fun buildFontFaceBlock(
|
||||
fontFamily: String,
|
||||
styleAndWeight: FontCharacteristics,
|
||||
fonts: List<Resource>,
|
||||
): String {
|
||||
val srcBlock =
|
||||
fonts.joinToString(separator = ",", postfix = ";") { resource ->
|
||||
val path = resource.uri.toPath()
|
||||
"""url('${path.name}') format('${path.extension}')"""
|
||||
}
|
||||
// language=CSS
|
||||
return """
|
||||
@font-face {
|
||||
font-family: '$fontFamily';
|
||||
src: $srcBlock
|
||||
font-weight: ${styleAndWeight.weight};
|
||||
font-style: ${styleAndWeight.style};
|
||||
}
|
||||
|
||||
""".trimIndent()
|
||||
}
|
||||
|
||||
private fun getFontCharacteristics(filename: String): FontCharacteristics {
|
||||
val style = if (filename.contains("italic", true)) "italic" else "normal"
|
||||
val weight = if (filename.contains("bold", true)) "bold" else "normal"
|
||||
return FontCharacteristics(style, weight)
|
||||
}
|
||||
|
||||
private data class FontCharacteristics(
|
||||
val style: String,
|
||||
val weight: String,
|
||||
)
|
||||
}
|
@ -19,6 +19,8 @@ komga:
|
||||
file: \${komga.config-dir}/database.sqlite
|
||||
lucene:
|
||||
data-directory: \${komga.config-dir}/lucene
|
||||
fonts:
|
||||
data-directory: \${komga.config-dir}/fonts
|
||||
config-dir: \${user.home}/.komga
|
||||
tasks-db:
|
||||
file: \${komga.config-dir}/tasks.sqlite
|
||||
|
Binary file not shown.
Binary file not shown.
BIN
komga/src/main/resources/embeddedFonts/OpenDyslexic/OpenDyslexic-Bold.woff
Executable file
BIN
komga/src/main/resources/embeddedFonts/OpenDyslexic/OpenDyslexic-Bold.woff
Executable file
Binary file not shown.
BIN
komga/src/main/resources/embeddedFonts/OpenDyslexic/OpenDyslexic-Bold.woff2
Executable file
BIN
komga/src/main/resources/embeddedFonts/OpenDyslexic/OpenDyslexic-Bold.woff2
Executable file
Binary file not shown.
BIN
komga/src/main/resources/embeddedFonts/OpenDyslexic/OpenDyslexic-Italic.woff
Executable file
BIN
komga/src/main/resources/embeddedFonts/OpenDyslexic/OpenDyslexic-Italic.woff
Executable file
Binary file not shown.
BIN
komga/src/main/resources/embeddedFonts/OpenDyslexic/OpenDyslexic-Italic.woff2
Executable file
BIN
komga/src/main/resources/embeddedFonts/OpenDyslexic/OpenDyslexic-Italic.woff2
Executable file
Binary file not shown.
BIN
komga/src/main/resources/embeddedFonts/OpenDyslexic/OpenDyslexic-Regular.woff
Executable file
BIN
komga/src/main/resources/embeddedFonts/OpenDyslexic/OpenDyslexic-Regular.woff
Executable file
Binary file not shown.
BIN
komga/src/main/resources/embeddedFonts/OpenDyslexic/OpenDyslexic-Regular.woff2
Executable file
BIN
komga/src/main/resources/embeddedFonts/OpenDyslexic/OpenDyslexic-Regular.woff2
Executable file
Binary file not shown.
Loading…
x
Reference in New Issue
Block a user