Skip to content

jollm/fontsloth

Repository files navigation

fontsloth

Fontsloth is an elisp OTF/TTF font loader and renderer.

It is inspired by fontdue. While fontdue is “the fastest font renderer in the world, written in pure Rust”, fontsloth is “the slowest font renderer in the world, written in pure Elisp”.

Despite the name, being slow is neither a goal nor a requirement.

It is currently pre version 1.0 and the API is not yet stable.

Mostly it is modeled after the aforementioned fontdue and the outliner in Rust ttf-parser. The only novel bit is using Emacs bindat.el to unpack OTF/TTF.

Current semantic version: 0.17.0

Melpa: https://melpa.org/packages/fontsloth-badge.svg

Note: at present this requires Emacs 28

To make it work on Emacs 27.1 would involved backporting the latest bindat.el which itself involves backporting the latest subr-x.el. If you would like this to happen, please let me know.

Contents

Synopsis of work in progress

Now supported

  • Load simple and composite glyphs and character mappings in TTF and OTF fonts

    These must contain loca/glyf or CFF1 tables for glyphs and cmap format4, format0, format6, and/or format12 for mappings.

  • Outline and rasterize glyphs that use quadratic or cubic bezier curves
  • Layouts for lines of horizontal text
  • kerning (as of 0.15.1)
  • OTF font outlining and rasterization (as of 0.16.0) It can now outline OpenType fonts with CFF version 1 tables.
  • Web Open Font Format (WOFF) (as of 0.17.0) Note: since WOFF is a meta format, the underlying format must also be supported.

Not currently supported (but planned)

  • OTF CFF version 2
  • Font variation indices
  • Vertical metrics tables (and vertical text layout)
  • All possible cmap tables for char-code to glyph-id mappings
  • All of GPOS and GSUB for every script/lang
  • Font collections
  • Allow custom per font family sets of additional char-code -> glyph mappings that derive new glyph outlines not in a font from those already present. E.g. subtract, add, or modify contours to produce glyphs that are variations of one or more existing glyphs. This could be fun for icon fonts or possibly to fill in missing standard unicode chars for fonts with similar enough existing glyphs.

Project organization

Fontsloth consists of four primary components:

  • OTF/TTF parser Entry point: file:fontsloth-otf.el

    This uses elisp#Byte Packing (i.e. bindat.el) to unpack TTF and OTF fonts.

    Packing is not currently supported but planned for after version 1.0

  • Glyph outliner Entry points:
    • file:fontsloth-otf.el: generics
    • fontsloth-otf-glyf.el: base implementation for TTF outlines
    • fontsloth-otf-cff.el: base implementation for OTF outlines
    • file:fontsloth-geometry.el: dispatch type and method implementations

    This is modeled after Rust ttf-parser and should be extensible for multiple backends. The current backend is modeled after fontdue.

  • Text layout Entry point: file:fontsloth-layout.el

    This is modeled after fontdue’s layout which is in an immature state (as is this).

    The gist of it is that given text styles, fonts, and a coordinate system, it produces a series of indexable glyph positions to aid in compositing glyphs for display.

  • Raster Entry point: file:fontsloth-raster.el

    This is currently just a port of fontdue’s raster. It is planned to support multiple raster implementations.

    The fontdue author includes the following:

    Notice to anyone that wants to repurpose the raster for your library: Please don’t reuse this raster. Fontdue’s raster is very unsafe, with nuanced invariants that need to be accounted for. Fontdue sanitizes the input that the raster will consume to ensure it is safe. Please be aware of this.

    This is part of the reason why it is planned to support multiple raster implementations.

    So far, I have tested the elisp implementation to faithfully reproduce fontdue’s raster on a byte by byte level for multiple fonts and pixel sizes.

Usage caveats

Currently fontsloth uses pcache.el to provide a persistent cache for loaded fonts, which is the default for fontsloth-load-font.

Invalidation at present must be handled manually. Expect cache load times in the seconds if you load more than 10 or so fonts at a time.

Bypass the cache

;; it will take longer, but won’t end up in cache
;; this is useful if you just want to try it out and see if it works
(fontsloth-load-font my/font :cache 'bypass)

Force reload a font

;; this reloads the font and then stores the result in cache
(fontsloth-load-font my/font :cache 'reload)

Invalidate a cache entry

;; this removes a single font entry from cache
(pcache-invalidate fontsloth-cache my/font)

Clear the cache

(pcache-clear fontsloth-cache)

Installation

MELPA

Install from MELPA using the builtin package manager assuming MELPA is in your package-archives list.

Alternatively install using quelpa (see below) or straight.el.

Manual installation (using Quelpa)

Quelpa allows an installation directly from this repo that is then managed the usual way via package.el. Quelpa can be installed from MELPA or bootstrapped directly from source if desired.

Install directly

;;; after installing quelpa

;; note this uses a MELPA recipe, so the usual MELPA options also apply
(quelpa '(fontsloth :fetcher github :repo "jollm/fontsloth"))

Install with use-package

First install quelpa-use-package (either with quelpa or from MELPA).

;; if quelpa use-package is installed, this should install fontsloth
(use-package fontsloth
  :quelpa ((fontsloth :fetcher github :repo "jollm/fontsloth")))

;; if you want to auto-check for upgrades
(use-package fontsloth
  :quelpa ((fontsloth :fetcher github :repo "jollm/fontsloth") :upgrade t))

Update to the latest git commit

After installation: M-x: quelpa-upgrade

Usage

See Usage caveats for how to load fonts without caching them in pcache.

Load and rasterize

(require 'fontsloth)
;; Rasterize the fontawesome wifi icon and put it in a preview buffer
;; Saving the buffer should turn on image-mode and display it
(defvar my/current-font
  (fontsloth-load-font "/usr/share/fonts/TTF/fontawesome.ttf"))
(pcase-let* ((font my/current-font)
             (glyph-id (fontsloth-font-glyph-id font ?))
             (px 32.0)
             ((cl-struct fontsloth-metrics+pixmap metrics pixmap)
              (benchmark-progn (fontsloth-font-rasterize font glyph-id px)))
             (pgm (fontsloth-raster-npbm pixmap
                                         (fontsloth-metrics-width metrics)
                                         (fontsloth-metrics-height metrics)
                                         'pgm))
             (buffer (get-buffer-create "fontsloth-raster-preview")))
  (with-current-buffer buffer
    (set-buffer-multibyte nil)
    (insert pgm)))

;; note that fontsloth-raster-npbm is unnecessary if you just want a pixmap

Layout some text

(require 'fontsloth-layout)

;; this will return a sequence of glyph position structs
(let ((font (fontsloth-load-font "/usr/share/fonts/TTF/AppleGaramond.ttf"))
      (x-start 0)
      (layout (fontsloth-layout-create)))
  (fontsloth-layout-reset layout (fontsloth-layout-settings-create
                                  :x x-start))
  (fontsloth-layout-append layout `(,font) (fontsloth-layout-text-style-create
                                            :text "Hello world!"
                                            :px 35.0 :font-index 0))
  (fontsloth-layout-finalize layout))

Attribution

Fontsloth at this stage wouldn’t at all be possible without fontdue and ttf-parser. In addition I began learning about TTF from TTF file parsing.

Performance

Update 11/18/21: Some initial profiling indicates unsurprisingly that the slowest aspects of this are loading and particularly outlining all glyphs and that time spent in garbage collection is ~50%. It may be possible to mitigate this by adding an option for lazy outlining. As for loading, I plan to try out having bindat work with streams.

How slow is it really? The short answer is I don’t know yet as benchmarking is still a TODO.

Anecdotally, on Thinkpad t440 with Emacs 28 native:

  • Glyph rasters for pixel sizes around 30.0 take on the order of a few milliseconds
  • To load a font and outline all of its glyphs at present takes longer (e.g. ~320 milliseconds on the same machine for AppleGaramond TTF), hence the font cache
  • Layout for short text strings takes sub 1 millisecond with the same setup

Future plans

I would like for this to be robust enough to handle everything that Emacs currently delegates to FreeType/Harfbuzz/Cairo, not necessarily for actual inclusion in Emacs proper, but as a good acid test if it could accomplish that without any loss of functionality.

Assuming feature parity with FreeType, I would like to port this to Guile 3 so that it could be an option for handling fonts in Guile Emacs. The Guile version would be a rewrite with requirements for being idiomatic and thread-safe.

Fonts tested so far (working for simple and composite glyphs)

In order of most to least tested:

  • free version of FontAwesome 5
  • IBMPlex series, the TTF and OTF versions
  • Bookerly TTF
  • all-the-icons TTF
  • AppleGaramond TTF
  • Roboto series, the TTF versions
  • DejaVu series, the TTF versions
  • Charter OTF (for some reason line height metrics are off for this specific font)
  • SF-Pro (apple’s flagship afaict) OTF (this is an enormous font that takes about 20 seconds to load the first time but it does work)
  • Hermit OTF
  • Fantasque series OTF
  • Cascadia series OTF

https://i.creativecommons.org/l/by-nc-sa/4.0/88x31.png This documentation is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.

Copyright (C) 2021 Jo Gay <jo.gay@mailfence.com>

About

inspired by fontdue, an OTF/TTF font loader and renderer written in pure elisp

Resources

License

Stars

Watchers

Forks

Packages

No packages published