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.
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.
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.
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.
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)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.
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")
#> pages: 4
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.
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)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.
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.
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.
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")
#> Courier '12345' at 10pt = 30 pt
cat("overlapping text boxes =", nrow(sp_overlaps(doc)), "\n")
#> overlapping text boxes = 0
sp_close(doc)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.
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)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():
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.