Since my last post, I did some more research in regard to this kerning thing on Kobo devices, and I wrote a whole script to automate many of the changes I’ve been manually making to various fonts.
In particular, I wanted to automate the creation of the legacy kern table that Kobo devices use when rendering text on firmware 4.x. That’s the same thing I blogged about last time.1
How we got here
It makes sense: give a software developer a tedious task he spends more than a couple of hours working on, and at that point he will consider automating it.
Before the advent of LLMs, this was time-consuming process. Today? Not so much.
It still took me twelve hours to get to the final script, but that was because I had never seriously used Python. I was also trying out some of these tools for the first time, too.
👷♂️Keep in mind that this blog post may contain outdated or incorrect information. I've made corrections where necessary, and please also take a look at the bottom of the post for corrections and updates, as there have been more developments in regard to this particular project.
The manual approach?
Like I wrote in the previous post, I needed to use FontForge to open the customized fonts I had previously prepared, one-by-one, and then export them again. For each of those fonts, I needed to make some adjustments to the name of the font family, unique ID and then export using the new “old-style kern” option.
I had already done this for the core font collection, which contained 24 font files (6 unique fonts with 4 styles each) but when I looked at the extra collection, I sighed.
Not only are there at the time of writing a grand total 67 files in that directory, but some of these fonts throw up a variety of warnings when opened with FontForge because they were made with more sophisticated font authoring tools. Oops.
So, manually editing them and exporting them again was not really an option. I had previously used some scripts to rename fonts, so I was wondering if it was possible to write a script to do what re-exporting with FontForge would also do.
Sure enough, once I consulted ChatGPT, Gemini and Claude, I got confirmation that this was, indeed, quite possible.
So, say hello to kobo-font-fix.
What does it do?
By default, it generates Kobo-friendly fonts. For example, you can run:
python3 kobofix.py *.ttf
What this means:
- Each font file is named correctly. This ensures that the correct font is used when using the
epubrenderer. - Each font file contains correct PANOSE information which ensures that the correct font weight and style is accessed when using the
kepubrenderer. Other metadata is also updated to make sure that the style matches the filename. - Each font family gets a different name, prefixed with a prefix of choice. This ensures our obligation to the OFL and the unique names policy is respected. Via a custom parameter the font name can be changed entirely.
- For each font, kern pairs from the GPOS table are copied to the legacy
kerntable. This only applies to fonts that have a GPOS table, which is used for kerning in modern fonts, but ensures proper font rendering using thekepubrenderer. - For each font, the line height is normalized to 20%, which is a great setting for most fonts.
But this script does a lot more. Let’s say I found a custom font and I want to create an “NV” customized version like I have done with other fonts. In most cases, all I’ve adjusted is the line-height, but that still makes it a derivative font.
So, I can run:
./kobofix.py --prefix NV --line-percent 20 --skip-kobo-kern *.ttf
This adds the “NV” prefix to the font, applies the 20% line spacing option but skips the Kobo kerning step, which may not be required.
If I want a custom name for this font, I can run:
./kobofix.py --prefix NV --name="Cornerstone" --line-percent 20 --skip-kobo-kern *.ttf
This is, obviously, super helpful. Assuming I don’t need to many any manual edits to a font, this makes adding normalized, new fonts to my ebooks repository a piece of cake. Previously this would have taken a lot of manual work, with a lot of opening fonts and exporting them…
If I already have a font that was previously authored, like the… I don’t know, 60+ fonts in the extra collection, all it takes is running:
nico@m1ni kobo-font-fix % ./kobofix.py --prefix KF --remove-prefix NV *.ttf --line-percent 0
For example, I ran this on NV Elstob, which is a tweaked version of this beautiful font for medievalists:
Processing: NV-Elstob-Bold.ttf
--remove-prefix enabled: using 'Elstob' as the new family name.
Renaming the font to: KF Elstob Bold
PANOSE corrected: bWeight 8->8, bLetterForm 2->2
Kerning: extracted 342467 pairs; wrote 342467 to legacy 'kern' table.
Saved: KF_Elstob-Bold.ttf
Skipping line adjustment step.
Processing: NV-Elstob-BoldItalic.ttf
--remove-prefix enabled: using 'Elstob' as the new family name.
Renaming the font to: KF Elstob Bold Italic
PANOSE corrected: bWeight 8->8, bLetterForm 3->3
Kerning: extracted 300746 pairs; wrote 300746 to legacy 'kern' table.
Saved: KF_Elstob-BoldItalic.ttf
Skipping line adjustment step.
Processing: NV-Elstob-Italic.ttf
--remove-prefix enabled: using 'Elstob' as the new family name.
Renaming the font to: KF Elstob Italic
PANOSE corrected: bWeight 5->5, bLetterForm 3->3
Kerning: extracted 286857 pairs; wrote 286856 to legacy 'kern' table.
Saved: KF_Elstob-Italic.ttf
Skipping line adjustment step.
Processing: NV-Elstob-Regular.ttf
--remove-prefix enabled: using 'Elstob' as the new family name.
Renaming the font to: KF Elstob
PANOSE corrected: bWeight 5->5, bLetterForm 2->2
Kerning: extracted 313998 pairs; wrote 313998 to legacy 'kern' table.
Saved: KF_Elstob-Regular.ttf
Skipping line adjustment step.
==================================================
Processed 4/4 fonts successfully.
Once this script has been executed, I can simply copy over all of the fonts to my Kobo’s fonts directory, eject it, and start reading.
What issues did I bump into?
Python is new to me
Let’s be honest: Python is not my forte. I usually build applications with PHP or Swift, so I had to get used to Python’s syntax, which thankfully didn’t take too long.
Still, I feel as though using indentation instead of curly brackets is proper madness!
Size restriction on kern table
Initially, it appeared as if it was a good idea to write all of the kern pairs to a single subtable. This was actually originally recommended by one of the LLMs for maximum compatibility.
Sadly, this meant that for certain fonts with many pairs, this would simply not work. For fonts with too many pairs, the table would not be read by my e-reader, resulting in the default “zero-kerning” approach you get without this tweak.
The secret, then, was to ensure that no more than 10k pairs are stored per sub-table:
@staticmethod
def add_legacy_kern(font: TTFont, kern_pairs: Dict[Tuple[str, str], int]) -> int:
"""
Create or replace a legacy 'kern' table with the supplied pairs.
Splits into multiple subtables if there are more than 10,000 pairs.
"""
if not kern_pairs:
return 0
kern_table = newTable("kern")
kern_table.version = 0
kern_table.kernTables = []
# Max pairs per subtable
MAX_PAIRS = 10000
items = [(tuple(k), int(v)) for k, v in kern_pairs.items() if v]
for i in range(0, len(items), MAX_PAIRS):
chunk = dict(items[i:i + MAX_PAIRS])
subtable = KernTable_format_0()
subtable.version = 0
subtable.length = None
subtable.coverage = 1
subtable.kernTable = chunk
kern_table.kernTables.append(subtable)
font["kern"] = kern_table
return len(items)
This fix ensures that the table (with its variable amount of subtables) is written to correctly, and we all win, because we get a better reading experience. Yay!
Correction
March 14, 2026
Actually… the paragraph above is wrong. Unfortunately, most text renderers only read the first subtable. While this was a clever workaround, it’s probably best to make a selection of the 10k most relevant kern pairs and to include only those.
For most use cases, it just makes the fonts unnecessarily larger. For more information about this, please see the update below.
Extra arguments
I also made sure to add some extra documentation and flags, which have proven very useful. You can check out this message by running:
python3 kobofix.py -h
I have declared each of these arguments like this:
def main():
"""Main entry point."""
parser = argparse.ArgumentParser(
description="Process fonts for Kobo e-readers: add prefix, kern table, "
"PANOSE validation, and line adjustments.",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
For a default experience, which will prefix the font with KF, add `kern` table and adjust line-height:
%(prog)s *.ttf
If you want to rename the font:
%(prog)s --prefix KF --name="Fonty" --line-percent 20 *.ttf
If you want to keep the line-height because for a given font the default was fine:
%(prog)s --line-percent 0 *.ttf
For improved legacy support, you can remove the GPOS table (not recommended):
%(prog)s --prefix KF --name="Fonty" --line-percent 20 --remove-gpos *.ttf
To remove a specific prefix, like "NV", before applying a new one:
%(prog)s --prefix KF --remove-prefix="NV" *.ttf
"""
)
parser.add_argument("fonts", nargs="+",
help="Font files to process (*.ttf). You can use a wildcard (glob).")
parser.add_argument("--name", type=str,
help="Optional new family name for all fonts. Other font metadata like copyright info is unaffected.")
parser.add_argument("--prefix", type=str, default=DEFAULT_PREFIX,
help=f"Prefix to add to font names. Required. (Default: {DEFAULT_PREFIX})")
parser.add_argument("--line-percent", type=int, default=DEFAULT_LINE_PERCENT,
help=f"Line spacing adjustment percentage. Set to 0 to make no changes to line spacing. (Default: {DEFAULT_LINE_PERCENT})")
parser.add_argument("--skip-kobo-kern", action="store_true",
help="Skip the creation of the legacy 'kern' table from GPOS data.")
parser.add_argument("--remove-gpos", action="store_true",
help="Remove the GPOS table after converting kerning to a 'kern' table. Does not work if `--skip-kobo-kern` is set.")
parser.add_argument("--verbose", action="store_true",
help="Enable verbose output.")
parser.add_argument("--remove-prefix", type=str,
help="Remove a leading prefix from font names before applying the new prefix. Only works if `--name` is not used. (e.g., --remove-prefix=\"NV\")")
So, what was the point?
I can now automate these fixes for many fonts, including the extra collection I was talking about earlier in this post. All I need to run is:
nico@m1ni kobo-font-fix % ./kobofix.py --prefix KF --remove-prefix NV *.ttf --line-percent 0
In fact, you can get a copy of my “KF” (Kobo-fixed) fonts as part of the latest release of my latest release in the ebook-fonts repository.
Make sure to pick up the kobo-core or kobo-extra files if you plan on reading any books purchased via the Kobo Store or converted via Calibre.2
I’ve also shared my Kobo configuration, which contains customizations to a NickelMenu config as I use it on my device.
If you want to fix a font that appears to be broken on Kobo devices, you can try kobo-font-fix. Obviously, please keep font licenses in mind, and do not modify fonts you have no permission to modify.
Update
March 14, 2026
I am currently working on an updated version of this script that includes a couple of important fixes. In order to make it easier to fix fonts, I’ve now added a couple of presets that can be directly used, and I’ve also added a --dry-run setting.
I’ve been able to verify that this script is not necessary if you set webkitTextRendering to optimizeLegibility, because then the renderer will correctly read GPOS data, or so it seems. This way you won’t need the legacy kern table.
The vast majority of users will unfortunately not get proper kerning and ligatures, because this workaround is not the default behaviour, so I still think there’s a good reason to keep the script around for the foreseeable future.
Currently, Rakuten is testing firmware 5.x which will likely come to these newer Kobo devices at some point. This updated version is supposedly using a newer version of Qt, so perhaps their font rendering code will also be updated. It’s my understanding that some webview (likely QtWebEngine?) is involved. I should note that I have yet to try this updated version, but I figure this script is going to be useful regardless since it is unlikely all Kobo devices will receive this latest update. In fact, I suspect only the latest devices at the time of writing (Clara BW, Clara Color, Libra Color) will receive this update. Of course, I could be wrong, but that is my suspicion. ↩
Each of these fonts is either a KF or an NV version, and both versions can coexist on any device, although compatibility may vary depending on the device in question. I recommend using the KF fonts on Kobo devices only. ↩
This was not generated: Unless explicitly specified, the posts on my blog are my own creative work. 100% written (typos included) and edited by me; not generated using e.g. large language models.

Comments
Nothing yet. Say the first thing.
Sign in to join the conversation.