mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-07 04:12:04 +00:00
build(pdf): add Unicode fonts for the doctor report
The report's built-in Helvetica is WinAnsi-only. These fonts cover what the report can carry: ReportSans (Noto Sans plus the arrows and mathematical operators of Noto Sans Math; Latin incl. Extended-A/B, Greek, Cyrillic) in regular, bold and italic, and ReportSansKR, the same plus Hangul from Noto Sans KR, in regular and bold. scripts/build-pdf-fonts.py rebuilds them from the upstream sources. All are SIL OFL 1.1; the licence sits beside the files. The directory is named in outputFileTracingIncludes because the renderer reads the files by a path the tracer cannot follow.
This commit is contained in:
1 parent
577e43004f
commit
80a62afbbf
9 files changed
+290
No files matched your search
@@ -309,6 +309,9 @@ const nextConfig: NextConfig = {
|
||||
"./src/lib/documents/pdf-child.mjs",
|
||||
"./src/lib/documents/pdf-engine.mjs",
|
||||
"./src/lib/documents/image-engine.mjs",
|
||||
// The doctor-report PDF embeds its fonts, read from disk by a path the
|
||||
// tracer cannot follow (`src/lib/doctor-report-pdf/fonts.ts`).
|
||||
"./src/lib/doctor-report-pdf/fonts/**",
|
||||
// Document AI local text extraction and rasterization both run the
|
||||
// app's one pdfjs-dist (`src/lib/documents/pdfjs.ts`) server-side; the
|
||||
// text read is the provider-free fallback that keeps content search
|
||||
|
||||
@@ -31,6 +31,7 @@ typecheck.
|
||||
- **`bench/`** — `seed-large-account.ts` (a synthetic ten-year account of about 1.26 million measurement rows) and `run.ts` (latency and SQL statements per request against it); see `docs/contributing/benchmarks.md`. Results land in `bench/results/`.
|
||||
- **`test-notifications.ts`** — exercise the notification dispatcher + reminder check.
|
||||
- **`fetch-geolite2.sh`** — download the MaxMind GeoLite2 MMDBs (needs a licence key) before `docker build`.
|
||||
- **`build-pdf-fonts.py`** — rebuild the fonts the doctor-report PDF embeds (`src/lib/doctor-report-pdf/fonts/`) from the upstream Noto sources; needs `fonttools`. The source URLs are in its header.
|
||||
- **`install-hooks.sh`** — install the local git hooks.
|
||||
- **`generate-sw-version.mjs`**, **`print-bundle-report.mjs`** — service-worker version stamp and bundle-size report.
|
||||
- **`i18n/`** — locale tooling; **`__tests__/`** — tests for the scripts above.
|
||||
|
||||
@@ -0,0 +1,191 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Build the fonts the doctor-report PDF embeds.
|
||||
|
||||
jsPDF draws one font per text run and has no glyph fallback, so every script
|
||||
the report can carry has to live in one font file. No single upstream Noto
|
||||
font covers what the report needs: Noto Sans has Latin, Greek and Cyrillic but
|
||||
no arrows or comparison signs, Noto Sans KR has Hangul but no Latin
|
||||
Extended-A (Polish). This script merges them:
|
||||
|
||||
ReportSans-{Regular,Bold,Italic}.ttf
|
||||
Noto Sans (Latin, Latin Extended-A/B, Greek, Cyrillic, punctuation,
|
||||
super-/subscripts, currency) plus the arrows and mathematical operators
|
||||
of Noto Sans Math.
|
||||
ReportSansKR-{Regular,Bold}.ttf
|
||||
The matching ReportSans weight plus Hangul (syllables, jamo,
|
||||
compatibility jamo) and CJK punctuation from Noto Sans KR, instanced
|
||||
at weight 400 / 700. Loaded only for a report that contains Hangul.
|
||||
|
||||
All sources are SIL Open Font License 1.1; the licence texts sit beside the
|
||||
output. The merged fonts carry their own family name, as the OFL asks of a
|
||||
modified version. Layout tables (GSUB/GPOS/GDEF/MATH) are dropped: jsPDF
|
||||
does no shaping, and precomposed Hangul syllables need none.
|
||||
|
||||
Requires fonttools (`pip install fonttools`). Usage:
|
||||
|
||||
python3 scripts/build-pdf-fonts.py <download-dir> <output-dir>
|
||||
|
||||
where <download-dir> holds the unhinted upstream TTFs:
|
||||
NotoSans-Regular.ttf, NotoSans-Bold.ttf, NotoSans-Italic.ttf
|
||||
https://github.com/notofonts/notofonts.github.io/tree/main/fonts/NotoSans/unhinted/ttf
|
||||
NotoSansMath-Regular.ttf
|
||||
https://github.com/notofonts/notofonts.github.io/tree/main/fonts/NotoSansMath/unhinted/ttf
|
||||
NotoSansKR[wght].ttf
|
||||
https://github.com/google/fonts/tree/main/ofl/notosanskr
|
||||
"""
|
||||
|
||||
import copy
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
from fontTools import subset
|
||||
from fontTools.ttLib import TTFont
|
||||
from fontTools.varLib import instancer
|
||||
|
||||
|
||||
def ranges(*spans):
|
||||
out = set()
|
||||
for lo, hi in spans:
|
||||
out.update(range(lo, hi + 1))
|
||||
return out
|
||||
|
||||
|
||||
BASE_CODEPOINTS = ranges(
|
||||
(0x0020, 0x007E), # Basic Latin
|
||||
(0x00A0, 0x024F), # Latin-1, Latin Extended-A/B
|
||||
(0x0250, 0x02FF), # IPA, spacing modifiers
|
||||
(0x0300, 0x036F), # combining diacritics
|
||||
(0x0370, 0x03FF), # Greek
|
||||
(0x0400, 0x052F), # Cyrillic + supplement
|
||||
(0x1E00, 0x1EFF), # Latin Extended Additional
|
||||
(0x2000, 0x206F), # general punctuation
|
||||
(0x2070, 0x209F), # super-/subscripts
|
||||
(0x20A0, 0x20CF), # currency
|
||||
(0x2100, 0x214F), # letterlike (°C, №, ™)
|
||||
(0x2150, 0x218F), # number forms
|
||||
)
|
||||
SYMBOL_CODEPOINTS = ranges(
|
||||
(0x2190, 0x21FF), # arrows
|
||||
(0x2200, 0x22FF), # mathematical operators (≈ ≤ ≥ ±)
|
||||
)
|
||||
HANGUL_CODEPOINTS = ranges(
|
||||
(0x1100, 0x11FF), # Hangul jamo
|
||||
(0x3000, 0x303F), # CJK symbols and punctuation
|
||||
(0x3130, 0x318F), # compatibility jamo
|
||||
(0xA960, 0xA97F), # jamo extended-A
|
||||
(0xAC00, 0xD7A3), # syllables
|
||||
(0xD7B0, 0xD7FF), # jamo extended-B
|
||||
)
|
||||
DROP_TABLES = ["GSUB", "GPOS", "GDEF", "MATH", "BASE", "JSTF", "DSIG", "STAT"]
|
||||
|
||||
|
||||
def subset_font(font: TTFont, codepoints: set[int]) -> TTFont:
|
||||
options = subset.Options()
|
||||
options.layout_features = []
|
||||
options.drop_tables += DROP_TABLES
|
||||
options.hinting = False
|
||||
options.notdef_outline = True
|
||||
options.name_IDs = ["*"]
|
||||
options.glyph_names = True
|
||||
subsetter = subset.Subsetter(options)
|
||||
subsetter.populate(unicodes=codepoints & set(font.getBestCmap()))
|
||||
subsetter.subset(font)
|
||||
return font
|
||||
|
||||
|
||||
def copy_glyph(base: TTFont, donor: TTFont, name: str, prefix: str) -> str:
|
||||
"""Copy one donor glyph (and its components) into base; return its name."""
|
||||
new_name = f"{prefix}{name}"
|
||||
glyf = base["glyf"]
|
||||
if new_name in glyf.glyphs:
|
||||
return new_name
|
||||
glyph = copy.deepcopy(donor["glyf"][name])
|
||||
if glyph.isComposite():
|
||||
for component in glyph.components:
|
||||
component.glyphName = copy_glyph(
|
||||
base, donor, component.glyphName, prefix
|
||||
)
|
||||
glyf.glyphs[new_name] = glyph
|
||||
glyf.glyphOrder.append(new_name)
|
||||
base["hmtx"].metrics[new_name] = donor["hmtx"].metrics[name]
|
||||
order = base.getGlyphOrder()
|
||||
if new_name not in order:
|
||||
order.append(new_name)
|
||||
base.setGlyphOrder(order)
|
||||
return new_name
|
||||
|
||||
|
||||
def merge(base: TTFont, donor: TTFont, prefix: str) -> None:
|
||||
if base["head"].unitsPerEm != donor["head"].unitsPerEm:
|
||||
raise SystemExit("unitsPerEm differs between base and donor")
|
||||
base_cmap = base.getBestCmap()
|
||||
additions = {}
|
||||
for cp, name in donor.getBestCmap().items():
|
||||
if cp in base_cmap:
|
||||
continue
|
||||
additions[cp] = copy_glyph(base, donor, name, prefix)
|
||||
for table in base["cmap"].tables:
|
||||
if table.isUnicode():
|
||||
if table.format == 4 and any(cp > 0xFFFF for cp in additions):
|
||||
continue
|
||||
table.cmap.update(additions)
|
||||
base["maxp"].numGlyphs = len(base.getGlyphOrder())
|
||||
base["hhea"].numberOfHMetrics = len(base.getGlyphOrder())
|
||||
base["hhea"].advanceWidthMax = max(
|
||||
base["hhea"].advanceWidthMax,
|
||||
max(width for width, _ in donor["hmtx"].metrics.values()),
|
||||
)
|
||||
|
||||
|
||||
def finish(font: TTFont, family: str, style: str, path: Path) -> None:
|
||||
"""Rename, drop glyph names, save.
|
||||
|
||||
jsPDF copies the whole `post` table into every PDF it writes, subset or
|
||||
not, so glyph names would cost each report tens of kilobytes (hundreds
|
||||
with Hangul). Format 3 carries none.
|
||||
"""
|
||||
font["post"].formatType = 3.0
|
||||
rename(font, family, style)
|
||||
font.save(path)
|
||||
|
||||
|
||||
def rename(font: TTFont, family: str, style: str) -> None:
|
||||
name = font["name"]
|
||||
postscript = f"{family}-{style}"
|
||||
for record in list(name.names):
|
||||
if record.nameID in (1, 16):
|
||||
record.string = family
|
||||
elif record.nameID in (2, 17):
|
||||
record.string = style
|
||||
elif record.nameID == 3:
|
||||
record.string = f"{postscript};HealthLog"
|
||||
elif record.nameID == 4:
|
||||
record.string = f"{family} {style}"
|
||||
elif record.nameID == 6:
|
||||
record.string = postscript
|
||||
|
||||
|
||||
def build(src: Path, out: Path) -> None:
|
||||
out.mkdir(parents=True, exist_ok=True)
|
||||
math = subset_font(TTFont(src / "NotoSansMath-Regular.ttf"), SYMBOL_CODEPOINTS)
|
||||
kr_source = src / "NotoSansKR[wght].ttf"
|
||||
|
||||
for style in ("Regular", "Bold", "Italic"):
|
||||
base = subset_font(TTFont(src / f"NotoSans-{style}.ttf"), BASE_CODEPOINTS)
|
||||
merge(base, math, "math.")
|
||||
finish(base, "ReportSans", style, out / f"ReportSans-{style}.ttf")
|
||||
|
||||
if style == "Italic":
|
||||
continue
|
||||
weight = 400 if style == "Regular" else 700
|
||||
kr = subset_font(TTFont(kr_source), HANGUL_CODEPOINTS)
|
||||
kr = instancer.instantiateVariableFont(kr, {"wght": weight})
|
||||
with_hangul = TTFont(out / f"ReportSans-{style}.ttf")
|
||||
merge(with_hangul, kr, "kr.")
|
||||
finish(with_hangul, "ReportSansKR", style, out / f"ReportSansKR-{style}.ttf")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) != 3:
|
||||
raise SystemExit(__doc__)
|
||||
build(Path(sys.argv[1]), Path(sys.argv[2]))
|
||||
@@ -0,0 +1,95 @@
|
||||
Copyright 2022 The Noto Project Authors (https://github.com/notofonts/latin-greek-cyrillic)
|
||||
Copyright 2022 The Noto Project Authors (https://github.com/notofonts/math)
|
||||
Copyright 2014-2021 Adobe (http://www.adobe.com/), with Reserved Font Name 'Source'
|
||||
|
||||
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:
|
||||
https://scripts.sil.org/OFL
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Reference in new issue
Block a user