lib/pdf/dimension.ex
defmodule Pdf.Dimension do
@moduledoc """
Resolves relative dimensions against a parent reference.
Supports:
- `:full` — 100% of the parent dimension
- `"N%"` — percentage of the parent dimension (e.g. `"50%"`)
- `number` — absolute value (pass-through)
## Examples
iex> Pdf.Dimension.resolve(:full, 400)
400
iex> Pdf.Dimension.resolve("50%", 400)
200.0
iex> Pdf.Dimension.resolve(200, 400)
200
"""
@doc """
Resolve a single dimension value against a parent dimension.
"""
@spec resolve(value :: number | :full | String.t(), parent_dim :: number) :: number
def resolve(:full, parent_dim), do: parent_dim
def resolve(pct, parent_dim) when is_binary(pct) do
case Float.parse(String.trim_trailing(pct, "%")) do
{n, ""} -> parent_dim * (n / 100)
_ -> raise ArgumentError, "invalid percentage: #{inspect(pct)}"
end
end
def resolve(value, _parent_dim) when is_number(value), do: value
@doc """
Resolve a `{w, h}` size tuple against a parent area `%{width:, height:}`.
Returns `{resolved_w, resolved_h}`.
"""
@spec resolve_size({any, any}, %{width: number, height: number}) :: {number, number}
def resolve_size({w, h}, %{width: pw, height: ph}) do
{resolve(w, pw), resolve(h, ph)}
end
@doc """
Returns `true` if the value is a relative dimension (`:full` or `"N%"`).
"""
@spec relative?(any) :: boolean
def relative?(:full), do: true
def relative?(v) when is_binary(v), do: String.ends_with?(v, "%")
def relative?(_), do: false
@doc """
Returns `true` if either dimension in the size tuple is relative.
"""
@spec needs_resolution?({any, any}) :: boolean
def needs_resolution?({w, h}), do: relative?(w) or relative?(h)
end