Packages

Analyze, transform, and render Figma files from Elixir

Current section

Files

Jump to
figler lib figler.ex
Raw

lib/figler.ex

defmodule Figler do
@moduledoc """
Fast analysis, transformation, and rendering for Figma `.fig` files.
Use `Figler.Scene` to inspect large files, `decode/1` and `encode/1` to change
complete documents with ordinary Elixir data operations, and `Figler.Render`
to render pages or selected layers.
Lower-level schema and message codecs are available for tools that already
manage their own Figma archive or container boundary.
"""
alias Figler.Document
alias Figler.Error
alias Figler.Native.CodecNifs
alias Figler.Schema.Message
@schema_path :figler |> :code.priv_dir() |> Path.join("schema/fig.kiwi")
@external_resource @schema_path
@schema_version @schema_path |> File.read!() |> :erlang.md5()
@schema_cache_key {__MODULE__, :schema}
@prepared_schema_cache_key {__MODULE__, :prepared_schema}
@doc """
Decodes a complete `.fig` archive, `fig-kiwi` container, or raw message.
The returned `Figler.Document` contains the decoded message alongside every
preserved archive entry. It can be changed with ordinary Elixir struct,
`Enum`, and `Access` operations before being passed to `encode/1`.
"""
@spec decode(binary()) :: {:ok, Document.t()} | {:error, Error.t()}
def decode(input) when is_binary(input) do
{:ok, decode!(input)}
rescue
error in Error -> {:error, error}
end
@doc "Like `decode/1`, but raises `Figler.Error` for malformed input."
@spec decode!(binary()) :: Document.t()
def decode!(input) when is_binary(input) do
document = Document.open!(input)
message =
Error.protect(:decode_document, fn ->
KiwiCodec.decode(document.payload, Message)
end)
%{document | message: message}
end
@doc """
Encodes a `Figler.Document` back to its original representation.
Archives retain all entries and replace only their `canvas.fig` container.
Containers retain their schema, version, compression family, and trailing
chunks. Documents decoded from raw messages encode back to raw messages.
"""
@spec encode(Document.t()) :: {:ok, binary()} | {:error, Error.t()}
def encode(%Document{} = document) do
{:ok, encode!(document)}
rescue
error in Error -> {:error, error}
end
@doc "Like `encode/1`, but raises `Figler.Error` for invalid document data."
@spec encode!(Document.t()) :: binary()
def encode!(%Document{} = document) do
Error.protect(:encode_document, fn ->
payload = encode_document_message!(document)
Document.encode_payload!(document, payload)
end)
end
@doc """
Returns the vendored Figma Kiwi schema text.
"""
@spec schema_text() :: String.t()
def schema_text do
File.read!(@schema_path)
end
@doc """
Parses the vendored Figma Kiwi schema.
"""
@spec schema() :: KiwiCodec.Schema.t()
def schema do
cached(@schema_cache_key, fn -> schema_text() |> KiwiCodec.parse_schema!() end)
end
@doc """
Decodes a Figma `Message` payload into generated schema structs.
"""
@spec decode_message(binary()) :: Figler.Schema.Message.t()
def decode_message(binary) when is_binary(binary) do
Error.protect(:decode_message, fn -> KiwiCodec.decode(binary, Figler.Schema.Message) end)
end
@doc """
Decodes a Figma `Message` payload into sparse schema-backed maps.
Sparse maps include `:__kiwi_module__` and only fields present on the wire.
This avoids materializing hundreds of nil fields per large `NodeChange`.
"""
@spec decode_sparse_message(binary()) :: map()
def decode_sparse_message(binary) when is_binary(binary) do
Error.protect(:decode_sparse_message, fn -> CodecNifs.decode_sparse_message(binary) end)
end
@doc """
Encodes a generated Figma `Message` struct.
"""
@spec encode_message(Figler.Schema.Message.t()) :: binary()
def encode_message(%Figler.Schema.Message{} = message) do
KiwiCodec.encode(message)
end
@doc """
Decodes a Figma `Message` payload with the runtime schema interpreter.
"""
@spec decode_message_runtime(binary()) :: map()
def decode_message_runtime(binary) when is_binary(binary) do
Error.protect(:decode_message_runtime, fn ->
prepared_schema()
|> KiwiCodec.SchemaInterpreter.decode("Message", binary)
end)
end
@doc """
Encodes a Figma `Message` map with the runtime schema interpreter.
"""
@spec encode_message_runtime(map()) :: binary()
def encode_message_runtime(message) when is_map(message) do
prepared_schema()
|> KiwiCodec.SchemaInterpreter.encode("Message", message)
end
defp encode_document_message!(%Document{message: nil, payload: payload})
when is_binary(payload),
do: payload
defp encode_document_message!(%Document{message: %Message{} = message}),
do: KiwiCodec.encode(message)
defp encode_document_message!(%Document{}) do
raise Error.new(:invalid_document, :encode_document, %{reason: :invalid_message})
end
defp prepared_schema do
cached(@prepared_schema_cache_key, fn -> KiwiCodec.SchemaInterpreter.prepare(schema()) end)
end
defp cached(key, build) do
case :persistent_term.get(key, :missing) do
{@schema_version, value} ->
value
_missing_or_stale ->
value = build.()
:persistent_term.put(key, {@schema_version, value})
value
end
end
end