Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 44 additions & 5 deletions apps/web/docs/fonts.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ Checked 2026-10-20.
| `SortsMillGoudy-Regular.woff`, `SortsMillGoudy-Italic.woff` | Sorts Mill Goudy Regular / Italic (v003.101) | Copyright (c) 2010, Barry Schwartz (chemoelectric@chemoelectric.org) | SIL OFL 1.1 | https://github.com/google/fonts/blob/main/ofl/sortsmillgoudy/OFL.txt | Keep |
| `YanoneKaffeesatz-Light.woff`, `YanoneKaffeesatz-Regular.woff`, `YanoneKaffeesatz-Bold.woff` | Yanone Kaffeesatz Light / Regular / Bold (v1.003) | Copyright 2010 The Yanone Kaffeesatz Project Authors (https://github.com/yanone/kaffeesatz) | SIL OFL 1.1 | https://github.com/google/fonts/blob/main/ofl/yanonekaffeesatz/OFL.txt | Keep |
| `ShadowsIntoLight.woff2` | Shadows Into Light (v001.000) | Copyright (c) 2010, Kimberly Geswein (kimberlygeswein.com) | SIL OFL 1.1 | https://github.com/google/fonts/blob/main/ofl/shadowsintolight/OFL.txt | Keep |
| `SGBatang-Regular.woff2`, `SGBatang-Bold.woff2` | SG Batang: subset of KoPub Batang Regular / Bold (v1.01), renamed | Copyright 2014 Ministry of Culture, Sports and Tourism and Korea Publishers Society, Reserved Font Name 'KoPub Batang' | SIL OFL 1.1 | https://github.com/google/fonts/tree/main/ofl/kopubbatang | Keep |
| `NotoSerifDevanagari-Regular.woff2`, `NotoSerifDevanagari-Bold.woff2` | Noto Serif Devanagari, static subsets of the variable font | Copyright 2022 The Noto Project Authors | SIL OFL 1.1 | https://github.com/google/fonts/tree/main/ofl/notoserifdevanagari | Keep |

The copyright lines above are copied from each family's upstream `OFL.txt` in the
google/fonts repository. They are not all in the font files themselves: the Sorts Mill
Expand All @@ -28,10 +30,39 @@ copies did not meet that condition. `public/css/webfont/OFL.txt` now sits beside
fonts (and is served at `/css/webfont/OFL.txt`) with the three copyright lines followed by
the full OFL 1.1 text, as Google Fonts packages it per family.

## Language fonts

Checked 2026-11-01 against `scripture_versions`: 89 English versions, 7 Korean (`KRBOM`, `KRSEB`,
`KLB`, `KRLDS`, `ERKRB`, `KRBOM67`, `KRREB`) and 1 Hindi (`HIBOM`). `versions.css` declares the faces,
which browsers download only on pages that use them.

Korean used to ask for KoPub Batang at `fonts.gstatic.com/ea/kopubbatang/v3/`. Those files now
return 404, although Google's CSS at `fonts.googleapis.com/earlyaccess/kopubbatang.css` still lists
them, so each device fell back to its own system font. `KLB` and `KRREB` were not in the rule at all.
The fonts are now self-hosted, so a third party cannot take them away again. `scripts/build-fonts.py`
rebuilds them from the google/fonts sources.

KoPub licensing: Google's copy is under the OFL, and "KoPub Batang" is a Reserved Font Name. A subset
is a Modified Version, so these files and the CSS family are named `SG Batang`. KOPUS's own download
page for the later KoPubWorld release says serving a font from a server for a web service needs
separate approval. These files do not come from that release. Do not swap them for a KoPubWorld
download without that approval.

The Korean subset holds the 2,350 KS X 1001 syllables plus every syllable the Korean versions use
(1,425 distinct), about 225 KB per weight. `body` uses `word-break: keep-all`, so Korean lines break
between words; it does not affect English. In Korean versions synthesized bold and oblique are off,
because Hangul has no true italic: headnotes are upright.

Pages with no version class (parallel view) get these fonts through the `body` stack. Those faces
(`SG Hangul`, the same files as `SG Batang`, and `Noto Serif Devanagari`) have a `unicode-range`, so
a page downloads them only if it has such characters.

## Served privately

The site's text font is the owner's licensed "Goudy Scripture" (one file, `scripture.woff2`:
weight 400, upright, 563 glyphs including Latin Extended and Cyrillic). The owner is licensing
upright, 563 glyphs including Latin Extended and Cyrillic). Its metadata is mixed (usWeightClass
400, but the Bold subfamily and style bits) and its outlines are heavy, about as heavy as Goudy
Old Style Bold, so nothing should thicken it further. The owner is licensing
it (decision 2026-10-20). A licence lets the site serve the font to visitors; it does not allow
publishing the file in this public repository or in the Docker image. So the file is never
committed or built into the image, and the app only points at it.
Expand All @@ -49,7 +80,7 @@ emits, after the stylesheets, a `<style id="private-fonts">` with two faces:
@font-face {
font-family: 'Goudy Scripture';
src: url('<FONT_BASE>/scripture.woff2') format('woff2');
font-weight: 400;
font-weight: 400 1000;
font-style: normal;
font-display: swap;
}
Expand All @@ -64,14 +95,22 @@ emits, after the stylesheets, a `<style id="private-fonts">` with two faces:

The licensed file is upright only, so italic text (prompts, headnotes, summaries, backgrounds,
`parallel.css`) uses Sorts Mill Goudy Italic, a real italic, instead of an oblique the browser
would synthesize. Bold text is still synthesized from the regular face.
would synthesize. The upright face covers weights 400 to 1000, so browsers do not synthesize bold
from it: bold text and headings use the same outlines as body text. The italic face stays at 400,
because Sorts Mill Goudy Italic is light and bold italic still needs synthesizing.

Scripture text is `1.25rem` (20px by default, set on `.passage`, and sizes inside it scale in
`em`). The earlier Chrome-only `font-weight: 800` rule was removed: it suited the lighter Sorts Mill
Goudy but smeared this font. Without the private font, verses therefore render at regular weight
in Sorts Mill Goudy in Chrome too.


Neither set: no `@font-face` for `'Goudy Scripture'`, so the page requests no font that would
404. At startup, when the font comes from `FONT_DIR` (`FONT_BASE` unset or `/private-fonts`)
and `FONT_DIR/<FONT_GOUDY_FILE>` does not exist, the app logs a warning and emits no
`@font-face` either. An external `FONT_BASE` is not checked.

The body stack in `style.css` is `'Goudy Scripture', 'Sorts Mill Goudy', Georgia, serif` (live:
The body stack in `style.css` is `'Goudy Scripture', 'Sorts Mill Goudy', 'SG Hangul', 'Noto Serif Devanagari', Georgia, serif` (live:
`'Sorts Mill Goudy', 'Goudy', Georgia, serif`). With the private font configured, it is the text
font; without it, the browser has no `'Goudy Scripture'` face and uses Sorts Mill Goudy, as
before. No standard system font has this family name, but a machine with Goudy Scripture installed locally uses that copy when nothing is configured, so test the Sorts Mill Goudy fallback on another machine. Notes:
Expand Down Expand Up @@ -133,7 +172,7 @@ name). The live site still serves them, but no page uses them, so they were dele
| File(s) | Family | Copyright holder (from file) | License | Notes |
|---|---|---|---|---|
| `Comfortaa-Regular.woff`, `Comfortaa-Bold.woff` | Comfortaa (v2.001) | Not in file | SIL OFL 1.1 (name ID 14) | Unused |
| `UnBatang.ttf`, `UnBatangBold.ttf` | Un Batang / Un Batang Bold (v1.00) | Copyright © 1998-2004 by Koaunghi Un | "GNU Public License" (name ID 13; URL `http://korea.gnu.org/copyleft/gpl.ko.html`). Whether this copy carries the font exception is not stated in the file. | Unused (Korean text uses KoPub Batang from Google Fonts in `versions.css`) |
| `UnBatang.ttf`, `UnBatangBold.ttf` | Un Batang / Un Batang Bold (v1.00) | Copyright © 1998-2004 by Koaunghi Un | "GNU Public License" (name ID 13; URL `http://korea.gnu.org/copyleft/gpl.ko.html`). Whether this copy carries the font exception is not stated in the file. | Unused (Korean text uses SG Batang, see Language fonts) |
| `LotteMartBold.ttf` | 12LotteMartDreamBold | Not in file | Unknown (no license fields; not verified against Lotte Mart's terms) | Unused |
| `palatino.ttf` | Palatino (v1.0) | Not in file | Unknown (no copyright or license fields; unique ID "DHBK Tp.HCM:231:Palatino" suggests a modified third-party copy; Palatino is a Linotype design) | Unused |

Expand Down
15 changes: 3 additions & 12 deletions apps/web/public/css/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -44,19 +44,9 @@
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

/* Use Chrome's Faux Bold */
body.chrome .passage span,
body.chrome .parallel td,
body.chrome div .section_footer,
body.chrome .gridtext,
body.chrome .diffmodel .original,
body.chrome .diffmodel .changed,
body.chrome .passage div {
font-weight: 800;
}

body {
font-family: 'Goudy Scripture', 'Sorts Mill Goudy', Georgia, serif;
font-family: 'Goudy Scripture', 'Sorts Mill Goudy', 'SG Hangul', 'Noto Serif Devanagari', Georgia, serif;
word-break: keep-all; /* only affects Korean, Chinese and Japanese: break lines between words */
font-size: 1.5em;
line-height: 1.5em;
background-color: #333;
Expand Down Expand Up @@ -304,6 +294,7 @@
}
.passage {
overflow: hidden;
font-size: 1.25rem;
}
.passage div button {
text-align: center;
Expand Down
72 changes: 49 additions & 23 deletions apps/web/public/css/versions.css
Original file line number Diff line number Diff line change
Expand Up @@ -267,46 +267,72 @@
}

/*
* KoPub Batang (Korean) http://www.google.com/fonts/earlyaccess
* Korean: 'SG Batang', a subset of KoPub Batang. Hindi: Noto Serif Devanagari. Both are self-hosted
* under the OFL (docs/fonts.md) and load only on pages that use them. They have real bold weights
* and Korean is not set in italics, so Korean turns off synthesized bold and oblique.
*/
@font-face {
font-family: 'KoPub Batang';
font-family: 'SG Batang';
font-style: normal;
font-weight: 300;
src: url(//fonts.gstatic.com/ea/kopubbatang/v3/KoPubBatang-Light.eot);
src: url(//fonts.gstatic.com/ea/kopubbatang/v3/KoPubBatang-Light.eot?#iefix) format('embedded-opentype'),
url(//fonts.gstatic.com/ea/kopubbatang/v3/KoPubBatang-Light.woff2) format('woff2'),
url(//fonts.gstatic.com/ea/kopubbatang/v3/KoPubBatang-Light.woff) format('woff'),
url(//fonts.gstatic.com/ea/kopubbatang/v3/KoPubBatang-Light.ttf) format('truetype');
font-weight: 400;
font-display: swap;
src: url('/css/webfont/SGBatang-Regular.woff2') format('woff2');
}
@font-face {
font-family: 'SG Batang';
font-style: normal;
font-weight: 700;
font-display: swap;
src: url('/css/webfont/SGBatang-Bold.woff2') format('woff2');
}
/* The same files for Hangul in the site-wide stack (parallel view, labels). The range keeps English
pages from downloading them. */
@font-face {
font-family: 'KoPub Batang';
font-family: 'SG Hangul';
font-style: normal;
font-weight: 400;
src: url(//fonts.gstatic.com/ea/kopubbatang/v3/KoPubBatang-Regular.eot);
src: url(//fonts.gstatic.com/ea/kopubbatang/v3/KoPubBatang-Regular.eot?#iefix) format('embedded-opentype'),
url(//fonts.gstatic.com/ea/kopubbatang/v3/KoPubBatang-Regular.woff2) format('woff2'),
url(//fonts.gstatic.com/ea/kopubbatang/v3/KoPubBatang-Regular.woff) format('woff'),
url(//fonts.gstatic.com/ea/kopubbatang/v3/KoPubBatang-Regular.ttf) format('truetype');
font-display: swap;
src: url('/css/webfont/SGBatang-Regular.woff2') format('woff2');
unicode-range: U+1100-11FF, U+3000-303F, U+3130-318F, U+AC00-D7A3;
}
@font-face {
font-family: 'KoPub Batang';
font-family: 'SG Hangul';
font-style: normal;
font-weight: 700;
src: url(//fonts.gstatic.com/ea/kopubbatang/v3/KoPubBatang-Bold.eot);
src: url(//fonts.gstatic.com/ea/kopubbatang/v3/KoPubBatang-Bold.eot?#iefix) format('embedded-opentype'),
url(//fonts.gstatic.com/ea/kopubbatang/v3/KoPubBatang-Bold.woff2) format('woff2'),
url(//fonts.gstatic.com/ea/kopubbatang/v3/KoPubBatang-Bold.woff) format('woff'),
url(//fonts.gstatic.com/ea/kopubbatang/v3/KoPubBatang-Bold.ttf) format('truetype');
font-display: swap;
src: url('/css/webfont/SGBatang-Bold.woff2') format('woff2');
unicode-range: U+1100-11FF, U+3000-303F, U+3130-318F, U+AC00-D7A3;
}
@font-face {
font-family: 'Noto Serif Devanagari';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url('/css/webfont/NotoSerifDevanagari-Regular.woff2') format('woff2');
unicode-range: U+0900-097F, U+1CD0-1CFF, U+200C-200D, U+20B9, U+25CC, U+A8E0-A8FF;
}
@font-face {
font-family: 'Noto Serif Devanagari';
font-style: normal;
font-weight: 700;
font-display: swap;
src: url('/css/webfont/NotoSerifDevanagari-Bold.woff2') format('woff2');
unicode-range: U+0900-097F, U+1CD0-1CFF, U+200C-200D, U+20B9, U+25CC, U+A8E0-A8FF;
}

.ver_KRSEB,
.ver_KRLDS,
.ver_KRBOM,
.ver_ERKRB,
.ver_KRBOM67
.ver_KRBOM67,
.ver_KRREB,
.ver_KLB
{
font-family: 'SG Batang', serif;
font-synthesis: small-caps;
}
.ver_HIBOM
{
font-family: 'KoPub Batang', serif;
font-family: 'Noto Serif Devanagari', 'Goudy Scripture', 'Sorts Mill Goudy', serif;
}

.ver_KRSEB p .verse,
Expand Down
Binary file not shown.
Binary file not shown.
3 changes: 3 additions & 0 deletions apps/web/public/css/webfont/OFL.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
Copyright (c) 2010, Barry Schwartz (chemoelectric@chemoelectric.org)
Copyright 2010 The Yanone Kaffeesatz Project Authors (https://github.com/yanone/kaffeesatz)
Copyright (c) 2010, Kimberly Geswein (kimberlygeswein.com)
Copyright 2014 Ministry of Culture, Sports and Tourism and Korea Publishers Society (kopus@kopus.org), with Reserved Font Name 'KoPub Dotum', 'KoPub Batang'
SGBatang-*.woff2 are Modified Versions: subsets of KoPub Batang, renamed 'SG Batang' as the Reserved Font Name requires.
Copyright 2022 The Noto Project Authors (https://github.com/notofonts/devanagari)

This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
Expand Down
Binary file added apps/web/public/css/webfont/SGBatang-Bold.woff2
Binary file not shown.
Binary file not shown.
60 changes: 60 additions & 0 deletions apps/web/scripts/build-fonts.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
#!/usr/bin/env python3
"""Build the Korean and Hindi web fonts in public/css/webfont from their OFL sources.

Sources (google/fonts, ofl/kopubbatang and ofl/notoserifdevanagari):
KoPubBatang-Regular.ttf, KoPubBatang-Bold.ttf, NotoSerifDevanagari[wdth,wght].ttf

Usage: build-fonts.py <source dir> <output dir> [extra-syllables.json]

'KoPub Batang' is a Reserved Font Name under its OFL, and a subset is a Modified Version, so
the subset is renamed 'SG Batang' in its name table and in CSS. Noto has no Reserved Font Name.
"""
import json
import sys
from pathlib import Path

from fontTools import subset
from fontTools.ttLib import TTFont
from fontTools.varLib import instancer

BATANG = 'SG Batang'
# KS X 1001: the 2,350 Hangul syllables in everyday use. Lead bytes B0-C8, trail bytes A1-FE.
KS_X_1001 = {ord(bytes([a, b]).decode('euc-kr')) for a in range(0xB0, 0xC9) for b in range(0xA1, 0xFF)}
BATANG_EXTRA = [*range(0x20, 0x7F), *range(0xA0, 0x100), 0x2013, 0x2014, 0x2018, 0x2019, 0x201C, 0x201D,
0x2026, 0x3001, 0x3002, 0x3008, 0x3009, 0x300A, 0x300B, 0x300C, 0x300D, 0x318D]
DEVANAGARI = [*range(0x900, 0x980), *range(0x1CD0, 0x1D00), *range(0xA8E0, 0xA900), 0x200C, 0x200D, 0x25CC, 0x20B9]


def make_subset(font, unicodes, out):
opts = subset.Options(flavor='woff2', layout_features=['*'], hinting=False, notdef_outline=True,
name_IDs=['*'], name_languages=['*'], drop_tables=subset.Options().drop_tables + ['FFTM'])
sub = subset.Subsetter(opts)
sub.populate(unicodes=unicodes)
sub.subset(font)
subset.save_font(font, str(out), opts)
print(f'{out.name}: {out.stat().st_size / 1024:.0f} KB')


def rename(font, family, style):
ps = f"{family.replace(' ', '')}-{style}"
values = {1: family, 2: style, 3: ps, 4: f'{family} {style}', 6: ps}
name = font['name']
# Remove every record that could still carry the Reserved Font Name, then write ours.
name.names = [r for r in name.names if r.nameID in (0, 13, 14)]
for nid, value in values.items():
name.setName(value, nid, 3, 1, 0x409)


def main():
src, out = Path(sys.argv[1]), Path(sys.argv[2])
syllables = {ord(c) for c in json.loads(Path(sys.argv[3]).read_text())} if len(sys.argv) > 3 else set()
for style in ('Regular', 'Bold'):
font = TTFont(src / f'KoPubBatang-{style}.ttf')
rename(font, BATANG, style)
make_subset(font, [*KS_X_1001, *syllables, *BATANG_EXTRA], out / f'SGBatang-{style}.woff2')
for style, wght in (('Regular', 400), ('Bold', 700)):
font = instancer.instantiateVariableFont(TTFont(src / 'NotoSerifDevanagari[wdth,wght].ttf'), {'wght': wght, 'wdth': 100})
make_subset(font, DEVANAGARI, out / f'NotoSerifDevanagari-{style}.woff2')


main()
10 changes: 6 additions & 4 deletions apps/web/src/presentation/fontFaces.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@ export const FAMILY = 'Goudy Scripture';
const SAFE = /^[A-Za-z0-9._~:/?#@!$&*+,;=%-]*$/; // no quotes, parentheses, spaces or angle brackets
const FORMATS = { woff2: 'woff2', woff: 'woff', ttf: 'truetype', otf: 'opentype' };

const face = (style, src) => `@font-face {
const face = (style, weight, src) => `@font-face {
font-family: '${FAMILY}';
src: ${src};
font-weight: 400;
font-weight: ${weight};
font-style: ${style};
font-display: swap;
}`;
Expand All @@ -26,9 +26,11 @@ export function goudyFontFace({ base, goudyFile }) {
}
const format = FORMATS[goudyFile.split('.').pop().toLowerCase()];
return [
face('normal', `url('${base}/${goudyFile}')${format ? ` format('${format}')` : ''}`),
// The file's outlines are already heavy, whatever its metadata says. Covering the whole
// weight range stops browsers thickening it further for bold text and headings.
face('normal', '400 1000', `url('${base}/${goudyFile}')${format ? ` format('${format}')` : ''}`),
// The licensed file is upright only; a real italic beats the browser's synthesized oblique.
face('italic', "url('/css/webfont/SortsMillGoudy-Italic.woff') format('woff')"),
face('italic', '400', "url('/css/webfont/SortsMillGoudy-Italic.woff') format('woff')"),
].join('\n');
}

Expand Down
Loading
Loading