---
title: "simPDF: Fast Multi-Page PDF Reports"
author: "Kyun-Seop Bae"
output: rmarkdown::html_vignette
vignette: >
  %\VignetteIndexEntry{simPDF: Fast Multi-Page PDF Reports}
  %\VignetteEngine{knitr::rmarkdown}
  %\VignetteEncoding{UTF-8}
---

```{r setup, include = FALSE}
knitr::opts_chunk$set(collapse = TRUE, comment = "#>")
library(simPDF)
out <- function() tempfile(fileext = ".pdf")   # examples write to a temp file
```

## Why simPDF

`simPDF` builds multi-page PDF **reports** on top of R's built-in graphics
device (`pdf()` / `cairo_pdf()`). It is deliberately small: base R only, no
`knitr`/`LaTeX`/`pandoc` toolchain, so a report is written in a single fast pass.

Its defining idea is **measured layout**. Every block of content reports its
real width and height (via `strwidth()`/`strheight()` on the open device); the
vertical cursor advances by that measured height; and pages break automatically.
This removes the text/table overlap that plagues reports built with
hand-computed row/column coordinates -- content never collides no matter how many
parameters, rows, or random effects it has.

Interactive fillable forms (AcroForm CRFs) are out of scope and handled by the
sibling package `pdfCRF`; `simPDF` is for reports and figures.

## The mental model

A report is a **document** (`sp_new()`), a rectangular content **frame**
(`frame_set()`), and a list of **blocks** run through the **flow engine**
(`flow_run()`). Coordinates are PDF points (1/72 inch); `doc$W`/`doc$H` are the
page width/height.

```{r quickstart}
doc <- sp_new(out(), paper = "letter")
flow_run(doc, list(
  block_para("Hello, simPDF", size = 16, font = 2),   # font: 1 plain, 2 bold, 3 italic
  block_rule(),
  block_para("This paragraph is measured and wraps to the frame width automatically.
              Add as much text as you like; the engine breaks lines and pages for you.")
))
sp_close(doc)
```

`flow_run()` measures each block, draws it if it fits, otherwise starts a new
page. `NULL` entries are dropped, so conditional blocks
(`if (cond) block_*()`) are fine.

## Text blocks

`block_para()` word-wraps and aligns prose. `block_pre()` renders a character
vector one physical line each -- ideal for `capture.output()` dumps -- and
auto-shrinks the font so the widest line still fits the frame width.

```{r text}
doc <- sp_new(out())
flow_run(doc, list(
  block_para("Left / centre / right alignment", size = 12, font = 2),
  block_para("centred", align = "center"),
  block_para("right",   align = "right"),
  block_para("A fixed-point summary printed verbatim:", size = 11, font = 2),
  block_pre(capture.output(summary(1:100)))
))
sp_close(doc)
```

## Tables and matrices

`block_table()` renders a data.frame/matrix with **column widths measured from
the content**, numeric columns right-aligned, a header that **repeats on every
page**, and automatic **row-boundary page splitting**. Row names become a
leading label column.

```{r table}
big <- data.frame(ID = 1:200, TIME = round(runif(200, 0, 24), 2),
                  DV = round(rlnorm(200), 3), WT = round(runif(200, 50, 90), 1))
doc <- sp_new(out())
frame_set(doc, top = doc$H - 45, bottom = 45, left = 45, right = doc$W - 45)
flow_run(doc, list(block_para("Input data", 13, font = 2), block_table(big, size = 9)))
cat("pages:", doc$page_no, "\n")
sp_close(doc)
```

`block_matrix()` handles a **wide** matrix by splitting its columns into groups
that each fit the page (row labels repeated in every group). This is what fixes
the classic "print a 14x14 covariance matrix" overlap.

```{r matrix}
M <- matrix(round(rnorm(144, 0, .4), 3), 12, 12,
            dimnames = list(paste0("Eta", 1:12), paste0("Eta", 1:12)))
doc <- sp_new(out()); frame_set(doc, top = doc$H-45, bottom = 45, left = 45, right = doc$W-45)
flow_run(doc, list(block_para("Omega matrix (12 x 12)", 13, font = 2),
                   block_matrix(M, size = 9)))
sp_close(doc)
```

## Plots and full-page figures

`block_plot()` draws one base-R plot into a reserved band **inline** with the
text (the expression is captured unevaluated and run at draw time), so a
heading, a plot, and a table can share a page and paginate together.

```{r plot}
set.seed(1); pred <- rlnorm(80); dv <- pred * exp(rnorm(80, 0, .3))
doc <- sp_new(out()); frame_set(doc, top = doc$H-45, bottom = 45, left = 45, right = doc$W-45)
flow_run(doc, list(
  block_keep(list(
    block_para("Goodness of fit", 12, font = 2),
    block_plot({ plot(pred, dv); abline(0, 1, lty = 3) }, height = 220)))
))
sp_close(doc)
```

For a **whole page** of panels (e.g. one diagnostic grid per subject), use
`sp_figure_page()`, which manages the page outside the flow engine. Draw exactly
`prod(mfrow)` panels per call.

```{r figpage}
doc <- sp_new(out())
for (s in 1:3)
  sp_figure_page(doc, { for (i in 1:6) plot(rnorm(30), rnorm(30), main = paste("Subj", s)) },
                 mfrow = c(2, 3))
cat("pages:", doc$page_no, "\n")
sp_close(doc)
```

## Structure: keep-together, spacers, rules, headers/footers

`block_keep()` keeps a group on one page (a heading never lands alone at the
bottom); `block_spacer(pts)` and `block_rule()` add vertical space and lines.
Running headers/footers are stamped on every page.

```{r structure}
doc <- sp_new(out()); frame_set(doc, top = doc$H-55, bottom = 45, left = 45, right = doc$W-45)
flow_run(doc,
  blocks = list(block_para("Report body", 12), block_spacer(10),
                block_para(paste(rep("More text.", 40), collapse = " "))),
  footer = block_para("CONFIDENTIAL", size = 8, align = "center"))
sp_close(doc)
```

## Measurement and quality checks

`sp_width()` / `sp_height()` expose the exact metrics the engine uses (AFM-exact
for the core fonts). The tracer records every drawn text box so you can assert
that a report has **zero overlaps** -- handy in package tests.

```{r qa}
doc <- sp_new(out()); frame_set(doc, top = doc$H-45, bottom = 45, left = 45, right = doc$W-45)
sp_trace(doc, TRUE)
flow_run(doc, list(block_para("Measured & overlap-free", 14, font = 2),
                   block_table(head(mtcars))))
cat("Courier '12345' at 10pt =", sp_width(doc, "12345", size = 10, family = "mono"), "pt\n")
cat("overlapping text boxes  =", nrow(sp_overlaps(doc)), "\n")
sp_close(doc)
```

## Authorship and signatures

`block_authorship()` adds a "Prepared by" block; `block_signature()` draws
visible approval lines and records their positions. `sp_add_sig_fields()` then
inserts **interactive Adobe Acrobat signature fields** (`/Sig`) over those lines
via a pure base-R incremental update -- the finished PDF is one-click signable in
the free Acrobat Reader.

```{r signature}
f <- out()
doc <- sp_new(f); frame_set(doc, top = doc$H-50, bottom = 60, left = 54, right = doc$W-54)
flow_run(doc, list(
  block_para("Validation Report", 16, font = 2), block_rule(),
  block_authorship("Kyun-Seop Bae", role = "Prepared by",
                   affiliation = "Asan Medical Center", date = "2026-07-11"),
  block_spacer(30), block_para("Approvals", 12, font = 2),
  block_signature(c("Performed by", "Reviewed by"))
))
sp_close(doc)
sp_add_sig_fields(f, doc)          # add clickable /Sig fields over the drawn lines
```

## Model flow diagrams

`block_flow_diagram()` lays out a tree of nodes (e.g. NONMEM model runs) as boxes
sized to their text, tidily arranged and **scaled to fit** -- so more models
shrink the diagram instead of cluttering it.

```{r flowdiagram}
nodes <- data.frame(
  id = c("001","002","003","004","005"),
  parent = c("", "001", "001", "002", "002"),
  label = c("Run 001\nOFV 1000", "Run 002\nOFV 980", "Run 003\nOFV 975",
            "Run 004\nOFV 960", "Run 005\nOFV 955"),
  stringsAsFactors = FALSE)
doc <- sp_new(out()); frame_set(doc, top = doc$H-45, bottom = 45, left = 40, right = doc$W-40)
flow_run(doc, list(block_para("Model development flow", 14, font = 2),
                   block_flow_diagram(nodes, height = 400)))
sp_close(doc)
```

## Using simPDF from your own package

The typical pattern (as used by `nmw`): keep your parsing/statistics, and replace
the PDF-generation with a `blocks` list plus one `flow_run()`, wrapping any
diagnostic plots in `block_plot()` / `sp_figure_page()`:

```r
my_report <- function(x, file = "report.pdf") {
  # ... compute tables/values from x ...
  doc <- sp_new(file, paper = "letter", family = "Courier")
  frame_set(doc, top = doc$H - 45, bottom = 45, left = 45, right = doc$W - 45)
  flow_run(doc, list(
    block_para("Summary", size = 16, font = 2), block_rule(),
    block_table(my_table),
    block_para("Diagnostics", size = 12, font = 2),
    block_plot(plot(my_x, my_y), height = 220)
  ), footer = block_para("CONFIDENTIAL", size = 8, align = "center"))
  sp_close(doc)
}
```

Add `simPDF` to your `Imports`. Because `simPDF` uses only the graphics device,
your diagnostic plots keep working unchanged -- they are simply placed by the
flow engine instead of by hand-computed coordinates, and never overlap.
