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