defmodule Pdf.Component.PageHeader do
@moduledoc """
Automatic page header component for PDF documents.
Registers a header template that renders on every page using
`Pdf.on_page(:header, ...)`. Combine with `Pdf.Component.Paginator`
for full header + footer support.
## Examples
doc
|> Pdf.Component.PageHeader.apply(%{
title: "Monthly Report",
subtitle: "Generated by ExPDF",
color: {0.1, 0.1, 0.1}
})
doc
|> Pdf.Component.PageHeader.apply(%{
left: "Acme Corp",
right: :date,
line: true,
line_color: {0.2, 0.5, 0.8}
})
"""
@default_font "Helvetica"
@default_font_size 9
@default_color {0.3, 0.3, 0.3}
@default_line_color {0.75, 0.75, 0.75}
@default_margin_top 30
@doc """
Apply an automatic header to the document.
This registers a header template — all subsequent pages will
have the header rendered automatically.
## Style options
- `:title` — centered title text
- `:left` — left-aligned text (overrides centered title)
- `:right` — right-aligned text, or `:date` for auto date, or `:page` for page number
- `:subtitle` — smaller text below title (only with `:title`)
- `:font` — font name (default `"Helvetica"`)
- `:font_size` — text size (default `9`)
- `:color` — text color (default dark gray)
- `:margin_top` — distance from page top (default `30`)
- `:line` — draw a horizontal line below header (default `true`)
- `:line_color` — line color (default light gray)
- `:line_width` — line stroke width (default `0.75`)
- `:skip_first` — skip header on first page (default `false`)
"""
def apply(doc, style \\ %{}) do
title = Map.get(style, :title)
left = Map.get(style, :left)
right = Map.get(style, :right)
subtitle = Map.get(style, :subtitle)
font = Map.get(style, :font, @default_font)
font_size = Map.get(style, :font_size, @default_font_size)
color = Map.get(style, :color, @default_color)
margin_top = Map.get(style, :margin_top, @default_margin_top)
show_line = Map.get(style, :line, true)
line_color = Map.get(style, :line_color, @default_line_color)
line_width = Map.get(style, :line_width, 0.75)
skip_first = Map.get(style, :skip_first, false)
Pdf.on_page(doc, :header, fn d, info ->
if skip_first and info.number == 1 do
d
else
%{width: pw, height: ph} = Pdf.size(d)
text_y = ph - margin_top
d = d |> Pdf.set_font(font, font_size) |> Pdf.set_fill_color(color)
d = cond do
# Two-column layout: left + right
left != nil ->
right_text = resolve_right(right, info)
d = Pdf.text_at(d, {40, text_y}, to_string(left))
if right_text do
rw = String.length(right_text) * font_size * 0.52
Pdf.text_at(d, {pw - 40 - rw, text_y}, right_text)
else
d
end
# Centered title
title != nil ->
tw = String.length(title) * font_size * 0.52
d = Pdf.text_at(d, {(pw - tw) / 2, text_y}, title)
d = if subtitle do
sub_size = max(font_size - 2, 6)
sw = String.length(subtitle) * sub_size * 0.52
d
|> Pdf.set_font(font, sub_size)
|> Pdf.set_fill_color(lighten(color, 0.3))
|> Pdf.text_at({(pw - sw) / 2, text_y - font_size - 2}, subtitle)
else
d
end
# Right side (date/page) even with centered title
right_text = resolve_right(right, info)
if right_text do
rw = String.length(right_text) * font_size * 0.52
d
|> Pdf.set_font(font, font_size)
|> Pdf.set_fill_color(lighten(color, 0.2))
|> Pdf.text_at({pw - 40 - rw, text_y}, right_text)
else
d
end
true ->
d
end
# Horizontal line
if show_line do
line_y = text_y - font_size - 4
line_y = if subtitle, do: line_y - font_size, else: line_y
d
|> Pdf.save_state()
|> Pdf.set_stroke_color(line_color)
|> Pdf.set_line_width(line_width)
|> Pdf.line({40, line_y}, {pw - 40, line_y})
|> Pdf.stroke()
|> Pdf.restore_state()
else
d
end
end
end)
end
defp resolve_right(nil, _info), do: nil
defp resolve_right(:date, _info) do
{{y, m, d}, _} = :calendar.local_time()
"#{pad(m)}/#{pad(d)}/#{y}"
end
defp resolve_right(:page, info), do: "Page #{info.number}"
defp resolve_right(text, _info) when is_binary(text), do: text
defp resolve_right(_, _), do: nil
defp pad(n) when n < 10, do: "0#{n}"
defp pad(n), do: "#{n}"
defp lighten({r, g, b}, amount) do
{r + (1 - r) * amount, g + (1 - g) * amount, b + (1 - b) * amount}
end
end