From a972a61ea0b9cc501420bde21408651e51de8e8d Mon Sep 17 00:00:00 2001 From: Yihui Xie Date: Mon, 6 Jul 2026 16:31:06 -0400 Subject: [PATCH 1/2] Add 1px height buffer to PDF @page size to fix two-page output on macOS scrollHeight returns an integer floor of the actual content height; on macOS, font metrics (San Francisco) can make the rendered table 0.x px taller than the measured value, causing the PDF to spill onto a second page. Adding 1px to the @page height absorbs this sub-pixel difference without visibly enlarging the crop. Co-Authored-By: Claude Sonnet 4.6 --- R/render.R | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/R/render.R b/R/render.R index 83370b5..78d7e2e 100644 --- a/R/render.R +++ b/R/render.R @@ -431,7 +431,9 @@ lt_export = function( } if (crop && is_pdf) { # @page size drives the PDF page box exactly; no image post-processing. - style = sprintf('', w, h, layout) + # scrollHeight is integer-floored; add 1px to absorb sub-pixel font + # metrics that differ by platform (e.g. macOS San Francisco vs Linux). + style = sprintf('', w, h + 1L, layout) html = sub('', paste0(style, ''), html, fixed = TRUE) with_temp_html(html, function(f) xfun::browser_print(f, output, browser = browser, ...)) From f967d0f8b10bf2830f62b6b463d29397e5171f32 Mon Sep 17 00:00:00 2001 From: Yihui Xie Date: Mon, 6 Jul 2026 17:02:16 -0400 Subject: [PATCH 2/2] Round measured table width up by 1px so PDF export stays single-page lt_measure() reads body.scrollWidth, which is the integer floor of the table's fractional natural (max-content) width. lt_export() then pins the body to that floored width for the PDF pass. Being a sub-pixel narrower than the content, a cell would wrap, growing the table taller than the measured height and pushing the PDF onto a second page. This reproduced on macOS (Google Chrome) but not Linux (Chromium), because their font metrics round differently. Fix: add 1px to the measured width so the body is never narrower than the content and never re-wraps. Replaces the earlier 1px height buffer (wrong axis). Adds a regression test exporting a wide, long-labelled table to a single-page PDF. Co-Authored-By: Claude Opus 4.8 --- DESCRIPTION | 2 +- R/render.R | 13 +++++++++---- tests/test-ci/test-js.R | 16 ++++++++++++++++ 3 files changed, 26 insertions(+), 5 deletions(-) diff --git a/DESCRIPTION b/DESCRIPTION index 350b1b9..fcf3f2d 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,7 +1,7 @@ Package: lt Type: Package Title: Lightweight Tables via JSON Specs and JavaScript -Version: 0.1.6 +Version: 0.1.7 Authors@R: person('Yihui', 'Xie', email = 'xie@yihui.name', role = c('aut', 'cre', 'cph'), comment = c(ORCID = '0000-0003-0645-5666', URL = 'https://yihui.org')) Description: A lightweight grammar of tables. Build a table by declaring a JSON diff --git a/R/render.R b/R/render.R index 78d7e2e..2cda69d 100644 --- a/R/render.R +++ b/R/render.R @@ -320,7 +320,14 @@ lt_measure = function(html, pad, width = NULL, browser = NULL) { dom = with_temp_html(html, function(f) xfun::browser_dom(f, browser = browser)) m = regmatches(dom, regexec('data-ltw="([0-9]+)" data-lth="([0-9]+)"', dom))[[1]] if (length(m) != 3L) stop('Failed to measure the table dimensions.') - as.integer(m[-1L]) + d = as.integer(m[-1L]) + # scrollWidth is the floor of the table's fractional natural width. Pinning + # the body to that floored width leaves it a sub-pixel too narrow, so a cell + # wraps and the table grows taller than the measured height, spilling the + # PDF onto a second page (platform-dependent: seen on macOS, not Linux). Add + # 1px so the body is never narrower than the content and never re-wraps. + d[1L] = d[1L] + 1L + d } #' Export an lt table to a file @@ -431,9 +438,7 @@ lt_export = function( } if (crop && is_pdf) { # @page size drives the PDF page box exactly; no image post-processing. - # scrollHeight is integer-floored; add 1px to absorb sub-pixel font - # metrics that differ by platform (e.g. macOS San Francisco vs Linux). - style = sprintf('', w, h + 1L, layout) + style = sprintf('', w, h, layout) html = sub('', paste0(style, ''), html, fixed = TRUE) with_temp_html(html, function(f) xfun::browser_print(f, output, browser = browser, ...)) diff --git a/tests/test-ci/test-js.R b/tests/test-ci/test-js.R index f98fa70..f6a0a9a 100644 --- a/tests/test-ci/test-js.R +++ b/tests/test-ci/test-js.R @@ -281,6 +281,22 @@ if (has_browser()) assert("lt_export() crops a large table to one PDF page", { (pdf_pages(pdf) %==% 1L) }) +# A wide table must crop to one page too. scrollWidth is the floor of the +# table's fractional natural width; pinning the body to that floored width +# used to leave it a sub-pixel too narrow, wrapping a cell and growing the +# table past the measured height, so the PDF spilled onto a second page +# (seen on macOS, not Linux). lt_measure() now rounds the width up by 1px. +if (has_browser()) assert("lt_export() crops a wide table to one PDF page", { + # Many columns with long labels give the table a fractional natural width. + x = lt(as.data.frame(matrix( + 1:32, 4, 8, dimnames = list(NULL, paste0("a_long_column_label_", 1:8)) + ))) + pdf = tempfile(fileext = ".pdf") + on.exit(unlink(pdf), add = TRUE) + lt_export(x, pdf) + (pdf_pages(pdf) %==% 1L) +}) + if (has_browser() && xfun::loadable("magick")) assert("lt_export() crops PNG tightly to the table size", { x = lt(head(mtcars))