Packages

Elixir binding for Harfbuzz using Rustybuzz as NIF, with RustlerPrecompiled

Current section

Files

Jump to
harfbuzz_ex lib harfbuzz_ex.ex
Raw

lib/harfbuzz_ex.ex

defmodule HarfbuzzEx do
@moduledoc """
Elixir wrapper for Rust `rustybuzz` crate for OpenType shaping.
If I/O and memory are not concerns (the font file is small, or the task infrequent), use `get/3` to access data from the shaped glyphs directly. This creates a shaper on a one-off basis.
If the font file-size is large and shaping is done frequently, explicitly use the `new/1` to load the font into memory. Shaping with `shape/2` is then CPU bound. Use `stop/1` to release the memory.
"""
use GenServer
alias HarfbuzzEx.Native
@doc """
Input a string, shape with the font, and return a list of shaped glyphs.
Use `:all` for a list of `%HarfbuzzEx.Glyph{}` structs, or one of `:name`, `:x_advance`, `:y_advance`, `:x_offset`, `:y_offset` for a list of string (`:name`) or integers.
"""
def get!(font_path, string, data \\ :name) when data in [:name, :x_advance, :y_advance, :x_offset, :y_offset, :all] do
{:ok, shaper} = new(font_path)
{:ok, results} = shape(shaper, string)
stop(shaper)
case data do
:all -> results
_ -> results |> Enum.map(&Map.get(&1, data))
end
end
def get(font_path, string, data \\ :name) when data in [:name, :x_advance, :y_advance, :x_offset, :y_offset, :all] do
{:ok, get!(font_path, string, data)}
end
# --- Client API ---
@doc """
Starts a new Rust instance for the given font, held in a GenServer.
Returns `{:ok, pid}` on success.
"""
def new(font_path, opts \\ []) do
GenServer.start_link(__MODULE__, font_path, opts)
end
@doc """
Same as `new/2` but raises on error.
"""
def new!(font_path, opts \\ []) do
{:ok, pid} = new(font_path, opts)
pid
end
@doc """
Shapes text using the given Shaper process, returning the full set of shaped instructions as a map of:
* `:name` (glyph name)
* `:x_advance`
* `:y_advance`
* `:x_offset`
* `:y_offset`
"""
def shape(pid, text) do
GenServer.call(pid, {:shape, text})
end
@doc """
Same as `shape/2` but raises on error.
"""
def shape!(pid, text) do
{:ok, results} = shape(pid, text)
results
end
@doc """
Shapes text using the given Shaper process, returning *only* the requested parameter as a list. This is most useful for extracting a list of glyphnames by invoking `shape!(pid, text, :name)`.
"""
def shape!(pid, text, data) when data in [:name, :x_advance, :y_advance, :x_offset, :y_offset] do
shape!(pid, text)
|> Enum.map(&Map.get(&1, data))
end
@doc """
Same as `shape!/3` but returns an `:ok` tuple.
"""
def shape(pid, text, data) when data in [:name, :x_advance, :y_advance, :x_offset, :y_offset] do
{:ok, shape!(pid, text, data)}
end
@doc """
Stops the Shaper process.
This drops the reference to the Rust resource, allowing the BEAM
to garbage collect the memory (font data + parsed tables).
"""
def stop(pid) do
GenServer.stop(pid)
end
# --- Server Callbacks ---
@impl true
def init(font_path) do
# Load the heavy resource once during process initialization
case Native.shaper_new(font_path) do
{:error, reason} -> {:stop, reason}
resource -> {:ok, resource}
end
end
@impl true
def handle_call({:shape, text}, _from, resource) do
# The resource is kept in the loop state
result = Native.shaper_shape(resource, text)
{:reply, {:ok, result}, resource}
end
end