# Exporting Clinical Tables with derrick

## Overview

`derrick` converts analysis-ready clinical tables into files written by `reporter`. It accepts either a `gtsummary` object or an already assembled plain data frame, then applies report metadata, column labels, spanning headers, widths, pagination controls, and output-format selection in one call to `derrick::gtsummary_reporter()`.

## Relationship to reporter

`derrick` uses `reporter` as the rendering engine. The examples in this vignette continue to rely on `reporter` for:

- report setup, including paper size, orientation, units, margins, font, and font size;
- table rendering, including column definitions, column widths, alignment, borders, header styling, and spanning headers;
- titles, footnotes, page footers, automatic pagination, and file writing;
- output writers for RTF, TXT, DOCX, PDF, and HTML.

The main difference is the level of abstraction. With `reporter`, the caller usually builds the report and table specification directly. `derrick` builds that specification from a `gtsummary` object or a plain data frame, then passes it to `reporter`.

`derrick` adds a clinical-table workflow layer around `reporter`:

- it reads displayed columns, labels, indentation, p-values, and spanning headers from `gtsummary` objects;
- it lets plain data frames use the same export path by supplying `column_labels` and `spanning_headers`;
- it centralizes clinical report metadata through `title1`, `footnote1`, `progname`, and related numbered variables;
- it adds table-width controls such as `max_table_width`, `column_widths`, and `max_chars_per_line`;
- it can write several output formats from the same table specification and optionally save the processed data used for reporting.

A typical workflow is:

1. Build or assemble a table.
2. Define report metadata such as `title1`, `footnote1`, and `progname`.
3. Call `derrick::gtsummary_reporter()` with the table, output path, layout arguments, and requested `output_types`.
4. Inspect the generated files and add manual widths or pagination only when needed.

The examples below write TXT and HTML outputs so the results can be displayed inside this HTML vignette. Use the same table specification with `output_types = c("RTF", "DOCX", "PDF")`, or any supported subset, when final delivery files are needed.

## Setup

    library(dplyr)
    library(gtsummary)
    library(derrick)

## Example 1: gtsummary summary input

The most common input is a `gtsummary` object. In this example, the analysis layer creates a demographic summary with a p-value column and a spanning header. `derrick` then exports the displayed table structure.

    demog_tbl <- gtsummary::trial |>
      dplyr::select(trt, age, grade) |>
      gtsummary::tbl_summary(by = trt) |>
      gtsummary::add_p() |>
      gtsummary::modify_spanning_header(
        gtsummary::all_stat_cols() ~ "**Treatment Group**"
      )

    demog_tbl

Characteristic | Treatment Group | p-value2
--- | --- | ---
Drug A N = 981 | Drug B N = 1021
Age | 46 (37, 60) | 48 (39, 56) | 0.7
Unknown | 7 | 4 |
Grade |  |  | 0.9
I | 35 (36%) | 33 (32%) |
II | 32 (33%) | 36 (35%) |
III | 31 (32%) | 33 (32%) |
1 Median (Q1, Q3); n (%)
2 Wilcoxon rank sum test; Pearson’s Chi-squared test

For `gtsummary` inputs, `derrick` reads displayed columns, column labels, indentation rules, p-values, and spanning headers from the object.

    preview_cols <- intersect(
      c("label", "stat_1", "stat_2", "p.value"),
      names(demog_tbl$table_body)
    )

    utils::head(demog_tbl$table_body[preview_cols])
    #> # A tibble: 6 × 4
    #>   label   stat_1      stat_2      p.value
    #>   <chr>   <chr>       <chr>         <dbl>
    #> 1 Age     46 (37, 60) 48 (39, 56)   0.718
    #> 2 Unknown 7           4            NA
    #> 3 Grade   <NA>        <NA>          0.871
    #> 4 I       35 (36%)    33 (32%)     NA
    #> 5 II      32 (33%)    36 (35%)     NA
    #> 6 III     31 (32%)    33 (32%)     NA

    title1 <- "Table 14.1.1 Summary of Demographics"
    title2 <- "Safety Population"
    footnote1 <- "Percentages are based on subjects in each treatment group."
    progname <- "programs/t-demog.R"

The `file_path` argument supplies the output stem. The actual formats written are controlled by `output_types`, and the function returns the generated file paths.

    demog_paths <- gtsummary_reporter(
      gts_obj = demog_tbl,
      file_path = file.path(output_dir, "t-demog.rtf"),
      output_types = c("TXT", "HTML"),
      save_rds = TRUE
    )

    output_manifest(demog_paths)
    #>   format         file bytes
    #> 1    TXT  t-demog.txt   856
    #> 2   HTML t-demog.html  3984

### TXT preview

TXT output is useful for checking fixed-width alignment, indentation, page titles, footnotes, and line wrapping.

    Table 14.1.1 Summary of Demographics
    Safety Population

    ______________________________________________
                       Treatment Group
                   _______________________
                     Drug A      Drug B
                     N = 98      N = 102
    Characteristic    N (%)       N (%)    p-value
    ______________________________________________

    Age            46 (37, 60) 48 (39, 56) 0.7183
        Unknown         7           4
    Grade                                  0.8708
        I           35 (36%)    33 (32%)
        II          32 (33%)    36 (35%)
        III         31 (32%)    33 (32%)
    ______________________________________________
    Percentages are based on subjects in each tr...

                                                                              programs/t-demog.R 03JUL2026 19:03

### HTML preview

HTML output is useful for a quick browser review of proportional-width table layout and header styling.

## Example 2: gtsummary cross-table input

Different `gtsummary` table types use the same export function. This example uses `tbl_cross()` to create a treatment-by-grade frequency table.

    cross_tbl <- gtsummary::trial |>
      dplyr::select(trt, grade) |>
      gtsummary::tbl_cross(
        row = grade,
        col = trt,
        percent = "row"
      ) |>
      gtsummary::modify_spanning_header(
        gtsummary::all_stat_cols() ~ "**Treatment Group**"
      )

    cross_tbl

| Treatment Group
--- | ---
Drug A | Drug B | Total
Grade |  |  |
I | 35 (51%) | 33 (49%) | 68 (100%)
II | 32 (47%) | 36 (53%) | 68 (100%)
III | 31 (48%) | 33 (52%) | 64 (100%)
Total | 98 (49%) | 102 (51%) | 200 (100%)

    title1 <- "Table 14.1.2 Grade by Treatment"
    title2 <- "Safety Population"
    footnote1 <- "Percentages are row percentages."
    progname <- "programs/t-grade-cross.R"

    cross_paths <- gtsummary_reporter(
      gts_obj = cross_tbl,
      file_path = file.path(output_dir, "t-grade-cross.rtf"),
      output_types = c("TXT", "HTML"),
      save_rds = FALSE
    )

    output_manifest(cross_paths)
    #>   format               file bytes
    #> 1    TXT  t-grade-cross.txt   674
    #> 2   HTML t-grade-cross.html  3640

### TXT preview

    Table 14.1.2 Grade by Treatment
    Safety Population

    _____________________________________
                   Treatment Group
            _____________________________
             Drug A   Drug B     Total
             N (%)     N (%)     N (%)
    _____________________________________

    Grade
        I   35 (51%) 33 (49%)  68 (100%)
        II  32 (47%) 36 (53%)  68 (100%)
        III 31 (48%) 33 (52%)  64 (100%)
    Total   98 (49%) 102 (51%) 200 (100%)
    _____________________________________
    Percentages are row percentages.

                                                                        programs/t-grade-cross.R 03JUL2026 19:03

### HTML preview

## Example 3: plain data frame input

Use a plain data frame when the table has already been assembled outside `gtsummary`, for example from a validated analysis data set or a custom summary program. In this mode, provide report labels and spanning headers explicitly, because there is no `gtsummary` styling metadata to read.

    ae_tbl <- data.frame(
      label = c(
        "Serious adverse events",
        "  Subjects with at least one event",
        "  Events leading to study drug interruption",
        "Treatment-emergent adverse events",
        "  Headache",
        "  Nausea",
        "  Alanine aminotransferase increased"
      ),
      placebo = c("", "2 (10.0%)", "1 (5.0%)", "", "6 (30.0%)", "4 (20.0%)", "1 (5.0%)"),
      active_low = c("", "1 (5.0%)", "0", "", "5 (25.0%)", "3 (15.0%)", "2 (10.0%)"),
      active_high = c("", "3 (15.0%)", "2 (10.0%)", "", "7 (35.0%)", "5 (25.0%)", "3 (15.0%)"),
      total = c("", "6 (10.0%)", "3 (5.0%)", "", "18 (30.0%)", "12 (20.0%)", "6 (10.0%)"),
      stringsAsFactors = FALSE
    )

    ae_tbl
    #>                                         label   placebo active_low active_high
    #> 1                      Serious adverse events
    #> 2            Subjects with at least one event 2 (10.0%)   1 (5.0%)   3 (15.0%)
    #> 3   Events leading to study drug interruption  1 (5.0%)          0   2 (10.0%)
    #> 4           Treatment-emergent adverse events
    #> 5                                    Headache 6 (30.0%)  5 (25.0%)   7 (35.0%)
    #> 6                                      Nausea 4 (20.0%)  3 (15.0%)   5 (25.0%)
    #> 7          Alanine aminotransferase increased  1 (5.0%)  2 (10.0%)   3 (15.0%)
    #>        total
    #> 1
    #> 2  6 (10.0%)
    #> 3   3 (5.0%)
    #> 4
    #> 5 18 (30.0%)
    #> 6 12 (20.0%)
    #> 7  6 (10.0%)

    title1 <- "Table 14.3.1 Overview of Adverse Events"
    title2 <- "Safety Population"
    footnote1 <- "Events are counted once per subject within each row."
    progname <- "programs/t-ae-overview.R"

    ae_paths <- gtsummary_reporter(
      gts_obj = ae_tbl,
      file_path = file.path(output_dir, "t-ae-overview.rtf"),
      column_labels = c(
        label = "System Organ Class / Preferred Term",
        placebo = "Placebo",
        active_low = "Active Low",
        active_high = "Active High",
        total = "Total"
      ),
      spanning_headers = data.frame(
        from = "placebo",
        to = "total",
        label = "Treatment Group"
      ),
      output_types = c("TXT", "HTML"),
      save_rds = FALSE
    )

    output_manifest(ae_paths)
    #>   format               file bytes
    #> 1    TXT  t-ae-overview.txt  1338
    #> 2   HTML t-ae-overview.html  4612

### TXT preview

    Table 14.3.1 Overview of Adverse Events
    Safety Population

    ____________________________________________________________________________________
                                                             Treatment Group
                                                ________________________________________
                                                           Active    Active
    System Organ Class / Preferred Term          Placebo     Low      High      Total
    ____________________________________________________________________________________

    Serious adverse events
      Subjects with at least one event          2 (10.0%) 1 (5.0%)  3 (15.0%) 6 (10.0%)
      Events leading to study drug interruption 1 (5.0%)      0     2 (10.0%)  3 (5.0%)
    Treatment-emergent adverse events
      Headache                                  6 (30.0%) 5 (25.0%) 7 (35.0%) 18 (30.0%)
      Nausea                                    4 (20.0%) 3 (15.0%) 5 (25.0%) 12 (20.0%)
      Alanine aminotransferase increased        1 (5.0%)  2 (10.0%) 3 (15.0%) 6 (10.0%)
    ____________________________________________________________________________________
    Events are counted once per subject within each row.

                                                                        programs/t-ae-overview.R 03JUL2026 19:03

### HTML preview

## Example 4: controlling column widths

The same table can look very different depending on the width controls. Start with reporter-managed automatic widths, then use `max_table_width` or `column_widths` when the report shell requires a specific layout.

### Automatic widths

With no width overrides, `derrick` passes no column widths to `reporter`. `reporter` then estimates the display columns from the target output format, font settings, headers, table content, and available page width.

    title1 <- "Table 14.3.1 Overview of Adverse Events"
    title2 <- "Automatic column widths"
    footnote1 <- "This version lets reporter estimate the width of each column."
    progname <- "programs/t-ae-overview.R"

    ae_auto_paths <- gtsummary_reporter(
      gts_obj = ae_tbl,
      file_path = file.path(output_dir, "t-ae-auto.rtf"),
      column_labels = c(
        label = "System Organ Class / Preferred Term",
        placebo = "Placebo",
        active_low = "Active Low",
        active_high = "Active High",
        total = "Total"
      ),
      spanning_headers = data.frame(
        from = "placebo",
        to = "total",
        label = "Treatment Group"
      ),
      output_types = "TXT",
      save_rds = FALSE
    )

    basename(ae_auto_paths)
    #> [1] "t-ae-auto.txt"

    Table 14.3.1 Overview of Adverse Events
    Automatic column widths

    ____________________________________________________________________________________
                                                             Treatment Group
                                                ________________________________________
                                                           Active    Active
    System Organ Class / Preferred Term          Placebo     Low      High      Total
    ____________________________________________________________________________________

    Serious adverse events
      Subjects with at least one event          2 (10.0%) 1 (5.0%)  3 (15.0%) 6 (10.0%)
      Events leading to study drug interruption 1 (5.0%)      0     2 (10.0%)  3 (5.0%)
    Treatment-emergent adverse events
      Headache                                  6 (30.0%) 5 (25.0%) 7 (35.0%) 18 (30.0%)
      Nausea                                    4 (20.0%) 3 (15.0%) 5 (25.0%) 12 (20.0%)
      Alanine aminotransferase increased        1 (5.0%)  2 (10.0%) 3 (15.0%) 6 (10.0%)
    ____________________________________________________________________________________
    This version lets reporter estimate the width of each column.

                                                                        programs/t-ae-overview.R 03JUL2026 19:03

### Narrow max_table_width

`max_table_width` caps the whole table by passing a table-width constraint to `reporter`. A narrower width forces long labels to wrap sooner and leaves less room for treatment columns.

    title1 <- "Table 14.3.1 Overview of Adverse Events"
    title2 <- "Narrow table width"
    footnote1 <- "This version caps the total table width at 5.5 inches."
    progname <- "programs/t-ae-overview.R"

    ae_narrow_paths <- gtsummary_reporter(
      gts_obj = ae_tbl,
      file_path = file.path(output_dir, "t-ae-narrow.rtf"),
      column_labels = c(
        label = "System Organ Class / Preferred Term",
        placebo = "Placebo",
        active_low = "Active Low",
        active_high = "Active High",
        total = "Total"
      ),
      spanning_headers = data.frame(
        from = "placebo",
        to = "total",
        label = "Treatment Group"
      ),
      max_table_width = 5.5,
      output_types = "TXT",
      save_rds = FALSE
    )

    basename(ae_narrow_paths)
    #> [1] "t-ae-narrow.txt"

    Table 14.3.1 Overview of Adverse Events
    Narrow table width

    ____________________________________________________________________________________
                                                             Treatment Group
                                                ________________________________________
                                                           Active    Active
    System Organ Class / Preferred Term          Placebo     Low      High      Total
    ____________________________________________________________________________________

    Serious adverse events
      Subjects with at least one event          2 (10.0%) 1 (5.0%)  3 (15.0%) 6 (10.0%)
      Events leading to study drug interruption 1 (5.0%)      0     2 (10.0%)  3 (5.0%)
    Treatment-emergent adverse events
      Headache                                  6 (30.0%) 5 (25.0%) 7 (35.0%) 18 (30.0%)
      Nausea                                    4 (20.0%) 3 (15.0%) 5 (25.0%) 12 (20.0%)
      Alanine aminotransferase increased        1 (5.0%)  2 (10.0%) 3 (15.0%) 6 (10.0%)
    ____________________________________________________________________________________
    This version caps the total table width at 5.5 inches.

                                                                        programs/t-ae-overview.R 03JUL2026 19:03

### Manual column_widths

`column_widths` assigns widths in display-column order. For this data frame, the order is `label`, `placebo`, `active_low`, `active_high`, and `total`. Manual widths are useful when the label column needs more space than reporter’s automatic estimate provides.

    title1 <- "Table 14.3.1 Overview of Adverse Events"
    title2 <- "Manual column widths"
    footnote1 <- "This version gives more room to the label column."
    progname <- "programs/t-ae-overview.R"

    ae_manual_paths <- gtsummary_reporter(
      gts_obj = ae_tbl,
      file_path = file.path(output_dir, "t-ae-manual.rtf"),
      column_labels = c(
        label = "System Organ Class / Preferred Term",
        placebo = "Placebo",
        active_low = "Active Low",
        active_high = "Active High",
        total = "Total"
      ),
      spanning_headers = data.frame(
        from = "placebo",
        to = "total",
        label = "Treatment Group"
      ),
      column_widths = "3.8|1.1|1.3|1.3|1.1",
      output_types = "TXT",
      save_rds = FALSE
    )

    basename(ae_manual_paths)
    #> [1] "t-ae-manual.txt"

    Table 14.3.1 Overview of Adverse Events
    Manual column widths

    _______________________________________________________________________________________________________
                                                                       Treatment Group
                                                  _________________________________________________________
    System Organ Class / Preferred Term             Placebo      Active Low      Active High      Total
    _______________________________________________________________________________________________________

    Serious adverse events
      Subjects with at least one event             2 (10.0%)      1 (5.0%)        3 (15.0%)     6 (10.0%)
      Events leading to study drug interruption     1 (5.0%)          0           2 (10.0%)      3 (5.0%)
    Treatment-emergent adverse events
      Headache                                     6 (30.0%)      5 (25.0%)       7 (35.0%)     18 (30.0%)
      Nausea                                       4 (20.0%)      3 (15.0%)       5 (25.0%)     12 (20.0%)
      Alanine aminotransferase increased            1 (5.0%)      2 (10.0%)       3 (15.0%)     6 (10.0%)
    _______________________________________________________________________________________________________
    This version gives more room to the label column.

                                                                        programs/t-ae-overview.R 03JUL2026 19:03

## Example 5: manual pagination

By default, `rows_per_page = NULL` leaves pagination to `reporter`. Set `rows_per_page` when a table shell requires predictable manual chunks before the report is written. The example below expands the adverse event table and starts a new page after every seven data rows.

    paged_ae_tbl <- rbind(
      ae_tbl,
      data.frame(
        label = c(
          "Skin and subcutaneous tissue disorders",
          "  Rash",
          "  Pruritus",
          "Respiratory, thoracic and mediastinal disorders",
          "  Cough",
          "  Oropharyngeal pain",
          "Gastrointestinal disorders",
          "  Diarrhea",
          "  Abdominal pain",
          "Investigations",
          "  Blood creatine phosphokinase increased"
        ),
        placebo = c(
          "", "2 (10.0%)", "1 (5.0%)", "", "3 (15.0%)", "1 (5.0%)",
          "", "2 (10.0%)", "1 (5.0%)", "", "1 (5.0%)"
        ),
        active_low = c(
          "", "3 (15.0%)", "1 (5.0%)", "", "2 (10.0%)", "2 (10.0%)",
          "", "4 (20.0%)", "2 (10.0%)", "", "2 (10.0%)"
        ),
        active_high = c(
          "", "4 (20.0%)", "2 (10.0%)", "", "5 (25.0%)", "2 (10.0%)",
          "", "5 (25.0%)", "3 (15.0%)", "", "4 (20.0%)"
        ),
        total = c(
          "", "9 (15.0%)", "4 (6.7%)", "", "10 (16.7%)", "5 (8.3%)",
          "", "11 (18.3%)", "6 (10.0%)", "", "7 (11.7%)"
        ),
        stringsAsFactors = FALSE
      )
    )

    nrow(paged_ae_tbl)
    #> [1] 18

    title1 <- "Table 14.3.2 Adverse Events by Body System"
    title2 <- "Manual pagination"
    footnote1 <- "This version starts a new report page after every seven data rows."
    progname <- "programs/t-ae-paged.R"

    paged_paths <- gtsummary_reporter(
      gts_obj = paged_ae_tbl,
      file_path = file.path(output_dir, "t-ae-paged.rtf"),
      column_labels = c(
        label = "System Organ Class / Preferred Term",
        placebo = "Placebo",
        active_low = "Active Low",
        active_high = "Active High",
        total = "Total"
      ),
      spanning_headers = data.frame(
        from = "placebo",
        to = "total",
        label = "Treatment Group"
      ),
      rows_per_page = 7,
      output_types = c("TXT", "HTML"),
      save_rds = FALSE
    )

    output_manifest(paged_paths)
    #>   format            file bytes
    #> 1    TXT  t-ae-paged.txt  3845
    #> 2   HTML t-ae-paged.html 12019

### TXT preview

TXT output repeats the title, table header, footnote, and footer after each manual page break.

    Table 14.3.2 Adverse Events by Body System
    Manual pagination

    ____________________________________________________________________________________
                                                             Treatment Group
                                                ________________________________________
                                                           Active    Active
    System Organ Class / Preferred Term          Placebo     Low      High      Total
    ____________________________________________________________________________________

    Serious adverse events
      Subjects with at least one event          2 (10.0%) 1 (5.0%)  3 (15.0%) 6 (10.0%)
      Events leading to study drug interruption 1 (5.0%)      0     2 (10.0%)  3 (5.0%)
    Treatment-emergent adverse events
      Headache                                  6 (30.0%) 5 (25.0%) 7 (35.0%) 18 (30.0%)
      Nausea                                    4 (20.0%) 3 (15.0%) 5 (25.0%) 12 (20.0%)
      Alanine aminotransferase increased        1 (5.0%)  2 (10.0%) 3 (15.0%) 6 (10.0%)
    ____________________________________________________________________________________
    This version starts a new report page after every seven data rows.

                                                                           programs/t-ae-paged.R 03JUL2026 19:03
    Table 14.3.2 Adverse Events by Body System
    Manual pagination

    ________________________________________________________________________________________
                                                                 Treatment Group
                                                    ________________________________________
                                                               Active    Active
    System Organ Class / Preferred Term              Placebo     Low      High      Total
    ________________________________________________________________________________________

    Skin and subcutaneous tissue disorders
      Rash                                          2 (10.0%) 3 (15.0%) 4 (20.0%) 9 (15.0%)
      Pruritus                                      1 (5.0%)  1 (5.0%)  2 (10.0%)  4 (6.7%)
    Respiratory, thoracic and mediastinal disorders
      Cough                                         3 (15.0%) 2 (10.0%) 5 (25.0%) 10 (16.7%)
      Oropharyngeal pain                            1 (5.0%)  2 (10.0%) 2 (10.0%)  5 (8.3%)
    Gastrointestinal disorders
    ________________________________________________________________________________________
    This version starts a new report page after every seven data rows.

                                                                           programs/t-ae-paged.R 03JUL2026 19:03
    Table 14.3.2 Adverse Events by Body System
    Manual pagination

    _________________________________________________________________________________
                                                          Treatment Group
                                             ________________________________________
                                                        Active    Active
    System Organ Class / Preferred Term       Placebo     Low      High      Total
    _________________________________________________________________________________

      Diarrhea                               2 (10.0%) 4 (20.0%) 5 (25.0%) 11 (18.3%)
      Abdominal pain                         1 (5.0%)  2 (10.0%) 3 (15.0%) 6 (10.0%)
    Investigations
      Blood creatine phosphokinase increased 1 (5.0%)  2 (10.0%) 4 (20.0%) 7 (11.7%)
    _________________________________________________________________________________
    This version starts a new report page after every seven data rows.

                                                                           programs/t-ae-paged.R 03JUL2026 19:03

### HTML preview

## Common layout controls

These arguments are the main controls to review when moving from a draft table to a production report shell:

- `output_types` chooses the files to write. This vignette runs TXT and HTML because they can be rendered directly inside an HTML page.
- `column_labels` overrides displayed headers for plain data frames or special sponsor wording.
- `spanning_headers` adds or overrides multi-column headers.
- `max_table_width` passes a total table-width constraint to `reporter`.
- `column_widths` controls exact column allocation.
- `max_chars_per_line` constrains TXT output to a known character budget.
- `rows_per_page` forces manual chunks before `reporter` writes the report; leave it `NULL` when reporter’s automatic pagination is sufficient.

    gtsummary_reporter(
      gts_obj = ae_tbl,
      file_path = file.path(output_dir, "t-ae-final.rtf"),
      column_labels = c(
        label = "System Organ Class / Preferred Term",
        placebo = "Placebo",
        active_low = "Active Low",
        active_high = "Active High",
        total = "Total"
      ),
      spanning_headers = data.frame(
        from = "placebo",
        to = "total",
        label = "Treatment Group"
      ),
      column_widths = "3.8|1.1|1.3|1.3|1.1",
      max_chars_per_line = 132,
      rows_per_page = 24,
      output_types = c("RTF", "TXT", "HTML")
    )

## Output checklist

Before treating generated files as final, check that:

- the source table type is intentional (`gtsummary` object or plain data frame);
- all requested report formats were written;
- title, footnote, and footer metadata are present;
- label indentation is preserved after wrapping, especially for TXT outputs with manual label widths;
- spanning headers cover the intended display columns;
- any chosen manual widths fit the page setup and TXT character budget;
- manual page breaks occur at the intended row boundaries, if `rows_per_page` is used;
- saved RDS data matches the rows and columns sent to `reporter`.