Current section
Files
Jump to
Current section
Files
lib/pdf/layout/reflow.ex
defmodule Pdf.Layout.Reflow do
@moduledoc false
alias Pdf.Layout.Flow
@default_gap 4
@default_row_threshold 12
@doc """
Measure total inner height for reflow layout.
## Options
- `:gap` — vertical space between rows (default `4`)
- `:row_threshold` — band size in points to group children on the same row (default `12`)
"""
def measure(children, inner_width, doc \\ nil, opts \\ []) do
gap = Keyword.get(opts, :gap, @default_gap)
threshold = Keyword.get(opts, :row_threshold, @default_row_threshold)
children
|> List.flatten()
|> to_occupancies(inner_width, doc)
|> cluster_rows(threshold)
|> layout_rows(gap)
|> elem(1)
end
@doc false
def render(doc, area, style, children, render_child) when is_function(render_child, 3) do
gap = Map.get(style, :gap, @default_gap)
threshold = Map.get(style, :row_threshold, @default_row_threshold)
{doc, _height} =
children
|> List.flatten()
|> to_occupancies(area.width, doc)
|> cluster_rows(threshold)
|> Enum.reduce({doc, 0}, fn row, {doc, cursor} ->
{row_h, placements} = place_row(row, cursor, gap)
doc =
Enum.reduce(placements, doc, fn {child, x, top, width}, doc ->
child_area = %{
area
| x: area.x + x,
y: area.y - top,
width: width
}
render_placed_child(doc, child, child_area, render_child)
end)
{doc, cursor + row_h + gap}
end)
doc
end
defp layout_rows(rows, gap) do
Enum.reduce(rows, {[], 0}, fn row, {acc, cursor} ->
{row_h, _placements} = place_row(row, cursor, gap)
{acc ++ [{row, row_h}], cursor + row_h + gap}
end)
end
defp place_row(row, cursor, gap) do
spacing = if cursor == 0, do: 0, else: gap
preferred_top = row |> Enum.map(& &1.pref_top) |> Enum.min()
actual_top = max(preferred_top, cursor + spacing)
row_h = row |> Enum.map(& &1.height) |> Enum.max()
placements =
Enum.map(row, fn occ ->
{occ.child, occ.x, actual_top, occ.width}
end)
{row_h, placements}
end
defp cluster_rows(occupancies, threshold) do
occupancies
|> Enum.sort_by(& &1.pref_top)
|> Enum.group_by(&row_band(&1.pref_top, threshold))
|> Enum.sort_by(fn {band, _} -> band end)
|> Enum.map(fn {_band, row} -> row end)
end
defp row_band(pref_top, threshold) do
div(pref_top, max(threshold, 1))
end
defp to_occupancies(children, inner_width, doc) do
Enum.map(children, fn child ->
{x, y} = child_position(child)
pref_top = abs(y)
width = child_width(child, inner_width, x)
height = child_height(child, width, doc)
%{child: child, x: x, pref_top: pref_top, width: width, height: height}
end)
|> Enum.reject(&skip_child?/1)
end
defp skip_child?(%{child: child}), do: Map.get(child, :position) == :absolute
defp render_placed_child(doc, child, area, render_child) do
case child do
%{type: :text, props: props} ->
style =
props
|> Map.get(:style, %{})
|> Map.put(:position, {0, 0})
|> Map.put(:break_text, true)
|> Map.put(:wrap_width, area.width)
child
|> put_in([:props, :style], style)
|> then(&render_child.(doc, &1, area))
%{text: _} = map_child ->
map_child
|> Map.put(:x, 0)
|> Map.put(:y, 0)
|> Map.put(:break_text, true)
|> Map.put(:wrap_width, area.width)
|> then(&render_child.(doc, &1, area))
_ ->
child
|> place_child()
|> then(&render_child.(doc, &1, area))
end
end
defp place_child(%{type: _, props: props} = child) do
style = Map.get(props, :style, %{}) |> Map.put(:position, {0, 0})
put_in(child, [:props, :style], style)
end
defp place_child(child), do: child |> Map.put(:x, 0) |> Map.put(:y, 0)
defp child_width(child, inner_width, x) do
case child do
%{type: :key_value, props: props} ->
style = Map.get(props, :style, %{})
resolve_width(Map.get(style, :width, inner_width), inner_width)
%{key_value: _} = map_child ->
style = Map.drop(map_child, [:key_value, :pairs])
resolve_width(Map.get(style, :width, inner_width), inner_width)
_ ->
max(inner_width - x, 1)
end
end
defp child_height(%{type: :text, props: props}, width, doc) do
Flow.measure_text(Map.get(props, :content, ""), Map.get(props, :style, %{}), width, doc)
end
defp child_height(%{text: text} = child, width, doc) do
style = Map.drop(child, [:text, :x, :y, :position])
Flow.measure_text(text, style, width, doc)
end
defp child_height(%{type: :key_value, props: props}, width, _doc) do
style = Map.get(props, :style, %{})
{x, _} = Map.get(style, :position, {0, 0})
x_off = if is_number(x), do: abs(trunc(x)), else: 0
Pdf.Component.KeyValue.measure_height(style, Map.get(props, :pairs, []), width, x_offset: x_off)
end
defp child_height(%{key_value: {x, _}, pairs: pairs} = child, width, _doc) do
style = Map.drop(child, [:key_value, :pairs])
x_off = if is_number(x), do: abs(trunc(x)), else: 0
Pdf.Component.KeyValue.measure_height(style, pairs, width, x_offset: x_off)
end
defp child_height(%{type: :avatar, props: props}, _width, _doc) do
{_, h} = normalize_size(Map.get(Map.get(props, :style, %{}), :size, 40))
h
end
defp child_height(%{avatar: _} = child, _width, _doc) do
style = Map.drop(child, [:avatar])
{_, h} = normalize_size(Map.get(style, :size, 40))
h
end
defp child_height(%{type: :chip, props: props}, _width, _doc) do
Map.get(Map.get(props, :style, %{}), :height, 24)
end
defp child_height(%{type: :line_segment}, _width, _doc), do: 1
defp child_height(%{spacer: amount}, _width, _doc), do: amount
defp child_height(_, _width, _doc), do: 0
defp child_position(%{type: :line_segment, props: props}) do
style = Map.get(props, :style, %{})
case Map.get(style, :from, {0, 0}) do
{x, y} -> {x, y}
end
end
defp child_position(%{type: _, props: props}) do
Map.get(Map.get(props, :style, %{}), :position, {0, 0})
end
defp child_position(%{text: _} = child), do: {Map.get(child, :x, 0), Map.get(child, :y, 0)}
defp child_position(%{key_value: {x, y}}), do: {x, y}
defp child_position(%{avatar: {x, y}}), do: {x, y}
defp child_position(%{chip: {x, y}}), do: {x, y}
defp child_position(_), do: {0, 0}
defp normalize_size(size) when is_number(size), do: {size, size}
defp normalize_size({w, h}), do: {w, h}
defp resolve_width(:full, parent), do: parent
defp resolve_width(value, parent), do: Pdf.Dimension.resolve(value, parent)
end