Module 0 of Make Thai Fonts That Sell · free to read in full
Module 0 — Your First Font in 45 Minutes
What you will have at the end: A working .ttf/.woff2 family with your own name inside it, installed and typed with
Download the starter kit
9 files — the three scripts this module runs, the config they read, and the real Anuphan variable font they run against. Every command on this page works with it, start to finish.
Download font-factory-starter.zip ↓
Needs Python 3.11 or newer — Step 1 below installs it, one step at a time.
By the end of this module you will have real font files in four weights — Light, Regular, Medium, Bold — as .ttf for installing on your machine and .woff2 for the web, with the family name, manufacturer, designer, URL and vendor ID inside the file belonging to you and to nobody else. You will run a 52-check suite that proves the tone marks still sit where they belong, then install that font on your own machine and type Thai with it. None of this requires opening a font editor, and none of it costs a baht in software.
This module is not here to make you know something. It is here to put something in your hands in one sitting, because seeing a file you built yourself working on screen is the only thing that gives people the nerve to carry on into the harder modules.
One naming convention, stated once so the rest of the toolkit reads straight. ThaiGraph Condensed is my own production font — the one that ships in the toolkit’s config.json and whose real output appears throughout Module 3 and Module 4. Somsak Display is the worked example you build here, so that nothing in your dist/ folder can be confused with mine. Wherever a later module quotes a filename like ThaiGraphCondensed-Regular.ttf, that is my file being shown to you as evidence; yours will be named after whatever you put in ps_family.
Read this first: the licensing trap that will land you in trouble
The font you are about to build in this module cannot be sold on its own, and not because it is not good enough. The license forbids it outright. The default in config.json points at Anuphan, a variable font released under the SIL Open Font License 1.1 by Cadson Demak. When you condense it and rename it, what you hold is a Modified Version of an OFL font, and OFL clause 1 reads:
“Neither the Font Software nor any of its individual components, in Original or Modified Versions, may be sold by itself.”
— SIL Open Font License 1.1, PERMISSION & CONDITIONS clause 1 (accessed 15 August 2026)
Put into language you can make a decision with: neither the font software nor any individual component of it, in the original version or in the version you modified, may be sold by itself. No listing on MyFonts. No Creative Market. No price tag on Gumroad for the bare font file.
What you can do is give it away, bundle it with software or a product, embed it in commercial websites and apps, and use it on client work without limits. That makes it an excellent lead magnet. It does not make it a product.
scripts/build.py prints this warning on every run, so that nobody finds out after they have already hit publish. If your goal is a font you can sell, you need outlines you drew yourself from end to end, which is the business of Module 6, and the whole legal argument sits in Module 2. Read both before you put a price on anything.
What you need before you start
You need three things: a machine with an internet connection, the toolkit’s font-factory/ folder unzipped somewhere you can find it again, and patience with a black screen. No Glyphs. No FontLab. No prior Python. Every command line on this page can be copied and pasted as it stands.
Step 0 — download and unzip the toolkit files
Use the download button on this page — or, if you opened this from the toolkit contents page, the button there. There is one zip, and which one you get depends on where you are:
- Free starter kit —
font-factory-starter.zip. Nine files: the three scripts this module runs, the config they read, and the real variable font they run against. No purchase and no email. Every command on this page works with it, start to finish. - Core —
font-factory-core.zip, which addspublish-webfont.py,apply-fea.pyandcheck-shaping.py - Pro —
font-factory-pro.zip, which contains everything in the Core zip plus the original-design engine and the licence templates
Pro buyers download one file, not two. Nothing in this module needs anything that is only in the Core or Pro zips — that is what makes it the free one.
Unzip it. To keep every command on this page copy-pasteable, unzip it into your Documents folder, which gives you this:
Documents/
font-factory/
README.md
requirements.txt
config.json
scripts/ build.py, qa.py, specimen.py
(Core and Pro also add publish-webfont.py,
apply-fea.py and check-shaping.py — this
module uses none of them)
sources/ Anuphan[wght].ttf, OFL-Anuphan.txt
LICENCE-toolkit-code.txt
That is twelve files, and six of them are scripts. Four you will use in this module. The other two belong to the original-face route: apply-fea.py attaches the Thai OpenType features to a font you drew yourself, and check-shaping.py proves they work. Module 6 and Module 7 use them. They are in the Core zip, not the Pro zip, because the route to a font you may sell has to be complete at Core.
Windows — right-click the downloaded .zip, choose Extract All…, and set the destination to C:\Users\<your name>\Documents. Do not skip the extract step and work inside the zip preview window: Windows will let you open files in there, but Python cannot write into it, and the build will fail in a way that looks like a Python problem.
macOS — double-click the .zip in ~/Downloads, then drag the resulting font-factory folder into ~/Documents.
If you put it somewhere else, that is fine — every cd command below just has to point at wherever you actually put it.
scripts/ is the complete build pipeline: it turns a source font into named, condensed, QA-checked, packaged output, and it is in both zips. Every command in this module runs against it, as do Modules 3 and 4. The Pro zip adds a second folder, original/, which is a parametric engine for drawing outlines from scratch as data — an accelerator for Module 6, not a missing piece. Where a later module reaches for a file in original/, it says so in a marked box and names the Core route to the same result. Nothing on this page needs it.
How long this really takes — read before you start the clock
Forty-five minutes is the time at the keyboard once Python is already installed and answering. If this is your first time, budget ninety minutes, and treat that as normal rather than as a sign you are slow.
Where the extra time goes, all of it documented on this page: downloading and installing Python itself (10–20 minutes, mostly your connection); the Windows Microsoft Store alias detour in Step 1 (5–15 minutes); the PowerShell execution-policy detour in Step 2 (5 minutes); and a
pip installthat has to compile a wheel instead of downloading a prebuilt one in Step 3 (5–15 minutes, and only on some machines).Second and third runs really are 45 minutes, because none of those detours happen twice. That is the number the module is named after.
Step 1 — Install Python and confirm it is on the PATH
This is the step people break most often, and almost never because the install failed. It is because the terminal cannot find the program that was installed. This pipeline needs Python 3.11 or newer, and was developed and tested on 3.13.2.
Download from python.org/downloads. The stable version that page offered as of 15 August 2026 is 3.14.7 (python-3.14.7-amd64.exe for Windows, python-3.14.7-macos11.pkg for macOS). If you want the smoothest possible ride, take the 3.13.x line from the All releases page, because that is the version every dependency is pinned against and tested on.
Windows — during installation you must tick “Add python.exe to PATH” on the first screen. If you miss it, run the installer again and choose Modify.
Check it:
python --version
python3 --version
You want a single line that looks like Python 3.13.2. If the minor number is below 11, install again.
If you are stuck here — Windows opens the Microsoft Store instead of reporting a version
Windows ships a fake “app execution alias” named
python.exethat sends you to the store. Turn it off at Settings → Apps → Advanced app settings → App execution aliases, then switch offpython.exeandpython3.exe. After that, close the PowerShell window and open a new one, because PATH is only reloaded when a window opens.If it still fails, try
py --version. Thepycommand is the Python launcher, which the installer always puts in place. Ifpyworks andpythondoes not, usepyin place ofpythonfor Step 2 only. After that we call Python through the venv, which has nothing to do with PATH any more.
Step 2 — Create and activate the venv
A venv is a separate box holding this project’s libraries so they never mix with the system Python. The real benefit is not tidiness but reproducibility. With versions pinned in requirements.txt, the venv guarantees your machine runs exactly the same code as the machine this pipeline was tested on.
Windows (PowerShell)
cd $HOME\Documents\font-factory
python -m venv .venv
.\.venv\Scripts\Activate.ps1
macOS
cd ~/Documents/font-factory
python3 -m venv .venv
source .venv/bin/activate
Those two cd lines assume you unzipped into Documents as described in Step 0. If you put the folder somewhere else, swap in that path. Everything from here on runs with font-factory/ as the current folder, and every later module assumes the same thing.
When activation succeeds, the prompt line starts with (.venv). That is the only signal you need to look for.
If you are stuck here — PowerShell refuses with “running scripts is disabled on this system”
That is the Windows Execution Policy, not a Python problem. Run this line first, then run activate again:
Set-ExecutionPolicy -Scope Process -ExecutionPolicy Bypass
-Scope Processmeans it applies to this window only. Close the window and the old setting is back. It is the safest route. Do not set-Scope LocalMachine.An easier way around it: do not activate at all. Call the Python inside the box directly every time, for example
.\.venv\Scripts\python scripts\build.py. The result is identical in every respect.
Step 3 — Install the dependencies, and why brotli matters
One command does it, but read what it installs, because one of them is the trap that fools people best in this pipeline. requirements.txt is in the folder you unzipped in Step 0 — all three zips ship the same one — so this command works with no further downloads.
python -m pip install --upgrade pip
pip install -r requirements.txt
python -m pip install --upgrade pip
pip install -r requirements.txt
Step 2 claimed that the venv makes your machine run the same code as the machine this pipeline was tested on. Here is the whole of that claim, made checkable — these are the pins, copied out of requirements.txt as shipped:
fonttools==4.63.0
brotli==1.2.0
uharfbuzz==0.56.0
skia-pathops==0.9.2
freetype-py==2.5.1
pillow==12.3.0
== means exactly that version, not “that version or later”. Open requirements.txt yourself and check the numbers against this list before you install; if they differ, trust the file in your folder over this page, because the file is what pip reads.
If requirements.txt ever goes missing, or you want to install into a venv somewhere else without copying the file about, this one line installs the same set:
pip install fonttools==4.63.0 brotli==1.2.0 uharfbuzz==0.56.0 skia-pathops==0.9.2 freetype-py==2.5.1 pillow==12.3.0
pip install fonttools==4.63.0 brotli==1.2.0 uharfbuzz==0.56.0 skia-pathops==0.9.2 freetype-py==2.5.1 pillow==12.3.0
Five main things go in:
- fontTools — reads, converts and writes font files. The whole pipeline stands on it.
- uharfbuzz — HarfBuzz, the text shaping engine, used in
qa.pyto prove the tone marks really attach. - skia-pathops — boolean path merging, used only by the Pro zip’s
original/pipeline. - freetype-py and pillow — render the visual proof sheets, again only for
original/. - brotli — WOFF2 compression.
brotli is the trap. Not one file in the pipeline has a line that says import brotli. Your editor will tell you it is unused, and you will want to delete it. Do not. fontTools calls it itself the moment you save a .woff2 file. Without brotli the job breaks at the save step, not at the install step, and the error you get will not tell you that brotli is the problem.
Confirm all of it with one line:
python -c "import fontTools, uharfbuzz, brotli; print('ok')"
python -c "import fontTools, uharfbuzz, brotli; print('ok')"
If you get ok, you are ready. If you do not, fix it now rather than when the build breaks.
If you are stuck here — pip fails with SSLError / CERTIFICATE_VERIFY_FAILED
Almost always a corporate network with a proxy in the middle, or a pip that is too old. The order to try:
python -m pip install --upgrade pip, then try again- If you are on an office network or a VPN, switch to a mobile connection and try again — this works more often than you would think
- Last resort only, and only if you genuinely must:
pip install -r requirements.txt --trusted-host pypi.org --trusted-host files.pythonhosted.org. Understand that this switches off certificate checking. Use it only on machines you control, and do not let it become a habit.
If you are stuck here — pip reports that building a wheel failed for skia-pathops or pillow
This usually happens when your Python is so new that no prebuilt wheel exists yet. The most direct fix is to step back to Python 3.13.x, the version these pins were tested against. The other option is to change only the failing line in
requirements.txtfrom==to>=, then runpython scripts/qa.py— the QA result is what tells you the pipeline still works, not the install log.There is also a shortcut worth knowing, because it can save you the whole detour. Nothing in
scripts/importsskia-pathops,freetype-pyorpillow:build.py,qa.py,specimen.pyandpublish-webfont.pybetween them need only fontTools, brotli and uharfbuzz. Those three other packages exist for the Pro zip’soriginal/engine. So if one of them refuses to build and you are not working inoriginal/today, delete its line fromrequirements.txtand install again. You lose nothing this module uses.
Step 4 — Edit config.json so the font is yours
scripts/build.py does not contain a single string identifying the font. Everything that says what this font is called and whose it is lives in config.json alone. That is why pointing the script at your config gives you your font, rather than somebody else’s font with a new label stuck on it.
Open config.json — it sits at the top level of the folder you unzipped, next to README.md — and edit it line by line.
Two terms appear in the table below and you only need a working definition of each for now. A name ID is a numbered slot in the font’s name table, which is the part of the file that stores text about the font: ID 0 is the copyright line, ID 6 is the PostScript name, ID 13 is the licence text, and so on. The numbers are fixed by the OpenType format, so every program on earth looks in the same slot for the same thing. achVendID is a four-byte field in another table, OS/2, holding a short code that identifies who made the font. Both tables get taken apart properly in Module 3; for this step, treat them as labelled boxes that build.py fills in from your config.
Here is what every key means and what it does to the actual font file:
| Key | What it does to the font |
|---|---|
source | Path to the source variable font, resolved relative to the config file itself. It already points at the font that shipped in your zip; leave it alone unless you deliberately swap the source |
source_project | The credit line for the source. Embedded in the copyright line (name ID 0) and the header of OFL.txt |
license_source | Path to the source project’s OFL file. Defaults to sources/OFL-Anuphan.txt |
family_name | The name people see in the font menu (name IDs 16 and 1). Thai is allowed |
ps_family | The PostScript name. Must be ASCII with no spaces. Appears in name IDs 6 and 3, and is also the filename that gets saved |
version | Version number, e.g. 1.000. Appears in name IDs 5 and 3 |
condense_factor | Horizontal condensation ratio. 0.85 means 15% narrower, and it is the value QA checks against |
manufacturer | Foundry / manufacturer name (name ID 8) |
designer | Designer credit (name ID 9) |
vendor_id | Four-character vendor code in OS/2 achVendID. Go over four characters and the build stops immediately |
vendor_url | Your URL (name ID 11) |
modified_by, modified_year | Who modified it and in what year. Assembled into the copyright line, and QA checks that this name really appears |
license_model | ofl-derivative or proprietary, nothing else |
license_description, license_url | Required when license_model is proprietary. Leave them out and the build fails |
copyright | Optional. If present, it overrides the copyright line the system generates |
weights | The list of weights. Each has name, target, source_wght |
The three keys people get wrong most often:
ps_family — put Thai or a space in it and the font behaves strangely in any program that refers to the PostScript name. Use plain ASCII run together, for example SomsakDisplay.
vendor_id — four characters is the format’s real ceiling, not a suggestion. build.py checks it as soon as the config loads and stops with a message explaining why. Choose your own abbreviation. Do not leave somebody else’s in place, because leaving the source vendor ID on derived work credits the wrong person.
weights — notice that source_wght is always higher than target. Weight 400, for instance, is pulled from the source at 490. That is stem compensation. When you squeeze letters narrower, the vertical strokes thin out with them, so you have to start from a weight heavier than your target for the result to read at the right weight.
An edited config looks like this:
{
"source": "sources/Anuphan[wght].ttf",
"source_project": "Anuphan (Copyright 2019 The Anuphan Project Authors, https://github.com/cadsondemak/Anuphan)",
"family_name": "Somsak Display",
"ps_family": "SomsakDisplay",
"version": "1.000",
"condense_factor": 0.85,
"manufacturer": "Somsak Type",
"designer": "Derived from Anuphan by Cadson Demak",
"vendor_id": "SMSK",
"vendor_url": "https://example.com",
"modified_by": "Somsak",
"modified_year": 2026,
"license_model": "ofl-derivative",
"weights": [
{ "name": "Light", "target": 300, "source_wght": 380 },
{ "name": "Regular", "target": 400, "source_wght": 490 },
{ "name": "Medium", "target": 500, "source_wght": 590 },
{ "name": "Bold", "target": 700, "source_wght": 700 }
]
}
About that first line. "source": "sources/Anuphan[wght].ttf" already resolves, because Anuphan[wght].ttf is in the zip you unzipped in Step 0, alongside OFL-Anuphan.txt, its licence. There is nothing to download and nothing to hunt for; the pipeline runs out of the box on purpose. If you want to see where it came from, the upstream project is cadsondemak/Anuphan — Anuphan, Copyright 2019 The Anuphan Project Authors, released by Cadson Demak under the SIL Open Font License 1.1. It is included so the pipeline has something real to chew on from your first command, and everything you derive from it inherits that licence, which is the whole subject of the section you read at the top of this page.
Do not switch license_model to proprietary in the hope of selling it. If the outlines still come from somebody else’s font, labelling them as your property is a license breach, not a loophole. build.py prints that sentence out itself.
Step 5 — Run the build
One command, eight files, plus one license file.
python scripts\build.py
python scripts/build.py
If you keep several families side by side, the script takes --config and --out:
python scripts\build.py --config families\myface.json
python scripts\build.py --out build\
python scripts/build.py --config families/myface.json
python scripts/build.py --out build/
Those two flags are everything build.py accepts. There is nothing else. --config defaults to config.json at the same level as the scripts/ folder, and --out defaults to dist/ next to the config file.
One trap that comes with --config, so you know it before you meet it: the source and license_source paths inside a config are resolved relative to that config file’s own folder, not to where you are standing. Move a config down into a families/ folder without changing those two paths and the build stops with CONFIG ERROR: source font not found. Module 4 sets this up properly. For now, leave config.json where it is and ignore both flags.
Four things happen inside: instance pins the variable font at the specified source weights → condense scales every horizontal coordinate, in glyf, in hmtx and in GPOS → rebrand rewrites the entire name table from your config → save as TTF, then convert to WOFF2.
Two of those words need unpacking now, and both get their proper treatment in Module 3. A variable font is one file that holds a whole continuous range instead of one fixed design — Anuphan carries a weight axis running from 100 to 700, and any number in between is a real, valid weight. To instance it, also called pinning, is to pick one number on that axis and freeze it into an ordinary static font that no longer varies. That is what build.py does four times, once per weight in your config, which is why you end up with four separate .ttf files rather than one. glyf, hmtx, GPOS and name are the four binary tables inside a font file that this step rewrites; Module 3 opens each of them and shows you what a user sees on screen when one is wrong.
The GPOS step is the one that decides whether your Thai font is usable or useless. The anchor points for mark and mkmk live in that table. Condense the outlines without condensing the anchors and every tone mark drifts off its consonant to the right, and there is no way you will see that from looking at the filenames.
What you should get:
building Somsak Display v1.000 -> ...\font-factory\dist (condense 0.85)
built SomsakDisplay-Light.ttf (...)
built SomsakDisplay-Light.woff2 (...)
built SomsakDisplay-Regular.ttf (...)
...
built OFL.txt
------------------------------------------------------------------
LICENSING - READ BEFORE YOU LIST THIS ANYWHERE
...
Notice that OFL.txt is always written out alongside the binaries. That is not a bonus. OFL requires the license text to travel with every copy, and QA checks that this file is really there.
If you are stuck here — the build itself errors instead of printing
built ...linesEverything up to now has been about getting the tools installed. This box is about the build failing once they are. There are only two shapes it comes in, and you can tell them apart from the first word of the output.
- A line starting
CONFIG ERROR:— the script read yourconfig.json, found something it cannot work with, and stopped before writing a single byte. It exits with code 2. Nothing was created and nothing was damaged. The message names the exact problem: a missing key, invalid JSON, avendor_idlonger than four characters, alicense_modelit does not recognise, or asourcepath that does not resolve. Appendix A, section A6 lists every one of these messages the pipeline can emit, with the cause and the fix beside it. Note that the checks run one at a time and stop at the first failure, so fixing one and immediately seeing another is the system working, not the system taunting you.- A Python traceback — several indented lines ending in something like
ModuleNotFoundErroror an error raised inside fontTools. That means the environment, not the config. Go back to the confirmation line in Step 3,python -c "import fontTools, uharfbuzz, brotli; print('ok')", and run it again. A traceback that appears only at the.woff2save step, after the.ttffiles were already written, is brotli almost every time — Appendix A, section A4 covers exactly that case.To read the exit code yourself, run
echo $LASTEXITCODEin PowerShell orecho $?in bash immediately after the command.0is success,2is a config error, and anything else is a crash.
Step 6 — Run QA and learn to read the 52 results
Opening the font in one program is not QA. A Thai font can look perfect in Notepad and drop every tone mark in InDesign, because text editors and layout engines disagree about which OpenType features to run. So qa.py pushes real Thai text through HarfBuzz and confirms the result with numbers.
python scripts\qa.py
python scripts/qa.py
qa.py takes two flags: --config and --dist (not --out — different script, different name):
python scripts\qa.py --config families\myface.json
python scripts\qa.py --dist build\
python scripts/qa.py --config families/myface.json
python scripts/qa.py --dist build/
A full pass ends with this line:
ALL 52 CHECKS PASSED
52 is not a magic number. It is 13 checks times 4 weights. Add a fifth weight to the config and the number becomes 65 on its own. Knowing where the number comes from matters, because if you ever see ALL 39 CHECKS PASSED it means one weight went missing quietly, not that everything is fine.
The thirteen checks per weight fall into two groups. The first is text shaping, six checks. The second is file identity, seven checks.
There are three lines you have to actually read, rather than just glance at for the word PASS.
One — the width ratio
PASS advance width ratio ~0.85 [ratio=0.850]
The number in brackets is the real total width of the test string in your font, divided by the same width in the source font. It must equal the condense_factor you set. If you set 0.85 and get ratio=1.000, the condensation never happened. If you get ratio=0.720, something was condensed twice. This number catches errors the naked eye cannot see.
Two — the ccmp swap to the descenderless consonant
PASS ccmp descless alternate fires for yo ying + sara u [gid 401 -> 402]
Two bits of shorthand in that one line. ccmp is an OpenType feature tag — a four-letter name for a rule set inside the font, in this case “glyph composition and decomposition”, the feature that swaps one glyph for another when the surrounding characters call for it. gid is short for glyph ID, the number a font uses internally to refer to a glyph; it has nothing to do with Unicode codepoints and is specific to this one file. Module 3 takes both apart properly, along with the tables they live in.
Read it like this: ญ (yo ying) on its own uses glyph number 401, but as soon as สระอุ (sara u, the vowel that hangs below the line) follows it, it swaps to glyph number 402, which is ญ without its descending tail. If the two numbers are the same, the swap is not firing, and your ญุ will have the tail of ญ colliding with sara u. The actual numbers vary by source font. What matters is that they differ, not that they are 401 and 402.
Three — mkmk mark stacking
PASS mkmk stacking matches source (y kept, x condensed) [src=... dst=...]
This line compares mark positions in your font against the source font one by one. The rule is that y must be unchanged (condensing horizontally should not touch height) and x must be multiplied by condense_factor exactly. This is the check that proves the GPOS step worked. Microsoft lists mark and mkmk in the feature table for Thai script as required features, not optional ones.
The other three shaping checks confirm that no .notdef shows up in a mixed Thai and Latin sample, that marks really are being repositioned, and that สระอำ (sara am) is decomposed into nikhahit and sara aa — behaviour HarfBuzz performs itself inside the Thai script shaper code.
The seven identity checks are the ones that catch the most expensive mistake in this pipeline: shipping a font that still has somebody else’s name on it. Has the family name really changed; is there any leftover word from the source in the PostScript name; is usWidthClass set to condensed; is the vendor ID yours; does the copyright line name you; is there license text; and is OFL.txt sitting next to the binaries.
If any check fails, the script exits with code 1 and prints a summary of what broke. Do not move on to the next step. Go back, fix the config or rebuild first.
Step 7 — Build the specimen and look at it
A specimen is not decoration. It is the file you send to clients, attach to a sales page, and use to check your own work at real size.
python scripts\specimen.py
python scripts/specimen.py
It takes --config and --dist, like qa.py:
python scripts\specimen.py --config families\myface.json --dist build\
python scripts/specimen.py --config families/myface.json --dist build/
The result is dist/specimen.html, which references the .woff2 files sitting beside it. Zip the whole folder, send it to anyone, and it opens. No server, no internet connection.
Open it:
start dist\specimen.html
open dist/specimen.html
The Thai section of that page is meant to be torture, not pretty sentences. Check all five of these words: นิ่ง (ning — an upper vowel and a tone mark stacked two levels), น้ำ (nam — sara am decomposed into nikhahit and sara aa), ญุ ฐู (consonants swapping to their descenderless forms), ปั่น (pan — a tall consonant, where the mark has to lift higher than it does over บ), and ผู้ใหญ่ (phu yai — a lower vowel plus a tone mark on a tall consonant). Get those five right and the rest usually follows.
Step 8 — Install the font and type Thai with it
This is the step that makes the whole thing real. Open Word, Figma or Illustrator and see the name you chose sitting in the menu.
Windows — open the dist\ folder, select all four .ttf files, right-click → Install for all users (plain Install puts them under the current user only, at %LOCALAPPDATA%\Microsoft\Windows\Fonts, where some older programs cannot see them).
macOS — double-click a .ttf file and press Install Font in Font Book, or drag all four files into ~/Library/Fonts.
Then open the program you actually work in, pick the name you set in family_name, and type:
ผู้ใหญ่หาผ้าใหม่ ให้สะใภ้ใช้คล้องคอ
น้ำใจ ญุ ฐู ปั่น นิ่ง
If every tone mark lands where it should, you have just built and installed your first Thai font.
If you are stuck here — installed, but the font does not appear in the menu, or appears with the old shapes
Windows caches the font list, and programs read that cache when they start. The order to work through:
- Quit the program completely and reopen it — Figma desktop, Illustrator and Word all read the font list once, at startup. This fixes it more than half the time.
- If it still does not show, clear the system cache. Open PowerShell as administrator and run:
net stop FontCache net start FontCache
- If you rebuilt using the same
family_name, Windows may still be holding the old shapes. Uninstall the old font first (Settings → Personalization → Fonts → select → Uninstall), then install again. While developing, bumping theversionnumber on every build reduces this.On macOS: if Font Book warns about duplicate fonts, choose Resolve Automatically, then restart the target program.
Back to licensing, before you do anything else with this file
You are now holding a font that genuinely works, and it still cannot be sold on its own. Nothing you did in the last hour or two changed its legal status, because every outline in it still descends from Anuphan.
What you can do straight away, entirely within the rules:
- Give it away as a lead magnet on your site, with
OFL.txtattached - Embed it as a webfont in client websites, commercial work included
- Bundle it with software, templates or products you sell — OFL clause 2 permits this explicitly, on condition that every copy carries the copyright line and the license text
- Use it as your own brand typeface
What you cannot do, however thoroughly you renamed it, condensed it and stamped your own vendor ID on it: put a price on this font file by itself.
One piece of luck is that Anuphan does not declare a Reserved Font Name in its copyright line. Your rename is therefore clean, with nobody to ask for permission. But if you ever switch to a different source font, that may no longer be true — how to check is in Module 2.
If you want a font you can actually sell, the answer is not a slicker config. It is owning every outline. That is Module 6, and both tiers get there.
Pro accelerator — the parametric route uses
original/design.py,original/glyphs.pyandoriginal/build.py, which are in the Pro zip.Core buyers reach the same end state by drawing the outlines in FontForge, “a free and open source font editor” with installers for Windows, macOS and GNU+Linux, exporting a TTF, and running that TTF through the same
scripts/pipeline you have just used for naming, metrics, QA, specimen and packaging. Module 6 documents that route in full. It takes longer; it does not take a different toolkit, and the font it produces is one you may sell.
Checklist — what you are holding now
- Python 3.11+ installed, and
python --versionanswers from a fresh terminal - venv created, and
import fontTools, uharfbuzz, brotliprintsok -
config.jsonhas your ownfamily_name,ps_family,manufacturer,designer,vendor_id,vendor_urlandmodified_by - The
dist/folder holds four.ttffiles, four.woff2files andOFL.txt -
python scripts/qa.pyends withALL 52 CHECKS PASSED - You have read and understood three lines:
ratio=0.850,gid 401 -> 402, and the mkmk line -
dist/specimen.htmlopens, and all five Thai test words render correctly - The font is installed on your machine and you can type a Thai sentence with it
- You know this file can be given away, bundled and embedded, but not sold on its own
Next: Module 1 — How Fonts Actually Make Money, which is the free sample most buyers have already read; if you have, skim the four questions at the end of it and then go straight on to Module 2 — Licensing: What You Can Sell and What You Cannot, which will let you decide for yourself, every time, whether the file in front of you is a product or a giveaway.
Sources
- SIL OFL 1.1 clause 1 forbids selling the font software or any of its individual components by itself, in Original or Modified Versions.—SIL Open Font License 1.1 — official text (accessed Aug 15, 2026)
- Anuphan is released under OFL 1.1 by Cadson Demak, and its copyright line carries no 'with Reserved Font Name' phrase.—Anuphan — OFL.txt, cadsondemak/Anuphan (accessed Aug 15, 2026)
- The stable Python release offered on the downloads page is 3.14.7, with python-3.14.7-amd64.exe for Windows and python-3.14.7-macos11.pkg for macOS.—Python.org — Downloads (accessed Aug 15, 2026)
- The pipeline needs Python 3.11 or newer and is tested on 3.13.2. No file imports brotli directly; fontTools reaches for it when saving .woff2.—font-factory/README.md and font-factory/requirements.txt (shipped in this toolkit) (accessed Aug 15, 2026)
- requirements.txt pins fonttools==4.63.0, brotli==1.2.0, uharfbuzz==0.56.0, skia-pathops==0.9.2, freetype-py==2.5.1 and pillow==12.3.0.—font-factory/requirements.txt — read from the shipped file (accessed Aug 16, 2026)
- font-factory-core.zip contains 12 files: LICENCE-toolkit-code.txt, font-factory/README.md, font-factory/requirements.txt, font-factory/config.json, font-factory/scripts/{build.py, qa.py, specimen.py, publish-webfont.py, apply-fea.py, check-shaping.py}, font-factory/sources/Anuphan[wght].ttf and font-factory/sources/OFL-Anuphan.txt. font-factory-pro.zip is a superset, adding original/* and the two EULA templates.—ThaiGraph toolkit — entry listing read from the built public/toolkit-5bb8dac6d47353b9/assets/font-factory-core.zip, cross-checked against the manifest in scripts/build-product-assets.ts (accessed Aug 18, 2026)
- qa.py runs 13 checks per weight and reports ALL 52 CHECKS PASSED across four weights, with ratio=0.850 and gid 401 -> 402.—font-factory/scripts/qa.py — real run, 2026-08-15 (accessed Aug 15, 2026)
- The mark (GPOS) and mkmk (GPOS) features are listed as required for Thai script.—Microsoft — Developing OpenType Fonts for Thai Script (accessed Aug 15, 2026)
- HarfBuzz decomposes SARA AM into NIKHAHIT + SARA AA and moves the NIKHAHIT backwards over any above-base marks.—HarfBuzz — src/hb-ot-shaper-thai.cc (accessed Aug 15, 2026)
- FontForge describes itself as 'a free and open source font editor' and publishes install pages for Windows, Mac OS and GNU+Linux.—FontForge — project home page (accessed Aug 16, 2026)