Skip to main content

lib/pdf/component/page_header.ex

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