Skip to content

Implement kerning lookups#394

Merged
cmyr merged 17 commits intomasterfrom
kerning-lookups
Apr 8, 2026
Merged

Implement kerning lookups#394
cmyr merged 17 commits intomasterfrom
kerning-lookups

Conversation

@RickyDaMa
Copy link
Copy Markdown
Collaborator

@RickyDaMa RickyDaMa commented Mar 20, 2026

Every so often I get surprised that this isn't already part of norad, so I threw something together.

I've made a slow version for people only doing a couple kerning lookups and don't want the overhead of generating the reverse group lookup, and then there's also a ReverseGroupsLookup struct now which does exactly what it says on the tin and can get used to accelerate kerning lookups.

There is currently no check that you are using the ReverseGroupsLookup that corresponds to the Font you're calling the lookup on, but I'll document that caveat when I throw together some documentation ("garbage in, garbage out" or words to that effect). I could tie the two together with ✨ fancy ✨ API/types if we wanted.

I also made constants for the kerning group prefixes and did a quick & dirty replace throughout the codebase, so I can let autocomplete do the work instead of worrying that I've typo'd. Bikeshedding encouraged, I don't like how long their names are.

@RickyDaMa RickyDaMa self-assigned this Mar 20, 2026
Copy link
Copy Markdown

@Hoolean Hoolean left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for tackling, would love to have something like this in the shared Rust toolbox

@RickyDaMa RickyDaMa marked this pull request as ready for review March 20, 2026 17:23
@RickyDaMa RickyDaMa requested a review from cmyr March 23, 2026 14:58
Only iterate through groups once
Copy link
Copy Markdown
Member

@cmyr cmyr left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A couple notes in line, and the biggest point I'd make is already made in the earlier discussion: I think it would be nice if this was simplified to some kind of opaque KerningResolver type that ideally borrows data from the font. I'd also prefer to not have the two APIs, and just always compute the reverse map, unless there is some non-theoretical performance concern you're addressing.

Overall though, I'm happy with the idea of something like this being merged :)

@RickyDaMa
Copy link
Copy Markdown
Collaborator Author

RickyDaMa commented Apr 7, 2026

Hi @cmyr, I've updated the PR based on some of your comments. I've removed the slow version of the method, and updated the documentation of the remaining method.

I've made ReverseGroups depend on Font through lifetimes (which also saves cloning Names).

The only downside here is that the resolver holds a reference to things owned by the font, which means we can't mutate the font once it exists. I don't know how big of a problem this is?

Nothing stops the user from dropping their ReverseGroups. At least, in the current version of the API. I've kept ReverseGroups part of the public API for now, however from another review comment I'm relatively adverse to the idea:

Yeah it would be nice to abstract away ReverseGroups entirely, however that then adds state to Font, which isn't something we currently have AFAIK, and personally I prefer the idea of the norad::Font remaining as pure data. Plus, if it's entirely internal, it's now on us to keep it up-to-date if a user changes groups/kerning. Doable of course.

Remove slow version of method
Rename ReverseGroupsLookup to ReverseGroups
Make ReverseGroups hold references so its lifetime depends on the Font, also preventing edits without dropping it
impl Borrow<str> for &Name
src/font.rs Outdated
///
/// This method is requires a [`ReverseGroups`], which can be obtained from
/// [`Font::get_reverse_groups_lookup`].
pub fn kerning_lookup(
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

okay sorry for all the back-and-forth, but what I had in mind was an API that looks something like,

struct LookupKerning<'a> {
    first_groups: HashMap<Name, Name>,
    second_groups: HashMap<Name, Name>,
    kerning: &'a Kerning,
}

impl LookupKerning<'_> {
    fn get(&self, first: &str, second: &str) -> Option<f64> {
        todo!("basically the kerning_lookup method in this PR")

impl Font {
    /// returns a thing you can lookup kerns in
    fn lookup_kerning<'a>(&'a self) -> LookupKerning<'a> {
        todo!("build the thing")
    }
}

/// and then it works like,

let my_font: Font = load_my_font();
let kemings = my_font.lookup_kerning();
let a_value = kemings.lookup("A", "V");



... does that make sense? Is there a reason to prefer the version you've provided here?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Back & forth is fine! Please don't feel bad, I actively enjoy discussing and refactoring this stuff. I like your/Harry's idea more too, I'll shift my implementation to match it 👍

Rename ReverseGroups to KerningResolver
KerningResolver holds a reference to the kerning
Move kerning lookup method to KerningResolver
Go back to using owned Names within KerningResolver
Copy link
Copy Markdown
Member

@cmyr cmyr left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great thanks, this looks good!

@cmyr cmyr merged commit e336462 into master Apr 8, 2026
5 checks passed
@cmyr cmyr deleted the kerning-lookups branch April 8, 2026 15:10
@RickyDaMa
Copy link
Copy Markdown
Collaborator Author

Thanks for the merge! 🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants