diff --git a/BalooTammudu2-Telugu/vedic-build/.gitignore b/BalooTammudu2-Telugu/vedic-build/.gitignore new file mode 100644 index 0000000..a97d0a9 --- /dev/null +++ b/BalooTammudu2-Telugu/vedic-build/.gitignore @@ -0,0 +1,10 @@ +# Donor font tree — unpacked Noto Sans Devanagari release (~45MB) +donor/ + +# Intermediate TTFs compiled from ../TTX as graft input +intermediate/ + +# Ephemeral artifacts from the GitHub issue-draft step +make_issue_url.py +issue-body.md +issue-prefilled-url.txt diff --git a/BalooTammudu2-Telugu/vedic-build/README.md b/BalooTammudu2-Telugu/vedic-build/README.md new file mode 100644 index 0000000..9683a82 --- /dev/null +++ b/BalooTammudu2-Telugu/vedic-build/README.md @@ -0,0 +1,177 @@ +# Vedic Extensions support for Baloo Tammudu 2 (Telugu) + +Prototype build that adds rendering support for the Unicode **Vedic +Extensions** block (U+1CD0–U+1CFF) plus the four **Devanagari-block Vedic +accents** (U+0951–U+0954) to all five weights of Baloo Tammudu 2. + +This is a **non-authoritative prototype** built on top of the released TTX +artefacts in [`../../TTX/`](../../TTX/). The font's `.vfb` sources in +[`../Regular/VFB/`](../Regular/VFB/) etc. are untouched — for permanent +inclusion the outlines need to land there. A feature request tracking the +broader work has been opened against the upstream +[EkType/Baloo2](https://github.com/EkType/Baloo2/issues) repo. + +## What this produces + +Five patched TTFs (one per weight) with **45 codepoints** of Vedic mark +support: + +| Block | Range | Count | +|---|---|---| +| Devanagari Vedic accents | U+0951–U+0954 | 4 | +| Vedic Extensions | U+1CD0–U+1CFF | 41 | + +Each grafted codepoint is: + +- Mapped in `cmap` (Windows BMP subtable). +- Classified as a combining mark (GDEF class 3) with zero advance width. +- Positioned over Telugu consonants via a new `abvm` GPOS lookup with + mark-to-base anchors on 74 Telugu base glyphs. + +The seven codepoints in U+1CD0–U+1CFF that aren't covered are either too +recent (U+1CF7 from Unicode 12, U+1CFA from Unicode 13) or unassigned +(U+1CFB–U+1CFF). The donor font doesn't have them either. + +## Pipeline + +``` +TTX/BalooTammudu2-{Weight}.ttx + │ + │ fontTools.ttx + ▼ +intermediate/BalooTammudu2-{Weight}.ttf + │ + │ graft_vedic.py + ▼ (donor: NotoSansDevanagari-{Weight}.ttf, OFL) +BalooTammudu2-{Weight}.ttf (cmap + glyf + GDEF wired) + │ + │ add_vedic_gpos.py + ▼ +BalooTammudu2-{Weight}.ttf (+ abvm MarkBasePos lookup on tel2/telu) +``` + +## Scripts in this directory + +| File | Purpose | +|---|---| +| `graft_vedic.py` | Copies Vedic glyph outlines from a donor TTF into a recipient TTF. Flattens composites, weight-matches, adapts glyph geometry: above-marks have their bbox-bottom aligned to Y=600 (Baloo's existing mark band); all marks have their bbox-center shifted to X=-300 to land over a typical Telugu base. Adds cmap entries, GDEF mark classification, sets zero advance. | +| `add_vedic_gpos.py` | Adds a `MarkBasePos` (GPOS lookup type 4) connecting Vedic above-marks to Telugu consonants. Base anchors at consonant `(xmid, ymax + 220)`, mark anchors at `(xmid, ymin)`. Above-base vowel signs (matras U+0C3E–U+0C56) and halant are excluded from the base coverage so the mark attaches to the consonant directly — the matra is allowed to overlap visually but the mark sits over the consonant column. Registers a new `abvm` feature on the `tel2` and `telu` scripts. | + +## Donor font + +[Noto Sans Devanagari v2.006](https://github.com/notofonts/devanagari) (SIL +Open Font License). Chosen because it has 41/48 of the assigned Vedic +Extensions codepoints and weight-matches Baloo Tammudu 2 (Regular through +ExtraBold). Outlines are imported as-is and then transformed; no other +changes to the donor are carried over. + +The attribution is recorded in the top-level +[`OFL.txt`](../../OFL.txt) as required by OFL §4. + +## How to rebuild + +From the repo root: + +```powershell +# 1. Compile Baloo TTX -> TTF (5 weights, ~12s total) +$weights = 'Regular','Medium','SemiBold','Bold','ExtraBold' +foreach ($w in $weights) { + python -m fontTools.ttx -q ` + -o "BalooTammudu2-Telugu/vedic-build/intermediate/BalooTammudu2-$w.ttf" ` + "TTX/BalooTammudu2-$w.ttx" +} + +# 2. Download donor zip (one-time) +[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 +$assets = (Invoke-WebRequest 'https://api.github.com/repos/notofonts/devanagari/releases/latest' ` + -UseBasicParsing -Headers @{'User-Agent'='curl'}).Content | ConvertFrom-Json +Invoke-WebRequest $assets.assets[0].browser_download_url ` + -OutFile 'BalooTammudu2-Telugu/vedic-build/donor/NotoSansDevanagari.zip' +Expand-Archive -Force ` + 'BalooTammudu2-Telugu/vedic-build/donor/NotoSansDevanagari.zip' ` + 'BalooTammudu2-Telugu/vedic-build/donor/' + +# 3. Graft outlines + add GPOS anchors for each weight +foreach ($w in $weights) { + python BalooTammudu2-Telugu/vedic-build/graft_vedic.py ` + --recipient "BalooTammudu2-Telugu/vedic-build/intermediate/BalooTammudu2-$w.ttf" ` + --donor "BalooTammudu2-Telugu/vedic-build/donor/NotoSansDevanagari/full/ttf/NotoSansDevanagari-$w.ttf" ` + --output "BalooTammudu2-Telugu/vedic-build/BalooTammudu2-$w.ttf" + python BalooTammudu2-Telugu/vedic-build/add_vedic_gpos.py ` + --input "BalooTammudu2-Telugu/vedic-build/BalooTammudu2-$w.ttf" ` + --output "BalooTammudu2-Telugu/vedic-build/BalooTammudu2-$w.ttf" +} +``` + +Requires `fontTools` (`pip install fontTools`). + +## Known limitations + +These are inherent to a prototype that grafts outlines without involving +the FontLab `.vfb` sources or the original designers. + +- **Style mismatch.** Noto's stroke contrast and weight don't match Baloo's + heavy, spurless display character. Vedic marks will look thinner and + out-of-family next to native Telugu glyphs in the same line. +- **Approximate vertical clearance.** The static Y-gap of 220 units above + each consonant works for most clusters, but tall above-base matras like + `matraIi` (Y up to 1051) come within ~80 units of the marks. Tweak + `BASE_ANCHOR_Y_GAP` in `add_vedic_gpos.py` if more clearance is wanted. +- **No anchors for below-marks.** Only the `abvm` (above-base) lookup was + added. U+0952 anudatta and the few below-marks in Vedic Extensions still + rely on the static negative-X-baked positioning from the graft step. +- **Coverage gaps.** Seven Vedic Extensions codepoints (U+1CF7, U+1CFA, + U+1CFB–U+1CFF) aren't in the donor and remain unrendered. +- **Sources still untouched.** Patches live in this directory only. The + `.vfb` files need updates from a designer for permanent inclusion. + +## TODO — what a proper fix requires + +This prototype is a working approximation, not the real fix. For +upstreamable Vedic support in Baloo Tammudu 2 the following needs to +happen in FontLab on the `.vfb` sources, then propagate through the +existing build chain (VFB → AFDKO → TTF/TTX): + +- [ ] **Draw native Vedic glyphs** for U+0951–U+0954 and U+1CD0–U+1CFF + in all five weights, matched to Baloo's heavy spurless display + style (stroke contrast, weight, x-height). This is the blocker; + everything else depends on it. +- [ ] **Cover the seven gaps** the donor doesn't have: U+1CF7 (Unicode + 12), U+1CFA (Unicode 13). U+1CFB–U+1CFF are unassigned in Unicode + and should remain unmapped. +- [ ] **Add entries to** `BalooTammudu2-Telugu/GlyphOrderAndAliasDB` + mapping each new glyph name (`uni0951` … `uni1CFA`) to its + codepoint, following the existing naming convention. +- [ ] **Classify in** `BalooTammudu2-Telugu/GDEF` as combining marks + (class 3) with zero advance width. +- [ ] **Real GPOS anchor design.** Replace the `add_vedic_gpos.py` + heuristic (consonants at `ymax + 220`, marks at `ymin`) with + anchors drawn intentionally in FontLab for each base and mark. + Add anchors for **below-marks too** (U+0952 anudatta etc.) — the + prototype only handles above-marks. +- [ ] **Handle the above-base matras properly.** The prototype excludes + them from base coverage so the mark attaches to the consonant + instead. The right solution is either (a) `mkmk` anchors so the + Vedic mark attaches to the matra's top, or (b) contextual GPOS + that raises the mark when a matra is present in the cluster. +- [ ] **Register `abvm` and `blwm` features** on `tel2`/`telu` script + records pointing at the new lookups, following Baloo's existing + feature-table structure rather than the runtime patch this + prototype does. +- [ ] **Visual QA** across a Vedic test corpus (e.g. svaras over + common Telugu consonants, conjuncts, and aksharas with above-base + matras). The static `BASE_ANCHOR_Y_GAP = 220` prototype value is + a compromise and needs designer review. +- [ ] **Remove this `vedic-build/` directory** once the proper sources + ship — it's a temporary workspace, not a permanent build product. + +Tracking issue: (filed as +"Add rendering support for Unicode Vedic Extensions (U+1CD0–U+1CFF)"). + +## Files ignored by `.gitignore` + +`donor/` (~45MB Noto release tree), `intermediate/` (compiled TTX), the +ephemeral issue-draft files (`issue-body.md`, `issue-prefilled-url.txt`, +`make_issue_url.py`), and `*.ttf` (handled by the repo-wide +`.gitignore`). The 5 patched output TTFs are therefore not tracked — +regenerate them with the pipeline above when needed. diff --git a/BalooTammudu2-Telugu/vedic-build/add_vedic_gpos.py b/BalooTammudu2-Telugu/vedic-build/add_vedic_gpos.py new file mode 100644 index 0000000..6f20777 --- /dev/null +++ b/BalooTammudu2-Telugu/vedic-build/add_vedic_gpos.py @@ -0,0 +1,266 @@ +""" +Add GPOS MarkBasePos anchors so Vedic above-marks attach correctly +to Telugu base glyphs (consonants and above-base vowel signs). + +Without this lookup, the grafted Vedic marks rely on static negative-X-baked +geometry, which only positions correctly over a single average-width base. +For complex clusters like నమో॑ (Na+Ma+matraOo+udatta), the mark lands at the +wrong place because its position is computed from the cursor after the last +glyph in the cluster, not from the cluster's visual top-center. + +With GPOS MarkBasePos: +- Each Telugu base glyph (consonant or above-matra) gets a top-center anchor. +- Each Vedic above-mark gets a bottom-center "mark anchor". +- The shaper places the mark so its anchor coincides with the base's anchor. + +A new 'abvm' feature is registered on Telugu scripts (tel2, telu) referencing +the new lookup. +""" +from __future__ import annotations +import argparse +import os +import sys +from fontTools.ttLib import TTFont, newTable +from fontTools.ttLib.tables import otTables as ot + +VEDIC_CODEPOINTS = list(range(0x0951, 0x0955)) + list(range(0x1CD0, 0x1D00)) +ABOVE_THRESHOLD_Y = 500 # mark glyphs with ymax > this are above-marks +# Anchor placement constants +BASE_ANCHOR_Y_GAP = 220 # base anchor this far above the consonant top; + # chosen so the mark clears matraOo (Y max 947) + # which is ~116 taller than a typical consonant (831) +MARK_ANCHOR_OFFSET = 0 # mark anchor sits at the mark's bbox bottom + +# Codepoints of Telugu above-base vowel signs that we do NOT use as bases. +# We want the abvm mark to attach to the consonant directly (not to the matra +# bbox center, which is biased toward the matra's hook rather than the +# consonant body). Marks then sit horizontally over the consonant — what the +# user expects — and the elevated BASE_ANCHOR_Y_GAP keeps them clear of the +# matra geometry above the consonant. +EXCLUDED_BASE_CODEPOINTS = { + 0x0C3E, # matraAa + 0x0C3F, # matraI + 0x0C40, # matraIi + 0x0C46, # matraE + 0x0C47, # matraEe + 0x0C48, # matraAi + 0x0C4A, # matraO + 0x0C4B, # matraOo + 0x0C4C, # matraAu + 0x0C55, # Lengthmark + 0x0C56, # matraAiLengthmark + 0x0C4D, # Halant (virama; not a base for Vedic marks) +} + + +def get_bbox(font, glyph_name): + """Return (xmin, xmax, ymin, ymax) or None for empty/composite glyphs.""" + glyf = font['glyf'] + g = glyf[glyph_name] + if g.numberOfContours <= 0: + return None + coords, _, _ = g.getCoordinates(glyf) + if not coords: + return None + xs = [x for x, _ in coords] + ys = [y for _, y in coords] + return min(xs), max(xs), min(ys), max(ys) + + +def make_anchor(x, y): + a = ot.Anchor() + a.Format = 1 + a.XCoordinate = int(round(x)) + a.YCoordinate = int(round(y)) + return a + + +def make_coverage(glyph_names, font): + """Build a Coverage table with glyphs sorted by glyphID (required by spec).""" + order = font.getGlyphOrder() + glyph_index = {g: i for i, g in enumerate(order)} + cov = ot.Coverage() + cov.glyphs = sorted(glyph_names, key=lambda g: glyph_index[g]) + return cov + + +def build_mark_base_pos(font, base_anchors, mark_anchors): + """Build a single MarkBasePos subtable. + + base_anchors: {glyph_name: (x, y)} + mark_anchors: {glyph_name: (x, y)} -- all in class 0 (above) + """ + sub = ot.MarkBasePos() + sub.Format = 1 + sub.ClassCount = 1 + + order = font.getGlyphOrder() + glyph_index = {g: i for i, g in enumerate(order)} + mark_glyphs = sorted(mark_anchors.keys(), key=lambda g: glyph_index[g]) + base_glyphs = sorted(base_anchors.keys(), key=lambda g: glyph_index[g]) + + sub.MarkCoverage = make_coverage(mark_glyphs, font) + sub.BaseCoverage = make_coverage(base_glyphs, font) + + mark_array = ot.MarkArray() + mark_records = [] + for gn in mark_glyphs: + x, y = mark_anchors[gn] + rec = ot.MarkRecord() + rec.Class = 0 + rec.MarkAnchor = make_anchor(x, y) + mark_records.append(rec) + mark_array.MarkRecord = mark_records + mark_array.MarkCount = len(mark_records) + sub.MarkArray = mark_array + + base_array = ot.BaseArray() + base_records = [] + for gn in base_glyphs: + x, y = base_anchors[gn] + rec = ot.BaseRecord() + rec.BaseAnchor = [make_anchor(x, y)] + base_records.append(rec) + base_array.BaseRecord = base_records + base_array.BaseCount = len(base_records) + sub.BaseArray = base_array + + return sub + + +def add_lookup(gpos_table, subtable): + """Append a new MarkBasePos lookup. Returns the lookup index.""" + lk = ot.Lookup() + lk.LookupType = 4 # MarkBasePos + lk.LookupFlag = 0 + lk.SubTable = [subtable] + lk.SubTableCount = 1 + + gpos_table.LookupList.Lookup.append(lk) + gpos_table.LookupList.LookupCount = len(gpos_table.LookupList.Lookup) + return gpos_table.LookupList.LookupCount - 1 + + +def add_feature_to_scripts(gpos_table, feature_tag, lookup_index, script_tags): + """Create a new Feature pointing at the lookup, then reference its index + from each named script's DefaultLangSys and LangSysRecords.""" + feat = ot.Feature() + feat.LookupListIndex = [lookup_index] + feat.LookupCount = 1 + feat.FeatureParams = None + + fr = ot.FeatureRecord() + fr.FeatureTag = feature_tag + fr.Feature = feat + + gpos_table.FeatureList.FeatureRecord.append(fr) + gpos_table.FeatureList.FeatureCount = len(gpos_table.FeatureList.FeatureRecord) + new_feature_index = gpos_table.FeatureList.FeatureCount - 1 + + for sr in gpos_table.ScriptList.ScriptRecord: + if sr.ScriptTag not in script_tags: + continue + if sr.Script.DefaultLangSys is not None: + sr.Script.DefaultLangSys.FeatureIndex.append(new_feature_index) + sr.Script.DefaultLangSys.FeatureCount = len( + sr.Script.DefaultLangSys.FeatureIndex) + for lsr in sr.Script.LangSysRecord: + lsr.LangSys.FeatureIndex.append(new_feature_index) + lsr.LangSys.FeatureCount = len(lsr.LangSys.FeatureIndex) + + return new_feature_index + + +def collect_telugu_bases(font): + """Return base glyph names: consonants and matras that have non-trivial + above-line extent. Skip below-mark glyphs and pure-spacing glyphs.""" + cmap = font.getBestCmap() + gdef = font['GDEF'].table.GlyphClassDef.classDefs + out = [] + for cp in range(0x0C00, 0x0C80): + if cp in EXCLUDED_BASE_CODEPOINTS: + continue + name = cmap.get(cp) + if not name or not name.endswith('.te'): + continue + if gdef.get(name) == 3: + continue + b = get_bbox(font, name) + if b is None: + continue + if b[3] < 400: + continue + out.append(name) + return out + + +def collect_vedic_above_marks(font): + """Return Vedic mark glyph names whose bbox is in the above-mark region.""" + cmap = font.getBestCmap() + out = [] + for cp in VEDIC_CODEPOINTS: + name = cmap.get(cp) + if not name: + continue + b = get_bbox(font, name) + if b is None: + continue + if b[3] > ABOVE_THRESHOLD_Y: + out.append(name) + return out + + +def add_anchors(input_path, output_path): + font = TTFont(input_path) + + bases = collect_telugu_bases(font) + marks = collect_vedic_above_marks(font) + + # Build base anchors at (xmid, ymax + gap) + base_anchors = {} + for gn in bases: + b = get_bbox(font, gn) + if b is None: + continue + xmin, xmax, _, ymax = b + base_anchors[gn] = ((xmin + xmax) / 2, ymax + BASE_ANCHOR_Y_GAP) + + # Build mark anchors at (xmid, ymin) + mark_anchors = {} + for gn in marks: + b = get_bbox(font, gn) + if b is None: + continue + xmin, xmax, ymin, _ = b + mark_anchors[gn] = ((xmin + xmax) / 2, ymin + MARK_ANCHOR_OFFSET) + + if not base_anchors or not mark_anchors: + raise RuntimeError( + f"Need at least one base and one mark; got " + f"{len(base_anchors)} bases, {len(mark_anchors)} marks") + + gpos = font['GPOS'].table + sub = build_mark_base_pos(font, base_anchors, mark_anchors) + lookup_index = add_lookup(gpos, sub) + add_feature_to_scripts(gpos, 'abvm', lookup_index, {'tel2', 'telu'}) + + os.makedirs(os.path.dirname(output_path) or '.', exist_ok=True) + font.save(output_path) + + return len(base_anchors), len(mark_anchors), lookup_index + + +def main(): + ap = argparse.ArgumentParser() + ap.add_argument("--input", required=True) + ap.add_argument("--output", required=True) + args = ap.parse_args() + + n_bases, n_marks, lk_idx = add_anchors(args.input, args.output) + print(f"Added abvm lookup #{lk_idx} to {args.output}") + print(f" Base glyphs with above-anchor: {n_bases}") + print(f" Mark glyphs with mark-anchor: {n_marks}") + + +if __name__ == "__main__": + sys.exit(main() or 0) diff --git a/BalooTammudu2-Telugu/vedic-build/graft_vedic.py b/BalooTammudu2-Telugu/vedic-build/graft_vedic.py new file mode 100644 index 0000000..cbf9f2e --- /dev/null +++ b/BalooTammudu2-Telugu/vedic-build/graft_vedic.py @@ -0,0 +1,224 @@ +""" +Graft Vedic Extensions (U+1CD0-U+1CFF) outlines from Noto Sans Devanagari +into Baloo Tammudu 2 Telugu. + +The donor outlines are copied as-is into the recipient's glyf table; above-marks +are translated down ~300 units so they sit above Telugu base characters +rather than above a Devanagari headstroke (which Telugu lacks). All marks are +classified in GDEF and given zero advance width. + +Notes & limitations: +- No GPOS anchor work is done. Marks position by their natural Y-offset, which + is approximate. Refining requires per-base anchor tables. +- Telugu has no native Vedic mark-to-base rules; renderers stack via OT shaping. +- Donor names retained internally as uni1CXX per Baloo's naming convention. +""" +from __future__ import annotations +import argparse +import os +import sys +from fontTools.ttLib import TTFont +from fontTools.ttLib.tables._g_l_y_f import Glyph +from fontTools.pens.recordingPen import DecomposingRecordingPen +from fontTools.pens.ttGlyphPen import TTGlyphPen +from fontTools.pens.transformPen import TransformPen +from fontTools.misc.transform import Offset + + +class _GlyfGlyphSet: + """Minimal glyph-set adapter so DecomposingRecordingPen can resolve + component refs against a raw glyf table.""" + def __init__(self, glyf): + self._glyf = glyf + def __contains__(self, name): + return name in self._glyf.glyphs + def __getitem__(self, name): + glyph = self._glyf[name] + return _DrawableGlyph(glyph, self._glyf) + + +class _DrawableGlyph: + def __init__(self, glyph, glyf): + self._glyph = glyph + self._glyf = glyf + def draw(self, pen): + self._glyph.draw(pen, self._glyf) + def drawPoints(self, pen): + self._glyph.drawPoints(pen, self._glyf) + + +def flatten_composite(donor_glyph: Glyph, d_glyf) -> Glyph: + """Return a simple (non-composite) glyph by inlining all component refs.""" + gs = _GlyfGlyphSet(d_glyf) + rec = DecomposingRecordingPen(gs) + donor_glyph.draw(rec, d_glyf) + out_pen = TTGlyphPen(None) + rec.replay(out_pen) + return out_pen.glyph() + +# Codepoints to graft: the Vedic Extensions block plus the four Devanagari-block +# Vedic accent marks (U+0951–U+0954), which are routinely used to mark Vedic +# prosody in Indic scripts including Telugu. +VEDIC_CODEPOINTS = list(range(0x0951, 0x0955)) + list(range(0x1CD0, 0x1D00)) +ABOVE_MARK_THRESHOLD = 500 # Y bbox max above this = above-mark +# Align each above-mark's bbox bottom to this Y. Baloo's Candrabindu_above +# sits at Y[635,865]; Halant at Y[527,919]. Bottom ≈ 600 lands new marks in +# the same band regardless of their individual height. +TARGET_ABOVE_BOTTOM_Y = 600 +# Baloo Tammudu 2 positions zero-advance marks via negative-X-baked geometry. +# Candrabindu_above center_x ≈ -375; Halant center_x ≈ -262. We center each +# imported mark at the midpoint of these so it lands roughly over a typical +# Telugu base (advance ~600). +TARGET_MARK_CENTER_X = -300 + + +def bbox(glyph: Glyph, glyf_table): + """Return (xmin, xmax, ymin, ymax) of a simple glyph, or None if empty.""" + if glyph.numberOfContours <= 0: + return None + coords, _, _ = glyph.getCoordinates(glyf_table) + if not coords: + return None + xs = [x for x, _ in coords] + ys = [y for _, y in coords] + return min(xs), max(xs), min(ys), max(ys) + + +def is_above_mark(glyph: Glyph, glyf_table) -> bool: + b = bbox(glyph, glyf_table) + return b is not None and b[3] > ABOVE_MARK_THRESHOLD + + +def translate_glyph(glyph: Glyph, glyf_table, dx: int, dy: int) -> Glyph: + """Re-emit a (simple, non-composite) glyph with coordinates shifted.""" + if glyph.numberOfContours <= 0: + return glyph + # Use a TTGlyphPen with a TransformPen wrapper for the shift + pen = TTGlyphPen(None) + transform_pen = TransformPen(pen, Offset(dx, dy)) + # Draw via fontTools' ttProgram-friendly path + glyph.draw(transform_pen, glyf_table) + new_glyph = pen.glyph() + return new_glyph + + +def graft(recipient_path: str, donor_path: str, output_path: str) -> dict: + recipient = TTFont(recipient_path) + donor = TTFont(donor_path) + + r_glyf = recipient['glyf'] + d_glyf = donor['glyf'] + r_hmtx = recipient['hmtx'] + d_hmtx = donor['hmtx'] + d_cmap = donor.getBestCmap() + + # Find a 4-3 (Windows Unicode BMP) cmap subtable to extend in recipient + target_cmap_subtables = [] + for sub in recipient['cmap'].tables: + if (sub.platformID, sub.platEncID) in [(3, 1), (3, 10), (0, 3), (0, 4)]: + target_cmap_subtables.append(sub) + if not target_cmap_subtables: + raise RuntimeError("Recipient has no Unicode cmap subtable to extend") + + # Ensure GDEF.GlyphClassDef exists; we'll add new glyphs as mark (class 3) + gdef = recipient.get('GDEF') + if gdef is None or gdef.table.GlyphClassDef is None: + raise RuntimeError("Recipient missing GDEF GlyphClassDef") + gcd = gdef.table.GlyphClassDef + + grafted = [] + skipped = [] + existing = set(recipient.getGlyphOrder()) + + for cp in VEDIC_CODEPOINTS: + if cp not in d_cmap: + continue + new_name = f"uni{cp:04X}" + if new_name in existing: + skipped.append((cp, "already in recipient")) + continue + existing.add(new_name) + + donor_name = d_cmap[cp] + donor_glyph = d_glyf[donor_name] + + # Flatten composite glyphs so we don't carry over component refs + # to donor-only glyph names that won't exist in the recipient. + if donor_glyph.isComposite(): + donor_glyph = flatten_composite(donor_glyph, d_glyf) + + # Compute Y-shift adaptively: above-marks land with bbox-bottom at the + # target Y. Short above-marks shift less, tall above-marks shift more. + # Below-marks and other geometries are left where they are. + b = bbox(donor_glyph, d_glyf) + if b is not None and b[3] > ABOVE_MARK_THRESHOLD: + dy = int(round(TARGET_ABOVE_BOTTOM_Y - b[2])) + else: + dy = 0 + + # Compute X-shift so the mark's geometric center lands at TARGET_MARK_CENTER_X. + # This emulates Baloo's existing negative-X-baked mark convention so + # zero-advance marks visually land over a typical Telugu base. + if b is not None: + xmin, xmax, _, _ = b + current_center_x = (xmin + xmax) / 2 + dx = int(round(TARGET_MARK_CENTER_X - current_center_x)) + else: + dx = 0 + + if dx or dy: + new_glyph = translate_glyph(donor_glyph, d_glyf, dx, dy) + else: + new_glyph = donor_glyph + + # Install the glyph + r_glyf[new_name] = new_glyph + + # Force zero advance; lsb at original bbox left + if new_glyph.numberOfContours > 0: + coords, _, _ = new_glyph.getCoordinates(r_glyf) + xs = [x for x, _ in coords] if coords else [0] + lsb = min(xs) if xs else 0 + else: + lsb = 0 + r_hmtx.metrics[new_name] = (0, lsb) + + # cmap mapping + for sub in target_cmap_subtables: + sub.cmap[cp] = new_name + + # GDEF: classify as mark + gcd.classDefs[new_name] = 3 + + grafted.append((cp, donor_name, new_name, dx, dy)) + + # r_glyf.__setitem__ appends to its internal glyphOrder; sync the font's + # master glyph order list to match so cmap/GDEF/hmtx all resolve names. + recipient.setGlyphOrder(list(r_glyf.glyphOrder)) + + os.makedirs(os.path.dirname(output_path), exist_ok=True) + recipient.save(output_path) + + return {"grafted": grafted, "skipped": skipped} + + +def main(): + ap = argparse.ArgumentParser() + ap.add_argument("--recipient", required=True) + ap.add_argument("--donor", required=True) + ap.add_argument("--output", required=True) + args = ap.parse_args() + + result = graft(args.recipient, args.donor, args.output) + print(f"Grafted {len(result['grafted'])} Vedic codepoints into {args.output}") + above = sum(1 for *_, dy in result['grafted'] if dy) + print(f" Above-marks Y-aligned to bbox-bottom={TARGET_ABOVE_BOTTOM_Y}: {above}") + print(f" Marks centered to X={TARGET_MARK_CENTER_X}: {len(result['grafted'])}") + if result['skipped']: + print(f" Skipped: {len(result['skipped'])}") + for cp, reason in result['skipped']: + print(f" U+{cp:04X}: {reason}") + + +if __name__ == "__main__": + sys.exit(main() or 0) diff --git a/OFL.txt b/OFL.txt index d45b560..6a0cff2 100644 --- a/OFL.txt +++ b/OFL.txt @@ -1,5 +1,13 @@ Copyright (c) 2019 Ek Type (www.ektype.in) +Portions of Baloo Tammudu 2 covering Vedic accent marks - the Unicode Vedic +Extensions block (U+1CD0-U+1CFF) and the Devanagari-block Vedic accents +(U+0951-U+0954) - are derived from Noto Sans Devanagari: +Copyright 2022 The Noto Project Authors (https://github.com/notofonts/devanagari) +Licensed under the SIL Open Font License, Version 1.1. +"Noto" is a trademark of Google Inc. and is not used in any Baloo Tammudu 2 +font name; the trademark is acknowledged here per OFL Section 4. + 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: http://scripts.sil.org/OFL