635 lines
25 KiB
Bash
Executable File
635 lines
25 KiB
Bash
Executable File
#!/usr/bin/env bash
|
||
#
|
||
# fetch-fonts.sh — scarica le 22 famiglie di caratteri incluse in pi-imgen.
|
||
#
|
||
# ---------------------------------------------------------------------------
|
||
# Target shell: bash 3.2 (the one macOS actually ships). No associative arrays,
|
||
# no `mapfile`, no `${var,,}`. BSD sed/grep/awk only: no `grep -P`, no GNU-only
|
||
# flags. Tools required: curl, unzip, awk, sed, od.
|
||
#
|
||
# WHERE THE FILES COME FROM, AND WHY (measured 2026-08-27, not assumed):
|
||
#
|
||
# 1. PRIMARY for the binaries — raw.githubusercontent.com/google/fonts.
|
||
# This is the ONLY source that yields VARIABLE TTFs, which is what we want
|
||
# because Typst 0.15 sets axes (wght/wdth/opsz/SOFT/WONK) at render time.
|
||
# Exact file names are read from METADATA.pb, never guessed: the bracketed
|
||
# variable name is spelled differently per family (Archivo[wdth,wght].ttf
|
||
# but Fraunces[SOFT,WONK,opsz,wght].ttf) and the axis order is not
|
||
# alphabetical by luck. Brackets are percent-encoded for the URL.
|
||
#
|
||
# NOTE — this inverts the source order given in the brief, deliberately.
|
||
# The brief assumed the Fontsource zip ships plain TTFs. It does not:
|
||
# `api.fontsource.org/v1/download/archivo` returns 54 TTFs that are all
|
||
# SUBSET BY UNICODE-RANGE (…-latin-400-normal.ttf, …-latin-ext-…,
|
||
# …-vietnamese-…) plus a `variable/` folder that is WOFF2 ONLY. Bundling
|
||
# those would give Typst a dozen faces sharing one family name and no
|
||
# variable axes at all. So Fontsource stays in the chain as the keyless
|
||
# safety net (tier 2), not as the primary. Set FONT_SOURCE=fontsource (or
|
||
# --source=fontsource) to force the brief's original order.
|
||
#
|
||
# 2. FALLBACK — the Fontsource zip. Keyless, ships a LICENSE, always up.
|
||
# We take its `static/*-latin-*.ttf` slice only: the Google "latin" subset
|
||
# covers the whole audited Italian set (accented vowels, « », ° € – — ‘ ’
|
||
# “ ”), and taking latin-ext as well would install duplicate faces under
|
||
# the same family name. Degraded mode — recorded as such in the manifest
|
||
# and in THIRD-PARTY-FONTS.md.
|
||
#
|
||
# 3. LAST RESORT — the Google css2 endpoint with a NON-BROWSER User-Agent,
|
||
# which hands back full unsubsetted static TTFs keyless. css2 ALWAYS
|
||
# instantiates: it can never return a variable font, so a family that
|
||
# lands here loses its axes.
|
||
#
|
||
# THE THREE RULES THAT PREVENT REAL MISTAKES (each implemented below):
|
||
#
|
||
# RULE 1 — the licence is read from the DIRECTORY, never from the binary.
|
||
# Roboto Condensed's shipped binary still declares Apache-2.0 in name ID 13
|
||
# while the actual grant is OFL. The bucket a family lives in under
|
||
# google/fonts is the grant; see license_id_for_bucket().
|
||
#
|
||
# RULE 2 — the bucket is NEVER hardcoded. Roboto and Open Sans have already
|
||
# migrated out of apache/. resolve_bucket() probes ofl/ apache/ ufl/
|
||
# cc-by-sa/ and uses whichever answers.
|
||
#
|
||
# RULE 3 — a 200 proves a path exists, NOT that it is the family you asked
|
||
# for. There are five distinct "Big Shoulders" families. resolve_bucket()
|
||
# accepts a bucket only when METADATA.pb's `name:` matches the family in
|
||
# fonts.ts EXACTLY. Downloaded bytes are checked for a real sfnt signature
|
||
# too, so an HTML error page can never be saved as a .ttf.
|
||
#
|
||
# The family list is PARSED from extensions/imgen/design/fonts.ts. It is never
|
||
# duplicated here — adding a family there is the only edit needed.
|
||
#
|
||
# Idempotent: a family whose manifest is complete is skipped unless --force.
|
||
#
|
||
# Uso:
|
||
# scripts/fetch-fonts.sh [--force] [--only=<id|famiglia>]
|
||
# [--source=gf|fontsource] [--check] [--help]
|
||
# ---------------------------------------------------------------------------
|
||
|
||
set -euo pipefail
|
||
|
||
SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd)
|
||
ROOT=$(cd "$SCRIPT_DIR/.." && pwd)
|
||
FONTS_TS="$ROOT/extensions/imgen/design/fonts.ts"
|
||
DEST="$ROOT/vendor/fonts"
|
||
REPORT="$ROOT/THIRD-PARTY-FONTS.md"
|
||
|
||
RAW="https://raw.githubusercontent.com/google/fonts/main"
|
||
TREE="https://github.com/google/fonts/tree/main"
|
||
FONTSOURCE="https://api.fontsource.org/v1/download"
|
||
CSS2="https://fonts.googleapis.com/css2"
|
||
|
||
# Non-browser UA: this is what makes css2 return static TTFs instead of woff2.
|
||
UA="pi-imgen/1.0"
|
||
|
||
# RULE 2: the bucket is probed, in this order, never assumed.
|
||
BUCKETS="ofl apache ufl cc-by-sa"
|
||
|
||
FORCE=0
|
||
ONLY=""
|
||
CHECK_ONLY=0
|
||
SOURCE_PREF="${FONT_SOURCE:-gf}"
|
||
|
||
TMP=$(mktemp -d "${TMPDIR:-/tmp}/pi-imgen-fonts.XXXXXX")
|
||
trap 'rm -rf "$TMP"' EXIT INT TERM
|
||
|
||
# --- output -----------------------------------------------------------------
|
||
if [ -t 1 ]; then C_OK=$'\033[32m'; C_WARN=$'\033[33m'; C_ERR=$'\033[31m'; C_DIM=$'\033[2m'; C_OFF=$'\033[0m'
|
||
else C_OK=""; C_WARN=""; C_ERR=""; C_DIM=""; C_OFF=""; fi
|
||
|
||
say() { printf '%s\n' "$*"; }
|
||
ok() { printf '%s✓%s %s\n' "$C_OK" "$C_OFF" "$*"; }
|
||
info() { printf '%s %s%s\n' "$C_DIM" "$*" "$C_OFF"; }
|
||
warn() { printf '%s!%s %s\n' "$C_WARN" "$C_OFF" "$*" >&2; }
|
||
fail() { printf '%s✗%s %s\n' "$C_ERR" "$C_OFF" "$*" >&2; }
|
||
die() { fail "$*"; exit 1; }
|
||
|
||
usage() {
|
||
cat <<'USAGE'
|
||
Scarica le famiglie di caratteri incluse in pi-imgen dentro vendor/fonts/.
|
||
|
||
--force riscarica anche le famiglie già complete
|
||
--only=<id|nome> lavora su una sola famiglia (es. --only=anton)
|
||
--source=gf|fontsource
|
||
sorgente preferita per i binari (predefinita: gf,
|
||
cioè i TTF variabili di google/fonts)
|
||
--check nessuna rete: verifica quanto è già su disco e
|
||
rigenera THIRD-PARTY-FONTS.md
|
||
--help questo messaggio
|
||
|
||
Variabili d'ambiente: FONT_SOURCE=gf|fontsource
|
||
USAGE
|
||
}
|
||
|
||
for arg in "$@"; do
|
||
case "$arg" in
|
||
--force) FORCE=1 ;;
|
||
--check) CHECK_ONLY=1 ;;
|
||
--only=*) ONLY=$(printf '%s' "${arg#--only=}" | tr '[:upper:]' '[:lower:]') ;;
|
||
--source=*) SOURCE_PREF="${arg#--source=}" ;;
|
||
--help|-h) usage; exit 0 ;;
|
||
*) die "Opzione sconosciuta: $arg (usa --help)" ;;
|
||
esac
|
||
done
|
||
|
||
case "$SOURCE_PREF" in
|
||
gf|fontsource) ;;
|
||
*) die "--source deve essere 'gf' oppure 'fontsource', ricevuto '$SOURCE_PREF'." ;;
|
||
esac
|
||
|
||
for bin in curl unzip awk sed od; do
|
||
command -v "$bin" >/dev/null 2>&1 || die "Manca il comando richiesto: $bin"
|
||
done
|
||
[ -f "$FONTS_TS" ] || die "Non trovo l'elenco delle famiglie: $FONTS_TS"
|
||
|
||
# --- helpers ----------------------------------------------------------------
|
||
|
||
# One place for every network read: retries, timeouts, the non-browser UA, and
|
||
# a hard requirement of HTTP 200 + non-empty body. curl leaves a truncated file
|
||
# behind on failure, so we remove it ourselves rather than trust a partial.
|
||
http_get() { # url dest
|
||
local url="$1" dest="$2" code
|
||
code=$(curl -sS -L -A "$UA" \
|
||
--connect-timeout 15 --max-time 300 \
|
||
--retry 3 --retry-delay 2 \
|
||
-o "$dest" -w '%{http_code}' "$url" </dev/null 2>/dev/null) || { rm -f "$dest"; return 1; }
|
||
[ "$code" = "200" ] || { rm -f "$dest"; return 1; }
|
||
[ -s "$dest" ] || { rm -f "$dest"; return 1; }
|
||
return 0
|
||
}
|
||
|
||
# RULE 3, byte level: a 200 can still be an HTML error page. Require a real
|
||
# sfnt signature (0x00010000 | 'true' | 'OTTO' | 'ttcf').
|
||
is_sfnt() { # path
|
||
local sig
|
||
sig=$(od -An -tx1 -N4 "$1" 2>/dev/null | tr -d ' \n')
|
||
case "$sig" in
|
||
00010000|74727565|4f54544f|74746366) return 0 ;;
|
||
*) return 1 ;;
|
||
esac
|
||
}
|
||
|
||
# Variable TTFs live at Family[axes].ttf; the brackets must be percent-encoded
|
||
# or raw.githubusercontent answers 404. Commas are legal in a path, leave them.
|
||
urlenc() { printf '%s' "$1" | sed -e 's/\[/%5B/g' -e 's/\]/%5D/g'; }
|
||
|
||
lower() { printf '%s' "$1" | tr '[:upper:]' '[:lower:]'; }
|
||
|
||
# RULE 1: the grant comes from the directory the family lives in, never from
|
||
# name ID 13 inside the binary (Roboto Condensed still says Apache-2.0 there
|
||
# while the real grant is OFL).
|
||
license_id_for_bucket() {
|
||
case "$1" in
|
||
ofl) printf 'OFL-1.1' ;;
|
||
apache) printf 'Apache-2.0' ;;
|
||
ufl) printf 'UFL-1.0' ;;
|
||
cc-by-sa) printf 'CC-BY-SA-4.0' ;;
|
||
*) printf 'SCONOSCIUTA' ;;
|
||
esac
|
||
}
|
||
|
||
# Candidate licence file names inside the bucket directory, most likely first.
|
||
license_files_for_bucket() {
|
||
case "$1" in
|
||
ofl) printf 'OFL.txt LICENSE.txt LICENSE' ;;
|
||
apache) printf 'LICENSE.txt LICENSE' ;;
|
||
ufl) printf 'UFL.txt LICENSE.txt LICENSE' ;;
|
||
cc-by-sa) printf 'LICENSE.txt LICENSE' ;;
|
||
*) printf 'LICENSE.txt OFL.txt LICENSE' ;;
|
||
esac
|
||
}
|
||
|
||
manifest_get() { # dir key
|
||
[ -f "$1/.fetch-manifest" ] || return 1
|
||
sed -n "s/^$2=//p" "$1/.fetch-manifest" | head -1
|
||
}
|
||
|
||
# --- the family list, parsed from fonts.ts ---------------------------------
|
||
# Emits TSV: family \t id \t slug \t declared-licence \t axes ("-" if static).
|
||
# Records are gathered by brace balancing, so reformatting fonts.ts does not
|
||
# break this. Never hand-duplicate the list.
|
||
parse_fonts_ts() {
|
||
awk '
|
||
function fld(rec, key, re, s) {
|
||
re = "(^|[^A-Za-z_])" key ":[ \t]*\"[^\"]*\""
|
||
if (!match(rec, re)) return ""
|
||
s = substr(rec, RSTART, RLENGTH)
|
||
sub(/^[^"]*"/, "", s)
|
||
sub(/"$/, "", s)
|
||
return s
|
||
}
|
||
function axesof(rec, s, out, t) {
|
||
if (!match(rec, /axes:[ \t]*\{[^}]*\}/)) return "-"
|
||
s = substr(rec, RSTART, RLENGTH)
|
||
sub(/^axes:[ \t]*\{/, "", s)
|
||
sub(/\}$/, "", s)
|
||
out = ""
|
||
while (match(s, /[A-Za-z]+:[ \t]*\[[ \t]*[0-9.]+[ \t]*,[ \t]*[0-9.]+[ \t]*\]/)) {
|
||
t = substr(s, RSTART, RLENGTH)
|
||
s = substr(s, RSTART + RLENGTH)
|
||
gsub(/[ \t\[\]]/, "", t)
|
||
sub(/,/, "-", t)
|
||
out = (out == "" ? t : out "," t)
|
||
}
|
||
return (out == "" ? "-" : out)
|
||
}
|
||
/export const FONTS/ { inarr = 1; next }
|
||
!inarr { next }
|
||
/^\];/ { inarr = 0; next }
|
||
{
|
||
line = $0
|
||
if (rec == "" && line !~ /\{[ \t]*family:/) next
|
||
rec = rec " " line
|
||
depth += gsub(/\{/, "{", line) - gsub(/\}/, "}", line)
|
||
if (depth <= 0) {
|
||
printf "%s\t%s\t%s\t%s\t%s\n", fld(rec, "family"), fld(rec, "id"), \
|
||
fld(rec, "slug"), fld(rec, "license"), axesof(rec)
|
||
rec = ""; depth = 0
|
||
}
|
||
}
|
||
' "$FONTS_TS"
|
||
}
|
||
|
||
LIST="$TMP/families.tsv"
|
||
parse_fonts_ts > "$LIST"
|
||
TOTAL=$(wc -l < "$LIST" | tr -d ' ')
|
||
# Sanity check the parse against a dumb count of the entries in the array.
|
||
DECLARED=$(awk '/export const FONTS/{i=1;next} /^\];/{i=0} i && /\{[ \t]*family:/{n++} END{print n+0}' "$FONTS_TS")
|
||
[ "$TOTAL" -gt 0 ] || die "Nessuna famiglia estratta da $FONTS_TS — il parser va aggiornato."
|
||
[ "$TOTAL" = "$DECLARED" ] || die "Parser incoerente: $TOTAL famiglie lette, $DECLARED dichiarate in fonts.ts."
|
||
awk -F'\t' '$1=="" || $2=="" || $3=="" {exit 1}' "$LIST" || die "Una voce di fonts.ts non ha family/id/slug."
|
||
|
||
say "pi-imgen — caratteri: $TOTAL famiglie da $FONTS_TS"
|
||
[ "$CHECK_ONLY" = 1 ] && say "Modalità --check: nessun download."
|
||
say ""
|
||
|
||
mkdir -p "$DEST"
|
||
|
||
# --- per-source fetchers ----------------------------------------------------
|
||
# Each prints the names of the files it installed into $stage, one per line, on
|
||
# fd 3 (stdout is reserved for progress), and returns non-zero if it got nothing.
|
||
|
||
# Tier 1 — google/fonts raw. Variable TTFs, full character set, exact names
|
||
# taken from METADATA.pb.
|
||
fetch_gf() { # stage meta bucket slug
|
||
local stage="$1" meta="$2" bucket="$3" slug="$4"
|
||
local got=0 f url
|
||
|
||
# Names straight from METADATA.pb: never reconstructed from the family name,
|
||
# never guessed at the axis order.
|
||
for f in $(sed -n 's/^[ ]*filename:[ ]*"\(.*\)"/\1/p' "$meta" | sort -u); do
|
||
url="$RAW/$bucket/$slug/$(urlenc "$f")"
|
||
if http_get "$url" "$stage/$f"; then
|
||
if is_sfnt "$stage/$f"; then
|
||
got=$((got + 1))
|
||
printf '%s\n' "$f" >&3
|
||
else
|
||
# 200 but not a font: an error page, or the path moved.
|
||
rm -f "$stage/$f"
|
||
warn " $slug: $f non è un font valido, scartato."
|
||
fi
|
||
fi
|
||
done
|
||
[ "$got" -gt 0 ]
|
||
}
|
||
|
||
# Tier 2 — the Fontsource zip. Keyless and always ships a LICENSE, but its TTFs
|
||
# are unicode-range subsets and its variable folder is woff2 only. We keep the
|
||
# `latin` subset, which covers the whole audited Italian character set.
|
||
fetch_fontsource() { # stage id
|
||
local stage="$1" id="$2" zip="$stage/.fontsource.zip" n
|
||
http_get "$FONTSOURCE/$id" "$zip" || return 1
|
||
unzip -o -q -j "$zip" 'static/*-latin-[0-9]*.ttf' -d "$stage" 2>/dev/null || true
|
||
# LICENSE from the zip is only a fallback for the bucket copy (RULE 1).
|
||
unzip -o -q -j "$zip" 'LICENSE' -d "$stage" 2>/dev/null || true
|
||
[ -f "$stage/LICENSE" ] && mv -f "$stage/LICENSE" "$stage/.fontsource-LICENSE"
|
||
rm -f "$zip"
|
||
n=0
|
||
for f in "$stage"/*.ttf; do
|
||
[ -e "$f" ] || continue
|
||
if is_sfnt "$f"; then n=$((n + 1)); printf '%s\n' "$(basename "$f")" >&3
|
||
else rm -f "$f"; fi
|
||
done
|
||
[ "$n" -gt 0 ]
|
||
}
|
||
|
||
# css2 is strict about the weight list: it MUST be ascending and duplicate-free,
|
||
# and every value must sit inside the family's own wght range — otherwise the
|
||
# endpoint answers 400 with an HTML page instead of CSS. Verified 2026-08-27:
|
||
# `Oswald:wght@200;400;700;700` -> 400, `…@200;400;700` -> 200.
|
||
css2_weights() { # min max
|
||
local lo="$1" hi="$2" v
|
||
{
|
||
printf '%s\n' "$lo" "$hi"
|
||
for v in 400 700; do
|
||
if [ "$v" -lt "$lo" ]; then printf '%s\n' "$lo"
|
||
elif [ "$v" -gt "$hi" ]; then printf '%s\n' "$hi"
|
||
else printf '%s\n' "$v"; fi
|
||
done
|
||
} | sort -n -u | tr '\n' ';' | sed 's/;$//'
|
||
}
|
||
|
||
# Tier 3 — css2 with a non-browser UA. Full unsubsetted statics, but css2 always
|
||
# instantiates: a family that lands here has NO variable axes.
|
||
fetch_css2() { # stage family axes
|
||
local stage="$1" family="$2" axes="$3" q wmin wmax n=0 line w url
|
||
q=$(printf '%s' "$family" | sed 's/ /+/g')
|
||
case "$axes" in
|
||
*wght:*)
|
||
wmin=$(printf '%s' "$axes" | sed -n 's/.*wght:\([0-9.]*\)-.*/\1/p')
|
||
wmax=$(printf '%s' "$axes" | sed -n 's/.*wght:[0-9.]*-\([0-9.]*\).*/\1/p')
|
||
q="$q:wght@$(css2_weights "${wmin%.*}" "${wmax%.*}")" ;;
|
||
esac
|
||
http_get "$CSS2?family=$q" "$stage/.css2.css" || return 1
|
||
# Pair each @font-face's weight/style with its .ttf url.
|
||
awk '
|
||
/font-style:/ { st=$2; sub(/;/,"",st) }
|
||
/font-weight:/ { w=$2; sub(/;/,"",w) }
|
||
/url\(/ { if (match($0, /https:[^)]*\.ttf/)) print w "-" st " " substr($0, RSTART, RLENGTH) }
|
||
' "$stage/.css2.css" | sort -u > "$stage/.css2.list"
|
||
rm -f "$stage/.css2.css"
|
||
while read -r line; do
|
||
[ -n "$line" ] || continue
|
||
w=${line%% *}; url=${line##* }
|
||
local out
|
||
out=$(printf '%s' "$family" | tr -d ' ')"-$w.ttf"
|
||
if http_get "$url" "$stage/$out" && is_sfnt "$stage/$out"; then
|
||
n=$((n + 1)); printf '%s\n' "$out" >&3
|
||
else
|
||
rm -f "$stage/$out"
|
||
fi
|
||
done < "$stage/.css2.list"
|
||
rm -f "$stage/.css2.list"
|
||
[ "$n" -gt 0 ]
|
||
}
|
||
|
||
# --- bucket resolution ------------------------------------------------------
|
||
# RULE 2 (probe, never hardcode) + RULE 3 (a 200 is not proof of identity).
|
||
resolve_bucket() { # family slug meta-out -> echoes bucket
|
||
local family="$1" slug="$2" meta="$3" b name
|
||
for b in $BUCKETS; do
|
||
http_get "$RAW/$b/$slug/METADATA.pb" "$meta" || continue
|
||
name=$(awk -F'"' '/^name:/ { print $2; exit }' "$meta")
|
||
if [ "$name" = "$family" ]; then
|
||
printf '%s' "$b"
|
||
return 0
|
||
fi
|
||
# e.g. bigshoulders vs the four other "Big Shoulders …" families.
|
||
warn " $slug: $b/ esiste ma contiene «$name», non «$family» — ignorato."
|
||
rm -f "$meta"
|
||
done
|
||
return 1
|
||
}
|
||
|
||
# Cross-check the axes declared in fonts.ts against the ones the upstream
|
||
# variable font actually has. Typst asks for these at render time, so a stale
|
||
# range in fonts.ts would silently render at a clamped weight.
|
||
check_axes() { # meta declared slug
|
||
local meta="$1" declared="$2" slug="$3" upstream a tag lo hi
|
||
[ "$declared" = "-" ] && { printf '%s' "-"; return 0; }
|
||
upstream=$(awk '
|
||
/^axes \{/ { inax=1; tag=""; lo=""; hi=""; next }
|
||
inax && /tag:/ { t=$2; gsub(/"/,"",t); tag=t }
|
||
inax && /min_value:/ { lo=$2; sub(/\.0$/,"",lo) }
|
||
inax && /max_value:/ { hi=$2; sub(/\.0$/,"",hi) }
|
||
inax && /^\}/ { inax=0; printf "%s%s:%s-%s", (n++ ? "," : ""), tag, lo, hi }
|
||
END { print "" }
|
||
' "$meta")
|
||
[ -z "$upstream" ] && upstream="-"
|
||
for a in $(printf '%s' "$declared" | tr ',' ' '); do
|
||
tag=${a%%:*}
|
||
case ",$upstream," in
|
||
*",$a,"*) ;;
|
||
*) warn " $slug: asse $a dichiarato in fonts.ts, upstream ha «$upstream» — controlla." ;;
|
||
esac
|
||
done
|
||
printf '%s' "$upstream"
|
||
}
|
||
|
||
# --- main loop --------------------------------------------------------------
|
||
FAILED=""
|
||
COUNT_OK=0; COUNT_SKIP=0; COUNT_FAIL=0
|
||
|
||
while IFS=$(printf '\t') read -r family id slug declic axes; do
|
||
[ -n "$family" ] || continue
|
||
if [ -n "$ONLY" ] && [ "$(lower "$id")" != "$ONLY" ] && [ "$(lower "$family")" != "$ONLY" ]; then
|
||
continue
|
||
fi
|
||
|
||
dir="$DEST/$id"
|
||
|
||
# Idempotency: complete family + no --force -> no network at all.
|
||
if [ "$FORCE" = 0 ] || [ "$CHECK_ONLY" = 1 ]; then
|
||
if [ -s "$dir/LICENSE" ] && [ -n "$(manifest_get "$dir" license || true)" ] && \
|
||
ls "$dir"/*.ttf >/dev/null 2>&1; then
|
||
info "$family — già presente, salto."
|
||
COUNT_SKIP=$((COUNT_SKIP + 1))
|
||
continue
|
||
fi
|
||
fi
|
||
if [ "$CHECK_ONLY" = 1 ]; then
|
||
fail "$family — incompleta o assente (modalità --check, non scarico)."
|
||
FAILED="$FAILED $family"
|
||
COUNT_FAIL=$((COUNT_FAIL + 1))
|
||
continue
|
||
fi
|
||
|
||
say "$family ($id)"
|
||
stage="$TMP/stage-$id"
|
||
rm -rf "$stage"; mkdir -p "$stage"
|
||
meta="$stage/METADATA.pb"
|
||
filelist="$stage/.files"
|
||
: > "$filelist"
|
||
|
||
bucket=""
|
||
if bucket=$(resolve_bucket "$family" "$slug" "$meta"); then
|
||
info "bucket risolto: $bucket/ (licenza dalla directory: $(license_id_for_bucket "$bucket"))"
|
||
else
|
||
bucket=""
|
||
warn " $family: nessun bucket di google/fonts corrisponde a «$family» (slug $slug)."
|
||
fi
|
||
|
||
# --- binaries, in the configured order -----------------------------------
|
||
source_used=""
|
||
if [ "$SOURCE_PREF" = "gf" ] && [ -n "$bucket" ]; then
|
||
if fetch_gf "$stage" "$meta" "$bucket" "$slug" 3>>"$filelist"; then source_used="google-fonts-raw"; fi
|
||
fi
|
||
if [ -z "$source_used" ]; then
|
||
if fetch_fontsource "$stage" "$id" 3>>"$filelist"; then source_used="fontsource-latin-subset"; fi
|
||
fi
|
||
if [ -z "$source_used" ] && [ "$SOURCE_PREF" = "fontsource" ] && [ -n "$bucket" ]; then
|
||
if fetch_gf "$stage" "$meta" "$bucket" "$slug" 3>>"$filelist"; then source_used="google-fonts-raw"; fi
|
||
fi
|
||
if [ -z "$source_used" ]; then
|
||
if fetch_css2 "$stage" "$family" "$axes" 3>>"$filelist"; then source_used="google-css2-static"; fi
|
||
fi
|
||
|
||
if [ -z "$source_used" ]; then
|
||
fail "$family: nessuna sorgente ha fornito un TTF valido."
|
||
FAILED="$FAILED $family"
|
||
COUNT_FAIL=$((COUNT_FAIL + 1))
|
||
continue
|
||
fi
|
||
|
||
# --- licence: from the DIRECTORY first (RULE 1) --------------------------
|
||
license_url=""
|
||
if [ -n "$bucket" ]; then
|
||
for lf in $(license_files_for_bucket "$bucket"); do
|
||
if http_get "$RAW/$bucket/$slug/$lf" "$stage/LICENSE"; then
|
||
license_url="$RAW/$bucket/$slug/$lf"
|
||
break
|
||
fi
|
||
done
|
||
fi
|
||
if [ ! -s "$stage/LICENSE" ] && [ -s "$stage/.fontsource-LICENSE" ]; then
|
||
mv -f "$stage/.fontsource-LICENSE" "$stage/LICENSE"
|
||
license_url="$FONTSOURCE/$id (LICENSE nello zip Fontsource)"
|
||
warn " $family: licenza presa dallo zip Fontsource, non dalla directory upstream."
|
||
fi
|
||
rm -f "$stage/.fontsource-LICENSE"
|
||
|
||
if [ ! -s "$stage/LICENSE" ]; then
|
||
fail "$family: nessun file LICENSE trovato — la famiglia NON viene installata."
|
||
FAILED="$FAILED $family"
|
||
COUNT_FAIL=$((COUNT_FAIL + 1))
|
||
continue
|
||
fi
|
||
|
||
license_id=$(license_id_for_bucket "${bucket:-ofl}")
|
||
[ -z "$bucket" ] && license_id="$declic(dichiarata)"
|
||
if [ -n "$bucket" ] && [ "$license_id" != "$declic" ]; then
|
||
warn " $family: fonts.ts dichiara $declic, la directory upstream dice $license_id."
|
||
fi
|
||
|
||
axes_upstream="-"
|
||
[ -f "$meta" ] && axes_upstream=$(check_axes "$meta" "$axes" "$slug")
|
||
|
||
variable="no"
|
||
grep -q '\[' "$filelist" 2>/dev/null && variable="sì"
|
||
|
||
# --- install atomically ---------------------------------------------------
|
||
files_csv=$(tr '\n' ';' < "$filelist" | sed 's/;$//')
|
||
nfiles=$(grep -c . "$filelist" || true)
|
||
{
|
||
printf 'family=%s\n' "$family"
|
||
printf 'id=%s\n' "$id"
|
||
printf 'slug=%s\n' "$slug"
|
||
printf 'bucket=%s\n' "${bucket:-sconosciuto}"
|
||
printf 'license=%s\n' "$license_id"
|
||
printf 'license_declared=%s\n' "$declic"
|
||
printf 'license_url=%s\n' "$license_url"
|
||
printf 'upstream=%s\n' "${bucket:+$TREE/$bucket/$slug}"
|
||
printf 'source=%s\n' "$source_used"
|
||
printf 'variable=%s\n' "$variable"
|
||
printf 'axes_declared=%s\n' "$axes"
|
||
printf 'axes_upstream=%s\n' "$axes_upstream"
|
||
printf 'files=%s\n' "$files_csv"
|
||
printf 'fetched=%s\n' "$(date -u '+%Y-%m-%dT%H:%M:%SZ')"
|
||
} > "$stage/.fetch-manifest"
|
||
rm -f "$filelist"
|
||
|
||
rm -rf "$dir"
|
||
mkdir -p "$(dirname "$dir")"
|
||
mv "$stage" "$dir"
|
||
|
||
ok "$family — $nfiles file, $license_id, sorgente: $source_used, variabile: $variable"
|
||
COUNT_OK=$((COUNT_OK + 1))
|
||
done < "$LIST"
|
||
|
||
# --- report -----------------------------------------------------------------
|
||
# Rebuilt from the manifests on disk, so it always describes what is actually
|
||
# installed — including families skipped as already-present in this run.
|
||
build_report() {
|
||
{
|
||
printf '# Caratteri di terze parti inclusi in pi-imgen\n\n'
|
||
printf 'Generato da `scripts/fetch-fonts.sh` il %s. Non modificare a mano.\n\n' "$(date -u '+%Y-%m-%d %H:%M UTC')"
|
||
printf 'I file dei caratteri stanno in `vendor/fonts/<id>/` e **non** sono versionati.\n'
|
||
printf 'Ogni cartella contiene il file `LICENSE` originale.\n\n'
|
||
printf 'La licenza è letta dalla **directory upstream** in `google/fonts` (il bucket\n'
|
||
printf '`ofl/`, `apache/`, `ufl/`, `cc-by-sa/`), mai dai metadati dentro il binario:\n'
|
||
printf 'per esempio il binario di Roboto Condensed dichiara ancora Apache-2.0 nel name\n'
|
||
printf 'ID 13 mentre la concessione reale è OFL. Il bucket viene risolto a ogni\n'
|
||
printf 'esecuzione, mai scritto a mano, perché le famiglie migrano fra bucket.\n\n'
|
||
printf '| Famiglia | Licenza | Origine upstream | File | Variabile | Sorgente |\n'
|
||
printf '|---|---|---|---|---|---|\n'
|
||
|
||
while IFS=$(printf '\t') read -r family id slug declic axes; do
|
||
[ -n "$family" ] || continue
|
||
d="$DEST/$id"
|
||
if [ ! -f "$d/.fetch-manifest" ]; then
|
||
printf '| %s | %s (dichiarata) | — | **non scaricata** | — | — |\n' "$family" "$declic"
|
||
continue
|
||
fi
|
||
lic=$(manifest_get "$d" license)
|
||
up=$(manifest_get "$d" upstream)
|
||
lurl=$(manifest_get "$d" license_url)
|
||
src=$(manifest_get "$d" source)
|
||
var=$(manifest_get "$d" variable)
|
||
nf=$(ls "$d"/*.ttf 2>/dev/null | wc -l | tr -d ' ')
|
||
[ -n "$up" ] || up="—"
|
||
case "$src" in
|
||
google-fonts-raw) srclabel='google/fonts (TTF completi)' ;;
|
||
fontsource-latin-subset) srclabel='Fontsource (**subset `latin`**, niente assi)' ;;
|
||
google-css2-static) srclabel='css2 (**statici**, niente assi)' ;;
|
||
*) srclabel="$src" ;;
|
||
esac
|
||
printf '| %s | [%s](%s) | [%s](%s) | %s | %s | %s |\n' \
|
||
"$family" "$lic" "${lurl:-$up}" "${up##*/main/}" "$up" "$nf" "$var" "$srclabel"
|
||
done < "$LIST"
|
||
|
||
printf '\n## Note\n\n'
|
||
printf -- '- **OFL-1.1**: ridistribuzione libera, anche commerciale, purché i caratteri\n'
|
||
printf -- ' restino accompagnati dalla licenza e non vengano venduti da soli. Le famiglie\n'
|
||
printf -- ' con Reserved Font Name (Playfair Display, DM Serif Display, Alfa Slab One)\n'
|
||
printf -- ' vanno incluse **non modificate**: rinominare il file non basta, va rinominato\n'
|
||
printf -- ' il font se lo si altera.\n'
|
||
printf -- '- **Apache-2.0**: nessun obbligo di attribuzione nel prodotto finale, ma il file\n'
|
||
printf -- ' `LICENSE` va conservato accanto ai binari.\n'
|
||
printf -- '- I TTF variabili arrivano solo da `raw.githubusercontent.com/google/fonts`.\n'
|
||
printf -- ' L'"'"'endpoint css2 istanzia sempre e non può restituire un font variabile;\n'
|
||
printf -- ' lo zip di Fontsource contiene TTF statici divisi per unicode-range e i\n'
|
||
printf -- ' variabili solo in WOFF2, inutili per Typst.\n'
|
||
printf -- '- Rigenera questo file con `scripts/fetch-fonts.sh --check`.\n'
|
||
} > "$REPORT"
|
||
}
|
||
|
||
build_report
|
||
ok "Rigenerato $REPORT"
|
||
|
||
# --- final verification: no family may end up without a LICENSE -------------
|
||
MISSING=""
|
||
while IFS=$(printf '\t') read -r family id slug declic axes; do
|
||
[ -n "$family" ] || continue
|
||
if [ -n "$ONLY" ] && [ "$(lower "$id")" != "$ONLY" ] && [ "$(lower "$family")" != "$ONLY" ]; then
|
||
continue
|
||
fi
|
||
d="$DEST/$id"
|
||
if [ ! -s "$d/LICENSE" ]; then
|
||
MISSING="$MISSING $family(LICENSE)"
|
||
elif ! ls "$d"/*.ttf >/dev/null 2>&1; then
|
||
MISSING="$MISSING $family(TTF)"
|
||
fi
|
||
done < "$LIST"
|
||
|
||
if [ -n "$ONLY" ] && [ $((COUNT_OK + COUNT_SKIP + COUNT_FAIL)) -eq 0 ]; then
|
||
die "--only=$ONLY non corrisponde a nessuna famiglia di fonts.ts."
|
||
fi
|
||
|
||
say ""
|
||
say "Scaricate: $COUNT_OK · già presenti: $COUNT_SKIP · fallite: $COUNT_FAIL"
|
||
|
||
if [ -n "$MISSING" ] || [ -n "$FAILED" ]; then
|
||
say ""
|
||
if [ -n "$FAILED" ]; then
|
||
fail "Non scaricate:$FAILED"
|
||
fi
|
||
if [ -n "$MISSING" ]; then
|
||
fail "Incomplete su disco:$MISSING"
|
||
fail "Una famiglia senza LICENSE non è ridistribuibile: non pubblicare questa build."
|
||
fi
|
||
fail "Riprova con: scripts/fetch-fonts.sh --force --only=<id>"
|
||
exit 1
|
||
fi
|
||
|
||
ok "Tutte le famiglie richieste hanno TTF e LICENSE."
|