import { MONOSPACE_FONTS, SANS_SERIF_FONTS, SERIF_FONTS, FALLBACK_FONTS, CJK_SANS_SERIF_FONTS, CJK_SERIF_FONTS, } from '@/services/constants'; import { ViewSettings } from '@/types/book'; import { themes, Palette, CustomTheme, generateLightPalette, generateDarkPalette, } from '@/styles/themes'; import { createFontCSS, CustomFont } from '@/styles/fonts'; import { getOSPlatform } from './misc'; import { SCROLL_WRAPPER_CLASS, SCROLL_WRAPPER_FIT_CLASS } from './scrollable'; /** * Build the resolved CSS font-family lists (serif / sans-serif / monospace) * from the user's font settings. Each value is a ready-to-use `font-family` * string ending in the matching generic family. Shared by getFontStyles (which * exposes them as CSS variables inside the reader iframe) and getBaseFontFamily * (which applies the body font directly to top-level UI such as the RSVP overlay). */ const buildFontFamilyLists = ( serif: string, sansSerif: string, monospace: string, defaultCJKFont: string, ) => { const lastSerifFonts = ['Georgia', 'Times New Roman']; const serifFonts = [ serif, ...(defaultCJKFont !== serif ? [defaultCJKFont] : []), ...SERIF_FONTS.filter( (font) => font !== serif && font !== defaultCJKFont && !lastSerifFonts.includes(font), ), ...CJK_SERIF_FONTS.filter((font) => font !== serif && font !== defaultCJKFont), ...lastSerifFonts.filter( (font) => SERIF_FONTS.includes(font) && !lastSerifFonts.includes(defaultCJKFont), ), ...FALLBACK_FONTS, ]; const sansSerifFonts = [ sansSerif, ...(defaultCJKFont !== sansSerif ? [defaultCJKFont] : []), ...SANS_SERIF_FONTS.filter((font) => font !== sansSerif && font !== defaultCJKFont), ...CJK_SANS_SERIF_FONTS.filter((font) => font !== sansSerif && font !== defaultCJKFont), ...FALLBACK_FONTS, ]; const monospaceFonts = [monospace, ...MONOSPACE_FONTS.filter((font) => font !== monospace)]; const quote = (fonts: string[]) => fonts.map((font) => `"${font}"`).join(', '); return { serif: `${quote(serifFonts)}, serif`, sansSerif: `${quote(sansSerifFonts)}, sans-serif`, monospace: `${quote(monospaceFonts)}, monospace`, }; }; /** * Resolve the body font-family string (serif or sans-serif chain, per the * user's "Default Font" setting) for use outside the reader iframe — e.g. the * RSVP overlay, which renders in the top document and can't read the iframe's * --serif/--sans-serif CSS variables. Custom fonts are already mounted in the * top document, so the returned chain resolves them by family name. */ export const getBaseFontFamily = (viewSettings: ViewSettings): string => { const families = buildFontFamilyLists( viewSettings.serifFont!, viewSettings.sansSerifFont!, viewSettings.monospaceFont!, viewSettings.defaultCJKFont!, ); return viewSettings.defaultFont!.toLowerCase() === 'serif' ? families.serif : families.sansSerif; }; const getFontStyles = ( serif: string, sansSerif: string, monospace: string, defaultFont: string, defaultCJKFont: string, fontSize: number, minFontSize: number, fontWeight: number, overrideFont: boolean, ) => { const families = buildFontFamilyLists(serif, sansSerif, monospace, defaultCJKFont); const defaultFontFamily = defaultFont.toLowerCase() === 'serif' ? '--serif' : '--sans-serif'; const fontStyles = ` html { --serif: ${families.serif}; --sans-serif: ${families.sansSerif}; --monospace: ${families.monospace}; --font-size: ${fontSize}px; --min-font-size: ${minFontSize}px; --font-weight: ${fontWeight}; } html, body { font-size: ${fontSize}px !important; font-weight: ${fontWeight}; -webkit-text-size-adjust: none; text-size-adjust: none; } /* lower specificity than ebook built-in font styles */ html { font-family: var(${defaultFontFamily}) ${overrideFont ? '!important' : ''}; } /* higher specificity than ebook built-in font styles */ html body { ${overrideFont ? `font-family: var(${defaultFontFamily}) !important;` : ''} } font[size="1"] { font-size: ${minFontSize}px; } font[size="2"] { font-size: ${minFontSize * 1.5}px; } font[size="3"] { font-size: ${fontSize}px; } font[size="4"] { font-size: ${fontSize * 1.2}px; } font[size="5"] { font-size: ${fontSize * 1.5}px; } font[size="6"] { font-size: ${fontSize * 2}px; } font[size="7"] { font-size: ${fontSize * 3}px; } /* hardcoded inline font size */ [style*="font-size: 16px"], [style*="font-size:16px"] { font-size: 1rem !important; } pre, code, kbd { font-family: var(--monospace); } body *:not(pre, code, kbd, .code):not(pre *, code *, kbd *, .code *) { ${overrideFont ? 'font-family: revert !important;' : ''} } `; return fontStyles; }; /** True for #fff, #f5f5f5, rgb(255,…), etc. Used when rewriting EPUB CSS in dark mode. */ const isLightCssColor = (value: string): boolean => { const v = value.trim().toLowerCase(); if (v === 'white') return true; const hex = v.match(/^#([0-9a-f]{3}|[0-9a-f]{6})$/i); if (hex) { const h = hex[1]!; const expand = h.length === 3 ? h .split('') .map((c) => c + c) .join('') : h; const r = parseInt(expand.slice(0, 2), 16); const g = parseInt(expand.slice(2, 4), 16); const b = parseInt(expand.slice(4, 6), 16); const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255; return luminance > 0.85; } const rgb = v.match(/^rgb\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)$/); if (rgb?.[1] != null && rgb[2] != null && rgb[3] != null) { const r = parseInt(rgb[1], 10); const g = parseInt(rgb[2], 10); const b = parseInt(rgb[3], 10); const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255; return luminance > 0.85; } return false; }; const getDarkModeLightBackgroundOverrides = (bg: string) => ` /* Callout boxes often use inline white/light backgrounds while html/body set dark fg. */ *[style*="background-color: #fff"], *[style*="background-color:#fff"], *[style*="background-color: #ffffff"], *[style*="background-color:#ffffff"], *[style*="background-color: white"], *[style*="background-color:white"], *[style*="background: #fff"], *[style*="background:#fff"], *[style*="background: #ffffff"], *[style*="background:#ffffff"], *[style*="background: white"], *[style*="background:white"], *[style*="background-color: rgb(255"], *[style*="background-color:rgb(255"], *[style*="background: rgb(255"], *[style*="background:rgb(255"] { background-color: ${bg} !important; } /* Force transparent, not the theme bg: the dark page fill already comes from the paginator container / reader grid cell, while an opaque body paints over the host background texture (#4446) — and foliate captures docBackground once per section load, so the body must stay transparent regardless of texture state. Book-forced light page backgrounds still get neutralized (#4392) since the theme-dark fill shows through. */ body.theme-dark { background-color: transparent !important; } `; const getEinkSelectionStyles = () => { return ` ::selection { color: var(--theme-bg-color); background: var(--theme-fg-color); } ::-moz-selection { color: var(--theme-bg-color); background: var(--theme-fg-color); } `; }; const getColorStyles = ( overrideColor: boolean, invertImgColorInDark: boolean, themeCode: ThemeCode, backgroundTextureId: string, isEink: boolean, ) => { const { bg, fg, primary, isDarkMode } = themeCode; const hasBackgroundTexture = !!backgroundTextureId && backgroundTextureId !== 'none'; const colorStyles = ` html { --bg-texture-id: ${backgroundTextureId}; --theme-bg-color: ${bg}; --theme-fg-color: ${fg}; --theme-primary-color: ${primary}; --override-color: ${overrideColor}; color-scheme: ${isDarkMode ? 'dark' : 'light'}; } html, body { color: ${fg}; } ${isEink ? getEinkSelectionStyles() : ''} html[has-background], body[has-background] { --background-set: var(--theme-bg-color); } html { background-color: var(--theme-bg-color, transparent); background: var(--background-set, none); } body { ${isEink ? `background-color: ${bg} !important;` : ''} } section, aside, blockquote, article, nav, header, footer, main, figure, div, p, font, h1, h2, h3, h4, h5, h6, li, span { ${overrideColor && !hasBackgroundTexture ? `background-color: ${bg} !important;` : ''} ${overrideColor && !hasBackgroundTexture ? `color: ${fg} !important;` : ''} ${overrideColor && !hasBackgroundTexture ? `border-color: ${fg} !important;` : ''} } pre, span { /* inline code blocks */ ${overrideColor ? `background-color: ${bg} !important;` : ''} } a:any-link { ${overrideColor ? `color: ${primary} !important;` : isDarkMode ? `color: lightblue;` : ''} text-decoration: ${isEink ? 'underline' : 'none'}; } body.pbg { ${isDarkMode ? `background-color: ${bg} !important;` : ''} } img { ${isDarkMode && invertImgColorInDark ? 'filter: invert(100%);' : ''} ${isDarkMode && overrideColor ? 'filter: grayscale(100%) contrast(1.2) brightness(1.2);' : ''} ${overrideColor ? 'mix-blend-mode: multiply;' : ''} } svg, img { ${overrideColor ? `background-color: transparent !important;` : ''}; } /* horizontal rule #1649 */ *:has(> hr.background-img):not(body) { background-color: ${bg}; } hr.background-img { mix-blend-mode: multiply; } p[width][height] > img:only-child { mix-blend-mode: multiply; } /* inline images */ *:has(> img.has-text-siblings):not(body) { ${overrideColor ? `background-color: ${bg};` : ''} } p img.has-text-siblings, span img.has-text-siblings, sup img.has-text-siblings { mix-blend-mode: ${isDarkMode ? 'screen' : 'multiply'}; } table:has(> colgroup) { table-layout: fixed; } td, th { word-break: break-word; overflow-wrap: anywhere; } /* code */ body.theme-dark code { ${isDarkMode ? `color: ${fg}cc;` : ''} ${isDarkMode ? `background: color-mix(in srgb, ${bg} 90%, #000);` : ''} ${isDarkMode ? `background-color: color-mix(in srgb, ${bg} 90%, #000);` : ''} } blockquote { ${isDarkMode ? `background: color-mix(in srgb, ${bg} 80%, #000);` : ''} } /* Only tint table descendants when the user has opted into color override. By default, leave them transparent so a plain table (and the invisible spacer cells some books use for vertical layout) keeps the page background instead of a different shade. Illegible light/zebra table backgrounds are handled separately by the dark-mode light-background rewriters (getDarkModeLightBackgroundOverrides / transformStylesheet). See #4419 (and #2377, which this gate originally fixed). */ blockquote, table * { ${isDarkMode && overrideColor ? `background: color-mix(in srgb, ${bg} 80%, #000);` : ''} ${isDarkMode && overrideColor ? `background-color: color-mix(in srgb, ${bg} 80%, #000);` : ''} } /* override inline hardcoded text color */ font[color="#000000"], font[color="#000"], font[color="black"], font[color="rgb(0,0,0)"], font[color="rgb(0, 0, 0)"], *[style*="color: rgb(0,0,0)"], *[style*="color: rgb(0, 0, 0)"], *[style*="color: #000"], *[style*="color: #000000"], *[style*="color: black"], *[style*="color:rgb(0,0,0)"], *[style*="color:rgb(0, 0, 0)"], *[style*="color:#000"], *[style*="color:#000000"], *[style*="color:black"] { color: ${fg} !important; } ${isDarkMode && !overrideColor ? getDarkModeLightBackgroundOverrides(bg) : ''} /* for the Gutenberg eBooks */ #pg-header * { color: inherit !important; } .x-ebookmaker, .x-ebookmaker-cover, .x-ebookmaker-coverpage { background-color: unset !important; } /* for the Feedbooks eBooks */ .chapterHeader, .chapterHeader * { border-color: unset; background-color: ${bg} !important; } .calibre { color: unset; background-color: unset; } `; return colorStyles; }; const getPageLayoutStyles = ( marginTop: number, marginRight: number, marginBottom: number, marginLeft: number, zoomLevel: number, writingMode: string, vertical: boolean, ) => ` html { --margin-top: ${marginTop}px; --margin-right: ${marginRight}px; --margin-bottom: ${marginBottom}px; --margin-left: ${marginLeft}px; } html, body { ${writingMode === 'auto' ? '' : `writing-mode: ${writingMode} !important;`} max-height: unset; -webkit-touch-callout: none; -webkit-user-select: text; } body { overflow: unset; zoom: ${zoomLevel}; padding: unset; margin: unset; } svg:where(:not([width])), img:where(:not([width])) { width: auto; } svg:where(:not([height])), img:where(:not([height])) { height: auto; } figure > div:has(img) { height: auto !important; } /* enlarge the clickable area of links */ a { position: relative !important; } a::before { content: ''; position: absolute; inset: -10px; } .${SCROLL_WRAPPER_CLASS} { display: block; overflow: auto; max-width: 100%; touch-action: pan-x pan-y; scrollbar-width: thin; -webkit-overflow-scrolling: touch; } .${SCROLL_WRAPPER_FIT_CLASS} { overflow: visible; } .${SCROLL_WRAPPER_CLASS} > table { display: table !important; max-width: 100%; } pre, code, math { white-space: pre-wrap !important; scrollbar-width: none; } math { overflow: auto; } table, math { max-width: calc(var(--available-width) * 1px); max-height: calc(var(--available-height) * 1px); } .epubtype-footnote, aside[epub|type~="endnote"], aside[epub|type~="footnote"], aside[epub|type~="note"], aside[epub|type~="rearnote"] { display: none; } /* Now begins really dirty hacks to fix some badly designed epubs */ body { line-height: unset; } .duokan-footnote-content, .duokan-footnote-item { display: none; } .duokan-image-gallery-cell { height: calc(var(--available-height) * 1px); } .duokan-image-gallery-cell img { height: 90%; } div:has(> img, > svg) { max-width: 100% !important; } body.paginated-mode td:has(img), body.paginated-mode td :has(img) { max-height: calc(var(--available-height) * 0.8 * 1px); } figure.code { overflow: unset !important; } /* some epubs set insane inline-block for p */ p { display: block; } /* inline images without dimension */ .ie6 img { width: unset; height: unset; } sup img { height: 1em; } img.has-text-siblings { ${vertical ? 'width: 1em;' : 'height: 1em;'} vertical-align: baseline; } :is(div) > img.has-text-siblings[style*="object-fit"] { display: block; height: auto; vertical-align: unset; } .duokan-footnote img:not([class]) { width: 0.8em; height: 0.8em; } div:has(img.singlepage) { position: relative; width: auto; height: auto; } /* some mobi */ p[width][height] > img:only-child { width: unset !important; height: unset !important; } /* page break */ body.paginated-mode div[style*="page-break-after: always"], body.paginated-mode div[style*="page-break-after:always"], body.paginated-mode p[style*="page-break-after: always"], body.paginated-mode p[style*="page-break-after:always"] { margin-bottom: calc(var(--available-height) * 1px); } .br { display: flow-root; } .h5_mainbody { overflow: unset !important; } `; // Paragraph-scoped CSS controlled by the Paragraph section of the Layout // panel. Gated by BookStyle.useBookLayout: when true, this chunk is skipped // and the book's own paragraph formatting takes over. const getParagraphLayoutStyles = ( overrideLayout: boolean, paragraphMargin: number, lineSpacing: number, wordSpacing: number, letterSpacing: number, textIndent: number, justify: boolean, hyphenate: boolean, vertical: boolean, ) => ` html { --default-text-align: ${justify ? 'justify' : 'start'}; hanging-punctuation: allow-end last; orphans: 2; widows: 2; } html, body { text-align: var(--default-text-align); } [align="left"] { text-align: left; } [align="right"] { text-align: right; } [align="center"] { text-align: center; } [align="justify"] { text-align: justify; } :is(hgroup, header) p { text-align: unset; hyphens: unset; } p, blockquote, dd, div:not(:has(*:not(b, a, em, i, strong, u, span))) { line-height: ${lineSpacing} ${overrideLayout ? '!important' : ''}; word-spacing: ${wordSpacing}px ${overrideLayout ? '!important' : ''}; letter-spacing: ${letterSpacing}px ${overrideLayout ? '!important' : ''}; text-indent: ${textIndent}em ${overrideLayout ? '!important' : ''}; -webkit-hyphens: ${hyphenate ? 'auto' : 'manual'} ${overrideLayout ? '!important' : ''}; hyphens: ${hyphenate ? 'auto' : 'manual'} ${overrideLayout ? '!important' : ''}; -webkit-hyphenate-limit-before: 3; -webkit-hyphenate-limit-after: 2; -webkit-hyphenate-limit-lines: 2; hanging-punctuation: allow-end last; widows: 2; } li { line-height: ${lineSpacing} ${overrideLayout ? '!important' : ''}; -webkit-hyphens: ${hyphenate ? 'auto' : 'manual'} ${overrideLayout ? '!important' : ''}; hyphens: ${hyphenate ? 'auto' : 'manual'} ${overrideLayout ? '!important' : ''}; } p.aligned-center, blockquote.aligned-center, dd.aligned-center, div.aligned-center { text-align: center ${overrideLayout ? '!important' : ''}; } p.aligned-left, blockquote.aligned-left, dd.aligned-left, div.aligned-left { ${justify && overrideLayout ? 'text-align: justify !important;' : ''} } p.aligned-right, blockquote.aligned-right, dd.aligned-right, div.aligned-right { text-align: right ${overrideLayout ? '!important' : ''}; } p.aligned-justify, blockquote.aligned-justify, dd.aligned-justify, div.aligned-justify { ${!justify && overrideLayout ? 'text-align: initial !important;' : ''}; } p:has(> img:only-child), p:has(> span:only-child > img:only-child), p:has(> img:not(.has-text-siblings)), p:has(> a:first-child + img:last-child) { text-indent: initial !important; } blockquote[align="center"], div[align="center"], p[align="center"], dd[align="center"], p.aligned-center, blockquote.aligned-center, dd.aligned-center, div.aligned-center, li p, ol p, ul p, td p { text-indent: initial !important; } p { ${vertical ? `margin-left: ${paragraphMargin}em ${overrideLayout ? '!important' : ''};` : ''} ${vertical ? `margin-right: ${paragraphMargin}em ${overrideLayout ? '!important' : ''};` : ''} ${vertical ? `margin-top: unset ${overrideLayout ? '!important' : ''};` : ''} ${vertical ? `margin-bottom: unset ${overrideLayout ? '!important' : ''};` : ''} ${!vertical ? `margin-top: ${paragraphMargin}em ${overrideLayout ? '!important' : ''};` : ''} ${!vertical ? `margin-bottom: ${paragraphMargin}em ${overrideLayout ? '!important' : ''};` : ''} ${!vertical ? `margin-left: unset ${overrideLayout ? '!important' : ''};` : ''} ${!vertical ? `margin-right: unset ${overrideLayout ? '!important' : ''};` : ''} } div { ${vertical && overrideLayout ? `margin-left: ${paragraphMargin}em !important;` : ''} ${vertical && overrideLayout ? `margin-right: ${paragraphMargin}em !important;` : ''} ${!vertical && overrideLayout ? `margin-top: ${paragraphMargin}em !important;` : ''} ${!vertical && overrideLayout ? `margin-bottom: ${paragraphMargin}em !important;` : ''} } p > font:only-child { display: flow-root; } :lang(zh), :lang(ja), :lang(ko) { widows: 1; orphans: 1; } /* workaround for some badly designed epubs */ div.left *, p.left * { text-align: left; } div.right *, p.right * { text-align: right; } div.center *, p.center * { text-align: center; } div.justify *, p.justify * { text-align: justify; } img.pi { ${vertical ? 'transform: rotate(90deg);' : ''} ${vertical ? 'transform-origin: center;' : ''} ${vertical ? 'height: 2em;' : ''} ${vertical ? `width: ${lineSpacing}em;` : ''} ${vertical ? `vertical-align: unset;` : ''} } .nonindent, .noindent { text-indent: unset !important; } `; export const getFootnoteStyles = () => ` .duokan-footnote-content, .duokan-footnote-item { display: block !important; } body { padding: 1em !important; overflow-wrap: break-word; } a:any-link { text-decoration: none; padding: unset; margin: unset; } ol { margin: 0; padding: 0; } p, li, blockquote, dd { margin: unset !important; text-indent: unset !important; } div { margin: unset !important; padding: unset !important; } dt { font-weight: bold; line-height: 1.6; } .epubtype-footnote, aside[epub|type~="endnote"], aside[epub|type~="footnote"], aside[epub|type~="note"], aside[epub|type~="rearnote"] { display: block; } `; /** * Baseline stylesheet injected into every dictionary card's shadow root * (alongside any loose `.css` files imported with the bundle and any * `` references resolved from the MDD). * * The seam exists so app-wide rules can be added in one place without * touching the provider code. Currently it ships: */ export const getDictStyles = (bg: string, fg: string, isDarkMode: boolean) => { void fg; return ` a:empty { background-color: transparent; mix-blend-mode: multiply; } a img { mix-blend-mode: multiply; } div[data-dict-kind="mdict"] .entry_name { font-size: 1.2em; margin-block-start: 0.5em; margin-block-end: 0.5em; } div[data-dict-kind="mdict"] .juan_drop { ${isDarkMode ? `background-color: color-mix(in srgb, ${bg} 80%, #000);` : ''} } `; }; const getTranslationStyles = (showSource: boolean) => ` .translation-source { } .translation-target { } .translation-target.hidden { display: none !important; } .translation-target-block { display: block !important; ${showSource ? 'margin: 0.5em 0 !important;' : ''} } .translation-target-toc { display: block !important; overflow: hidden; text-overflow: ellipsis; } `; const getWarichuStyles = () => ` /* Warichu (割注/夹注) — double-line inline annotation */ .warichu-pending { display: inline; font-size: 0.5em; line-height: 1.1; } .warichu-chunk { display: inline-block; line-height: 1.1; font-size: 0.5em; text-indent: 0; vertical-align: middle !important; width: 1lh !important; text-align: center !important; } .warichu-chunk .warichu-line { display: inline; } .warichu-open, .warichu-close { display: inline; font-size: 0.5em; vertical-align: middle; line-height: 1.1; } `; // Word Lens gloss styling is user-configurable: the font size (relative to // the word, in em) and the color. An empty color keeps the default muted, // theme-adaptive look (inherit + 0.7 opacity); a set color paints at full opacity. const getRubyStyles = (viewSettings: ViewSettings) => { const fontSize = viewSettings.wordLensGlossFontSize || 0.5; const color = viewSettings.wordLensGlossColor || ''; return ` rt { user-select: none; -webkit-user-select: none; } rp { display: none !important; } ruby.wl-gloss { cursor: help; } ruby.wl-gloss > rt { font-size: ${fontSize}em; line-height: 1.1; ${color ? `color: ${color};\n opacity: 1;` : 'opacity: 0.7;'} font-weight: normal; text-align: center; } `; }; export interface ThemeCode { bg: string; fg: string; primary: string; palette: Palette; isDarkMode: boolean; } export const getThemeCode = () => { let themeMode = 'auto'; let themeColor = 'default'; let systemIsDarkMode = false; let customThemes: CustomTheme[] = []; if (typeof window !== 'undefined') { themeColor = localStorage.getItem('themeColor') || 'default'; themeMode = localStorage.getItem('themeMode') || 'auto'; systemIsDarkMode = localStorage.getItem('systemIsDarkMode') === 'true'; customThemes = JSON.parse(localStorage.getItem('customThemes') || '[]'); } const isDarkMode = themeMode === 'dark' || (themeMode === 'auto' && systemIsDarkMode); let currentTheme = themes.find((theme) => theme.name === themeColor); if (!currentTheme) { const customTheme = customThemes.find((theme) => theme.name === themeColor); if (customTheme) { currentTheme = { name: customTheme.name, label: customTheme.label, colors: { light: generateLightPalette(customTheme.colors.light), dark: generateDarkPalette(customTheme.colors.dark), }, }; } } if (!currentTheme) currentTheme = themes[0]; const defaultPalette = isDarkMode ? currentTheme!.colors.dark : currentTheme!.colors.light; return { bg: defaultPalette['base-100'], fg: defaultPalette['base-content'], primary: defaultPalette.primary, palette: defaultPalette, isDarkMode, } as ThemeCode; }; export const getStyles = ( viewSettings: ViewSettings, themeCode?: ThemeCode, customFonts: CustomFont[] = [], ) => { if (!themeCode) { themeCode = getThemeCode(); } const pageLayoutStyles = getPageLayoutStyles( viewSettings.marginTopPx, viewSettings.marginRightPx, viewSettings.marginBottomPx, viewSettings.marginLeftPx, 1.0, viewSettings.writingMode!, viewSettings.vertical!, ); const paragraphLayoutStyles = viewSettings.useBookLayout ? '' : getParagraphLayoutStyles( viewSettings.overrideLayout!, viewSettings.paragraphMargin!, viewSettings.lineHeight!, viewSettings.wordSpacing!, viewSettings.letterSpacing!, viewSettings.textIndent!, viewSettings.fullJustification!, viewSettings.hyphenation!, viewSettings.vertical!, ); // scale the font size on-the-fly so that we can sync the same font size on different devices const isMobile = ['ios', 'android'].includes(getOSPlatform()); const fontScale = isMobile ? 1.25 : 1; // Only for backward compatibility, new viewSettings.zoomLevel will always be 100 for EPUBs const zoomScale = (viewSettings.zoomLevel || 100) / 100.0; const fontStyles = getFontStyles( viewSettings.serifFont!, viewSettings.sansSerifFont!, viewSettings.monospaceFont!, viewSettings.defaultFont!, viewSettings.defaultCJKFont!, viewSettings.defaultFontSize! * fontScale * zoomScale, viewSettings.minimumFontSize!, viewSettings.fontWeight!, viewSettings.overrideFont!, ); // Inline `@font-face` rules for the caller-supplied custom fonts so // they ship to the iframe synchronously with the rest of the // stylesheet. Paginator injects this CSS into the iframe `