From 95acd1e239ab5d8ca18b3ebeb90216e27c3a53fe Mon Sep 17 00:00:00 2001 From: Andreas Stefl Date: Mon, 5 Oct 2026 02:56:46 +0200 Subject: [PATCH] fix: bound spreadsheet width and skip empty spill scans --- CHANGELOG.md | 3 +++ src/odr/html.hpp | 2 +- src/odr/internal/html/document_element.cpp | 13 ++++++++----- test/src/html_test.cpp | 9 ++++++++- 4 files changed, 20 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 762601079..b23c185c6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,9 @@ The release run heads these entries with the version and opens a fresh ## Unreleased +- Spreadsheet rendering bounds columns as well as rows by the cell budget and + avoids rescanning blank spans for every empty cell. + - Browser text editors delete complete supplementary Unicode characters when the browser supplies no deletion range, preserving valid text and undo. diff --git a/src/odr/html.hpp b/src/odr/html.hpp index 251376644..10937b975 100644 --- a/src/odr/html.hpp +++ b/src/odr/html.hpp @@ -173,7 +173,7 @@ struct HtmlConfig { /// Largest sheet region written, per axis; cells past it are dropped. std::optional spreadsheet_limit{ TableDimensions(100000, 500)}; - /// Most cells written for one sheet; bounds the rows by the sheet's width. + /// Cell budget, at least one; caps columns first, then rows by that width. std::optional spreadsheet_cell_limit{500000}; /// Trim a sheet to the cells it uses before the limits above apply. bool spreadsheet_limit_by_content{true}; diff --git a/src/odr/internal/html/document_element.cpp b/src/odr/internal/html/document_element.cpp index 9d2f46029..36a900bfa 100644 --- a/src/odr/internal/html/document_element.cpp +++ b/src/odr/internal/html/document_element.cpp @@ -165,7 +165,11 @@ TableDimensions html::sheet_rendered_extent(const Sheet &sheet, } end_column = std::max(1u, end_column); if (config.spreadsheet_cell_limit) { - const std::uint64_t rows = *config.spreadsheet_cell_limit / end_column; + const std::uint64_t limit = + std::max(1, *config.spreadsheet_cell_limit); + end_column = + static_cast(std::min(end_column, limit)); + const std::uint64_t rows = limit / end_column; end_row = static_cast( std::min(end_row, std::max(1, rows))); } @@ -769,10 +773,9 @@ void html::translate_sheet(const Sheet &sheet, const WritingState &state) { // the bottom and would paint over the row below. cell_css += "overflow:hidden;"; } - // Over the empty cells beside it, cut where the next one has something - // to show, unbounded where nothing follows. Each blank cell is walked by - // the one cell that may spill over it, so the row costs one pass. - if (!wraps && cuts_its_text && column_pixels[column_index].has_value()) { + // Only nonblank cells spill, so each blank span is scanned once. + if (!wraps && cuts_its_text && column_pixels[column_index].has_value() && + !is_blank(cell)) { std::optional spill(0); bool bounded = false; for (std::uint32_t ahead = next_column; ahead < end_column; ++ahead) { diff --git a/test/src/html_test.cpp b/test/src/html_test.cpp index bb21c0672..2133f65db 100644 --- a/test/src/html_test.cpp +++ b/test/src/html_test.cpp @@ -1131,7 +1131,7 @@ TEST(html, an_editable_csv_states_that_it_takes_no_style) { } // The rows a sheet keeps follow how wide it turns out to be. -TEST(html, the_cell_budget_bounds_the_rows_by_the_width) { +TEST(html, the_cell_budget_bounds_columns_and_rows) { HtmlConfig config; config.spreadsheet_limit = TableDimensions(1000, 1000); config.spreadsheet_cell_limit = 60; @@ -1153,6 +1153,12 @@ TEST(html, the_cell_budget_bounds_the_rows_by_the_width) { // the rectangle still caps a sheet the budget would let through config.spreadsheet_limit = TableDimensions(5, 1000); EXPECT_EQ(rendered(30, 3).rows, 5); + config.spreadsheet_limit.reset(); + EXPECT_EQ(rendered(2, 80).columns, 60); + EXPECT_EQ(rendered(2, 80).rows, 1); + config.spreadsheet_cell_limit = 0; + EXPECT_EQ(rendered(2, 80).columns, 1); + EXPECT_EQ(rendered(2, 80).rows, 1); } // #822: an inline `style` is the one shape a browser cannot share across the @@ -1622,6 +1628,7 @@ TEST(html, a_cell_spills_over_a_blank_neighbour_and_is_cut_by_a_full_one) { EXPECT_NE(spills.find("max-width:0;white-space:nowrap;" "clip-path:inset(0 -96px 0 0)"), std::string::npos); + EXPECT_EQ(spills.find("nowrap;overflow:hidden"), std::string::npos); } // #822: and where nothing follows it at all, out onto the canvas — there is no