This post is part of a collection of posts related to electronic reading.
- Patching Fonts for my Kobo
- Tweaking EB Garamond
- Electronic Reading Adventures
- Down the Kobo Font Kerning Rabbit Hole
- Automating Font Tweaking with Python
Table of Contents
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.
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
epub
renderer. - Each font file contains correct PANOSE information which ensures that the correct font weight and style is accessed when using the
kepub
renderer. 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
kern
table. This only applies to fonts that have a GPOS table, which is used for kerning in modern fonts, but ensures proper font rendering using thekepub
renderer. - 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!
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.
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
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.
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 (which is really a sort of webview, I believe) will also be updated. 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 as of write (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. ↩