@hb61/thakar-gujarati-font-converter
v2.2.1
Published
Convert Gujarati text between Unicode and legacy typing-font encodings (KAP113 and more), by font name. Works in Node.js and in browser/React bundlers.
Maintainers
Readme
@hb61/thakar-gujarati-font-converter
Convert Gujarati text between Unicode and legacy Gujarati typing-font encodings (KAP113 today, more fonts to follow). Pick the font by name and pass your string through — the package handles the rest. Works in Node.js and in browser bundlers (React, Vite, Next.js, CRA, webpack, ...) with no extra config.
Install
npm install @hb61/thakar-gujarati-font-converterUsage
toFont and toUnicode always return a plain JavaScript string — never
a Promise, a Buffer, or a mutated argument — so you can assign it straight
to a variable, render it, or store it.
Node.js (CommonJS)
const { toFont, toUnicode, listFonts } = require('@hb61/thakar-gujarati-font-converter');
listFonts();
// -> [{ name: 'kap113', label: 'KAP113', mapped: true, fontFile: 'kap113_0.ttf' }, ...]
const kapText = toFont('kap113', 'હાર્દિક'); // Unicode -> font key sequence (string)
const unicodeText = toUnicode('kap113', 'CFlN"S'); // font key sequence -> Unicode (string)Node.js (ESM) / React / Vite / Next.js / webpack
import { toFont, toUnicode, listFonts } from '@hb61/thakar-gujarati-font-converter';
function LabelInKap113({ text }) {
const kapText = toFont('kap113', text); // plain string, safe to render directly
return <span style={{ fontFamily: 'KAP113' }}>{kapText}</span>;
}Both entry points (require and import) resolve to the same
conversion logic — pick whichever matches your project's module system.
The core library has no Node built-in dependencies (no fs, no path),
so it bundles cleanly for the browser.
Font names are case-insensitive ('kap113', 'KAP113' resolve the same).
Passing an unregistered font name throws immediately with the list of
available fonts, rather than silently returning garbage.
Using a KAP font as a CSS font-family in the browser
Every font's .ttf is published as part of the package and can be
imported directly through your bundler via a subpath import, then wired
up with a normal @font-face rule. Use the font's label (from
listFonts()) as the font-family name so it stays consistent with the
package's own naming.
// React / Vite / webpack / CRA — bundler resolves the .ttf to a URL
import kap113Ttf from '@hb61/thakar-gujarati-font-converter/fonts/kap113_0.ttf';/* app.css */
@font-face {
font-family: 'KAP113';
src: url('@hb61/thakar-gujarati-font-converter/fonts/kap113_0.ttf') format('truetype');
}
.kap113-text {
font-family: 'KAP113', sans-serif;
}import { toFont } from '@hb61/thakar-gujarati-font-converter';
import './app.css';
function Card() {
const kapText = toFont('kap113', 'હાર્દિક'); // string, ready to render
return <p className="kap113-text">{kapText}</p>;
}Any registered font's file (mapped or not) is reachable the same way,
e.g. @hb61/thakar-gujarati-font-converter/fonts/kap050_0.ttf — check
listFonts() for each font's exact fontFile name.
Node/server-side (no bundler)
For server-rendered @font-faces or copying the file at build time, use
the Node-only helper (not exported from the main entry, since it uses
node:path):
const { getFontFilePath } = require('@hb61/thakar-gujarati-font-converter/src/node-fonts');
getFontFilePath('kap113'); // -> absolute path to kap113_0.ttf on diskAbout mapped: false fonts
src/fonts/ currently holds ~156 .ttf files (kap001_0.ttf through
kap160_0.ttf, plus KAP113). Only KAP113 has a real, working
Unicode↔font mapping right now (src/mappings/kap113.js) — building that
table required real bilingual examples and a lot of verification (see
"Open items" below), and it still has gaps.
Every other font is registered so listFonts() reflects what's physically
on disk, but calling toFont/toUnicode with one of them throws a clear
error (... is registered but has no mapping data yet) instead of
returning garbage. See "Adding a new font" below for the process to
promote one from placeholder to working.
⚠️ Open items
land"were changed without verification (2026-07-16): on direct user instruction (l: ર્ reph → િ;": િ → ૅ candra-E) without a fresh bilingual example to confirm them — repeated requests for one went unanswered. If output involving િ, ૅ, or ર્ looks wrong on real data, this is the first place to check.Two unresolved conflicts from a vowel-list test: that same test implied
Zmight be ૨ (digit 2) instead of its current DERIVED value ર (RA), andomight be ઃ (visarga) instead of its current CONFIRMED value સ્ (SA+virama). Both were left as-is (Z=ર, o=સ્) rather than guessed at, since the clarifying answers given were too ambiguous to act on safely - "Z used for both" isn't possible for a single static byte in this font, and doesn't resolve which is right.Resolved, noted for history: the vowel-list test's implication that unshifted
5→ ૫ (Gujarati digit five) turned out to be wrong - a follow-up test of the full unshifted digit row (1234567890 → ક્ષદ્રઘલપણજ્ઞટઠડ) explicitly overrode it. The unshifted digit row is consonants/conjuncts, not digits - Gujarati digits live on Shift+0-9 (DERIVED_ENTRIES) as originally expected.New territory discovered: a conjunct-list test surfaced 5 keys in the 128-159 byte range (Alt+0130-0139-ish), which had never been tested at all before - all prior extended-range work only covered 160-254. The rest of 128-159 is still completely unexplored and may contain more glyphs worth a dedicated pass.
Q corrected: was ટ (TTA, READ tier), now ષ (SSA) per direct confirmation. ટ itself stays fully covered via digit key
8(from the digit-row batch), so no coverage was lost.Resolved: ૌ (dependent AU matra) was failing - visible directly in a rendered screenshot. Fixed by adding it as a compose entry (
F{, the same "au" suffix confirmed via ઔ=V+F+{), which works after any consonant since compose-matching is position-independent.Still unresolved: a conjunct example (ક્સિ →
˜S;) hasn't been cleanly parsed -Sand;already account for ક and સ, leaving˜(byte 0x98) needing to cover both the connecting virama and the િ matra, and it's unclear whether it's one or the other or both. Still fails visibly (confirmed via screenshot) since˜remains entirely unmapped. Needs a cleaner example to resolve rather than a guess - e.g. testing ક્સ alone (without the િ) would isolate what˜does.Pre-existing data conflict, found during npm packaging (2026-07-16):
=(0x3D) has exactly one entry inCONFIRMED_ENTRIES, mapping it to^(added as a later "correction" batch), but an earlier documented assumption said=was a true no-op.test/convert.test.jsstill asserts the no-op behavior, sonpm testcurrently fails on this one line. This is a data disagreement, not a bug in the conversion engine — left unresolved rather than guessed, same policy as the other open items here. Needs a real bilingual example containing=to settle it.
See the "OPEN ITEM"/"KNOWN GAP" comments in src/mappings/kap113.js for
the full detail on both. Everything else in this table is verified the
way it says below.
Status: two confidence tiers active, ~190-glyph table still incomplete
KAP113 is a "typing font" — every Gujarati glyph sits on a plain ASCII key code (33–126, plus a conjunct/matra-combo range at 160–255). There's no public specification for this mapping, so it's built from two sources:
- CONFIRMED — verified against real bilingual examples (the same text typed in both Unicode and KAP113, from actual ERP records). Ground truth, no ambiguity — except the open items above.
- DERIVED — read from a Terafont-Varun keyboard layout chart (a
different font product), on the theory that legacy Gujarati "typing
fonts" share one standardized keyboard convention. This isn't a blind
guess: 5 of the 6 original CONFIRMED entries independently matched
this chart exactly, a 5/6 hit rate that's real evidence. DERIVED
entries are scoped strictly to patterns with a validated anchor — see
src/mappings/kap113.jsfor exactly what's included and why. - COMPOSED — some independent vowels (આ, એ, ઐ, ઓ, ઔ, ઑ, અં) aren't
on their own KAP113 key at all; they're visually built by typing
multiple existing keys together (e.g. આ = V + F). Confirmed via a
real Unicode→KAP113 conversion. See
applyPositionalRuleToTokensand the compose-lookup logic insrc/engine.jsfor how this is handled architecturally differently from the single-byte entries.
Currently active: 144 confirmed + 26 derived + 9 read + 8 composed = 187 total.
| KAP key | Unicode | Character | Tier |
|---|---|---|---|
| C | U+0AB9 | હ (HA) | confirmed |
| F | U+0ABE | ા (AA matra) | confirmed |
| l | U+0ABF | િ (I matra) | unverified, see warning above |
| N | U+0AA6 | દ (DA) | confirmed |
| " | U+0AC5 | ૅ (candra E matra) | unverified, see warning above |
| S | U+0A95 | ક (KA) | confirmed |
| ¨ | U+0AB0 U+0ACD | ર્ (RA+virama, reph) | confirmed (extended range) |
| A D G H J K L | — | બ મ ન જ વ છ ી | derived |
| Z X V B M | — | ર શ અ ખ ો | derived |
| !@#$%^&*() | — | ૧–૦ (digits) | derived |
Still unmapped (needs real confirmation, not a chart guess): the Tab row
(Q–P), unshifted/lowercase key states, and the Alt+0161–254 extended
conjunct range. Don't point this at production ERP data without
spot-checking the DERIVED tier against at least one more real example —
see src/mappings/kap113.js for the full rationale and exact scope.
Adding a new font
The package is built as a registry of fonts, not a single hardcoded mapping, so adding a font doesn't touch the conversion engine at all:
- Drop the font file at
src/fonts/<name>.ttf(e.g.src/fonts/kap1.ttf). - Build its mapping module at
src/mappings/<name>.js, same shape assrc/mappings/kap113.js(KAP_TO_UNICODE,UNICODE_TO_KAP,COMPOSED_*tables,name/label). See "How to expand the mapping table" below for the process of actually deriving those entries. - Register it in
src/registry.js:const kap1 = require('./mappings/kap1'); const FONTS = { kap113, kap1 }; toFont('kap1', text)/toUnicode('kap1', text)work immediately —listFonts()picks it up automatically too.
How to expand a font's mapping table
- Look at
assets/glyph_sheet_0.png,glyph_sheet_1.png,glyph_sheet_2.png— every glyph in the font rendered next to its ASCII key code (regenerate per-font withnpm run render-glyphs, see below). - Fastest: if you have any more real bilingual examples (a name, a
sentence — same text in Unicode and typed in the font), add them to
scripts/examples.jsonand run:
It'll auto-verify against what's already confirmed and print exactly what's left to look up.npm run learn # defaults to kap113 npm run learn -- kap1 # or check a different registered font - Add confirmed entries to
CONFIRMED_ENTRIESin the font's module undersrc/mappings/(format:[asciiCode, unicodeSequence]). - Re-run
npm test.
If you ever need to regenerate the glyph sheets from a font file, run
npm run render-glyphs (needs Python 3 + Pillow on that machine). The
render scripts currently point at src/fonts/kap113.ttf; update the
FONT_PATH constant in each scripts/render-*.js to target a different
font's file when working on one.
Open investigation: does KAP113 share a layout with other Gujarati fonts?
Many legacy Gujarati "typing fonts" (KAP, Terafont, Krishna, LMG, ...)
were built around the same shared "Gujarati typewriter" key convention on
the main keyboard layer, so typists could switch fonts without retraining.
A Terafont-Varun keyboard chart was checked against KAP113 and got one
independent confirmation: KAP113's ! key (Shift+1) shape-matched to ૧
via automated comparison against Noto Sans Gujarati — the same value the
Terafont-Varun chart shows for Shift+1. That's one data point, not a
verified table.
Two comparison tools exist to push this further:
npm run render-keyboard-grid # -> assets/kap113_keyboard_grid.png
npm run render-altcode-grid # -> assets/kap113_altcode_grid.pngThe first renders every KAP113 glyph in physical-key shape (base + shift)
so it can be laid next to any Gujarati font's keyboard chart. The second
does the same for the Alt+0161–0254 extended byte range — but that range
is normally vendor-specific even when the main layer is shared, so don't
expect it to match by default. A reference chart (Terafont-Varun) is
saved in assets/reference-charts/ for this comparison.
See the "OPEN INVESTIGATION" block at the bottom of src/mappings/kap113.js
for the promotion path once specific keys are confirmed.
Unmapped characters pass through unchanged rather than being dropped, so gaps in the table are visually obvious (garbled text) instead of silently losing data.
Design notes
- Reph / conjuncts: some KAP113 keys render a ligature that corresponds
to more than one Unicode codepoint (e.g. RA+virama → single reph
glyph, key
l- also reachable via¨in the extended range,lwins ties since it's the original confirmed key). The engine handles this generically viaUNICODE_SEQUENCES_LONGEST_FIRST— a greedy longest-match tokenizer, so multi-codepoint entries are matched before falling back to single characters. - Matra order: Unicode always stores a dependent vowel sign after
its consonant, even when it displays before/around it (e.g. િ visually
sits to the left of its consonant but is stored after it). This font
keeps that same order when typing - confirmed directly against the
rendered font and the original હાર્દિક example (l=reph before N=દ,
"=િ naturally after N, giving
CFlN"Swith no reordering). A detour briefly added explicit reordering logic based on a misread correction; that was reverted once the original example turned out to already be correct. No manual reordering step is needed - direct sequence mapping is correct for everything confirmed so far.
Files
src/
fonts/
kap113_0.ttf — font file, importable directly by bundlers for @font-face (one per registered font)
mappings/
kap113.js — KAP113's ASCII <-> Unicode table (edit this to add entries)
registry.js — font-name -> mapping module lookup (register new fonts here); no Node built-ins, browser-safe
engine.js — generic conversion engine, shared by every font
index.js — public exports (CommonJS): toFont, toUnicode, listFonts
index.mjs — public exports (ESM), for `import` / bundlers
node-fonts.js — Node-only helper: getFontFilePath(name) -> absolute .ttf path
scripts/
learn-mapping.js — cross-check new examples against confirmed entries (per font)
render-glyph-sheet.js — regenerate assets/glyph_sheet_*.png from a font file
render-keyboard-grid.js — regenerate assets/kap113_keyboard_grid.png (main layer, chart comparison)
render-altcode-grid.js — regenerate assets/kap113_altcode_grid.png (Alt+0161-0254 range)
demo/
kap113-converter.html — self-contained interactive demo (embeds the font + mapping)
assets/
glyph_sheet_*.png — every glyph rendered with its ASCII code
kap113_keyboard_grid.png — main layer rendered in keyboard shape, for chart comparison
kap113_altcode_grid.png — extended byte range rendered for chart comparison
reference-charts/ — third-party layout charts used for cross-checking (not confirmed identical)
test/
convert.test.js — round-trip test using the confirmed example