Add Hebrew/Arabic/Devanagari/Sarabun font packages and unify Noto Sans into the package array

Closes the audit gaps from the original font system commit. The bundled NotoSans only covered Latin/Greek/Cyrillic but the descriptions claimed Arabic, Thai and Hindi too — that was false. DejaVu Sans, the prior dompdf default, did cover Hebrew, Arabic, Armenian and Georgian, so swapping it for NotoSans had silently regressed those scripts. The Thai conditional include was also dropped from every PDF template in that commit, leaving th locales rendering boxes despite THSarabunNew still sitting in resources/static/fonts/.

Adds four on-demand Font Packages — Noto Sans Hebrew, Noto Naskh Arabic (covering Arabic, Persian, Urdu, Sorani Kurdish), Noto Sans Devanagari (Hindi, Marathi, Sanskrit, Nepali) and Sarabun (Thai) — sourced from openmaptiles/fonts and google/fonts as static TTF. Static is mandatory because dompdf's PHP-Font-Lib does not parse variable fonts. Sarabun replaces THSarabunNew as the Thai face: same designer, OFL-licensed, maintained on a stable upstream URL, and surfaces through the same install flow as every other non-Latin script. The bundled THSarabunNew TTF files and the dead app/pdf/locale/th.blade.php legacy partial are removed as part of the migration.

Unifies the bundled Noto Sans into FONT_PACKAGES as a noto-sans entry with bundled => true and files served from resources/static/fonts/ instead of storage/fonts/. FontService::isInstalled, downloadPackage, getInstalledFontFaces and getPackageStatuses honor the flag through a new packageDir() helper. The hardcoded @font-face block in the PDF partial is gone — fonts.blade.php collapses to a single getInstalledFontFaces() call so the package array is the only source of truth for every face, bundled or on-demand. Admin → Font Packages now lists Noto Sans at the top with a primary-colored Bundled pill (new settings.fonts.bundled string) alongside the existing Installed badge / Install button states.

Also fixes the misleading settings.fonts.description and settings.fonts.bundled_info copy to actually describe what ships out of the box vs. what's optional, and rebuilds the en locale chunk.
This commit is contained in:
Darko Gjorgjijoski
2026-04-07 11:50:34 +02:00
parent 27c60bb6f5
commit 04952d91ed
9 changed files with 148 additions and 70 deletions

View File

@@ -17,6 +17,35 @@ class FontService
* Sans / Noto Sans CJK rebuilt as static TTF in Regular + Bold weights.
*/
public const FONT_PACKAGES = [
'noto-sans' => [
'name' => 'Noto Sans (Latin, Greek, Cyrillic)',
'family' => 'NotoSans',
'locales' => [],
'size' => '~1.7MB',
'bundled' => true,
'files' => [
[
'file' => 'NotoSans-Regular.ttf',
'weight' => 'normal',
'style' => 'normal',
],
[
'file' => 'NotoSans-Bold.ttf',
'weight' => 'bold',
'style' => 'normal',
],
[
'file' => 'NotoSans-Italic.ttf',
'weight' => 'normal',
'style' => 'italic',
],
[
'file' => 'NotoSans-BoldItalic.ttf',
'weight' => 'bold',
'style' => 'italic',
],
],
],
'noto-sans-sc' => [
'name' => 'Noto Sans Simplified Chinese',
'family' => 'NotoSansCJKsc',
@@ -97,6 +126,86 @@ class FontService
],
],
],
'noto-sans-hebrew' => [
'name' => 'Noto Sans Hebrew',
'family' => 'NotoSansHebrew',
'locales' => ['he', 'yi'],
'size' => '~40KB',
'files' => [
[
'file' => 'NotoSansHebrew-Regular.ttf',
'url' => 'https://github.com/openmaptiles/fonts/raw/master/noto-sans/NotoSansHebrew-Regular.ttf',
'weight' => 'normal',
'style' => 'normal',
],
[
'file' => 'NotoSansHebrew-Bold.ttf',
'url' => 'https://github.com/openmaptiles/fonts/raw/master/noto-sans/NotoSansHebrew-Bold.ttf',
'weight' => 'bold',
'style' => 'normal',
],
],
],
'noto-naskh-arabic' => [
'name' => 'Noto Naskh Arabic (Arabic, Persian, Urdu)',
'family' => 'NotoNaskhArabic',
'locales' => ['ar', 'fa', 'ur', 'ckb'],
'size' => '~285KB',
'files' => [
[
'file' => 'NotoNaskhArabic-Regular.ttf',
'url' => 'https://github.com/openmaptiles/fonts/raw/master/noto-sans/NotoNaskhArabic-Regular.ttf',
'weight' => 'normal',
'style' => 'normal',
],
[
'file' => 'NotoNaskhArabic-Bold.ttf',
'url' => 'https://github.com/openmaptiles/fonts/raw/master/noto-sans/NotoNaskhArabic-Bold.ttf',
'weight' => 'bold',
'style' => 'normal',
],
],
],
'noto-sans-devanagari' => [
'name' => 'Noto Sans Devanagari (Hindi, Marathi, Sanskrit, Nepali)',
'family' => 'NotoSansDevanagari',
'locales' => ['hi', 'mr', 'sa', 'ne'],
'size' => '~280KB',
'files' => [
[
'file' => 'NotoSansDevanagari-Regular.ttf',
'url' => 'https://github.com/openmaptiles/fonts/raw/master/noto-sans/NotoSansDevanagari-Regular.ttf',
'weight' => 'normal',
'style' => 'normal',
],
[
'file' => 'NotoSansDevanagari-Bold.ttf',
'url' => 'https://github.com/openmaptiles/fonts/raw/master/noto-sans/NotoSansDevanagari-Bold.ttf',
'weight' => 'bold',
'style' => 'normal',
],
],
],
'sarabun' => [
'name' => 'Sarabun (Thai)',
'family' => 'Sarabun',
'locales' => ['th'],
'size' => '~180KB',
'files' => [
[
'file' => 'Sarabun-Regular.ttf',
'url' => 'https://github.com/google/fonts/raw/main/ofl/sarabun/Sarabun-Regular.ttf',
'weight' => 'normal',
'style' => 'normal',
],
[
'file' => 'Sarabun-Bold.ttf',
'url' => 'https://github.com/google/fonts/raw/main/ofl/sarabun/Sarabun-Bold.ttf',
'weight' => 'bold',
'style' => 'normal',
],
],
],
];
/**
@@ -126,13 +235,27 @@ class FontService
}
}
/**
* Resolve where a package's font files live on disk.
* Bundled packages ship with the repo under resources/static/fonts/;
* on-demand packages are downloaded into storage/fonts/.
*/
private function packageDir(array $package): string
{
return ! empty($package['bundled'])
? resource_path('static/fonts')
: storage_path('fonts');
}
/**
* Check if a font package is installed (all files present).
*/
public function isInstalled(array $package): bool
{
$dir = $this->packageDir($package);
foreach ($package['files'] as $entry) {
if (! File::exists(storage_path('fonts/'.$entry['file']))) {
if (! File::exists($dir.'/'.$entry['file'])) {
return false;
}
}
@@ -142,9 +265,14 @@ class FontService
/**
* Download a font package to storage/fonts/.
* No-op for bundled packages — those ship with the repo.
*/
public function downloadPackage(array $package): void
{
if (! empty($package['bundled'])) {
return;
}
$fontsDir = storage_path('fonts');
if (! File::isDirectory($fontsDir)) {
@@ -181,6 +309,7 @@ class FontService
'locales' => $package['locales'],
'size' => $package['size'],
'installed' => $this->isInstalled($package),
'bundled' => ! empty($package['bundled']),
];
}
@@ -195,15 +324,16 @@ class FontService
public function getInstalledFontFaces(): array
{
$faces = [];
$fontsDir = storage_path('fonts');
foreach (self::FONT_PACKAGES as $package) {
if (! $this->isInstalled($package)) {
continue;
}
$dir = $this->packageDir($package);
foreach ($package['files'] as $entry) {
$filePath = $fontsDir.'/'.$entry['file'];
$filePath = $dir.'/'.$entry['file'];
$faces[] = "@font-face {
font-family: '{$package['family']}';

View File

@@ -1354,9 +1354,10 @@
},
"fonts": {
"title": "Font Packages",
"description": "These font packages are used exclusively when generating PDF documents. The bundled Noto Sans already covers Latin, Cyrillic, Greek, Arabic, Thai and Hindi — install the packages below to render Chinese, Japanese or Korean characters in your PDFs.",
"bundled_info": "Noto Sans (bundled) covers Latin, Cyrillic, Greek, Arabic, Thai, Hindi, and most other scripts. Install additional packages below for CJK language support.",
"description": "These font packages are used exclusively when generating PDF documents. The bundled Noto Sans covers Latin, Greek and Cyrillic scripts. Install the packages below to render Hebrew, Arabic, Persian, Urdu, Hindi (Devanagari), Thai or East Asian (CJK) characters in your PDFs.",
"bundled_info": "Bundled font: Noto Sans (Latin / Greek / Cyrillic). Install additional packages below for Hebrew, Arabic, Persian, Hindi, Thai or CJK support.",
"installed": "Installed",
"bundled": "Bundled",
"install": "Install",
"downloading": "Downloading...",
"download_started": "Downloading {name}. This may take a moment.",

View File

@@ -12,6 +12,7 @@ interface FontPackage {
locales: string[]
size: string
installed: boolean
bundled?: boolean
}
const { t } = useI18n()
@@ -84,7 +85,14 @@ async function installFont(pkg: FontPackage): Promise<void> {
<div class="flex items-center gap-3">
<span
v-if="pkg.installed"
v-if="pkg.bundled"
class="inline-flex items-center rounded-full bg-primary-50 px-2.5 py-1 text-xs font-medium text-primary-600"
>
{{ $t('settings.fonts.bundled') }}
</span>
<span
v-else-if="pkg.installed"
class="inline-flex items-center rounded-full bg-success px-2.5 py-1 text-xs font-medium text-status-green"
>
{{ $t('settings.fonts.installed') }}

View File

@@ -1,34 +0,0 @@
<style type="text/css">
@font-face {
font-family: 'THSarabunNew';
font-style: normal;
font-weight: normal;
src: url("{{ resource_path('static/fonts/THSarabunNew.ttf') }}") format('truetype');
}
@font-face {
font-family: 'THSarabunNew';
font-style: normal;
font-weight: bold;
src: url("{{ resource_path('static/fonts/THSarabunNew-Bold.ttf') }}") format('truetype');
}
@font-face {
font-family: 'THSarabunNew';
font-style: italic;
font-weight: normal;
src: url("{{ resource_path('static/fonts/THSarabunNew-Italic.ttf') }}") format('truetype');
}
@font-face {
font-family: 'THSarabunNew';
font-style: italic;
font-weight: bold;
src: url("{{ resource_path('static/fonts/THSarabunNew-BoldItalic.ttf') }}") format('truetype');
}
body {
font-family: "THSarabunNew", sans-serif !important;
}
</style>

View File

@@ -1,34 +1,7 @@
{{-- Bundled Noto Sans covers Latin, Cyrillic, Greek, Arabic, Thai, Hindi, and most scripts --}}
{{-- All @font-face rules bundled and on-demand are emitted by
FontService::getInstalledFontFaces(). Bundled packages live under
resources/static/fonts/, on-demand packages under storage/fonts/. --}}
<style type="text/css">
@font-face {
font-family: 'NotoSans';
font-style: normal;
font-weight: normal;
src: url("{{ resource_path('static/fonts/NotoSans-Regular.ttf') }}") format('truetype');
}
@font-face {
font-family: 'NotoSans';
font-style: normal;
font-weight: bold;
src: url("{{ resource_path('static/fonts/NotoSans-Bold.ttf') }}") format('truetype');
}
@font-face {
font-family: 'NotoSans';
font-style: italic;
font-weight: normal;
src: url("{{ resource_path('static/fonts/NotoSans-Italic.ttf') }}") format('truetype');
}
@font-face {
font-family: 'NotoSans';
font-style: italic;
font-weight: bold;
src: url("{{ resource_path('static/fonts/NotoSans-BoldItalic.ttf') }}") format('truetype');
}
{{-- On-demand CJK @font-face rules (only emitted for installed packages). --}}
{!! implode("\n", app(\App\Services\FontService::class)->getInstalledFontFaces()) !!}
body {