From 6c6b2b278446653eb05acf6ad3bb08f9b52de09d Mon Sep 17 00:00:00 2001 From: Andreas Stefl Date: Tue, 6 Oct 2026 10:55:19 +0200 Subject: [PATCH 1/2] fix(html): stop a cell spill at a covered cell The cell index of an ods holds no covered cell, so a lookup there gives a blank cell. The spill scan then let text run over the merged cell from a row above, for example '(KWH)' over 'KWH Meter' in style-various-2.ods. The sheet loop now records, per column, the row where a span from above ends, and the scan stops at a covered column, as LibreOffice does. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01Gb1fLafqqzehpqPuU6uBfn --- CHANGELOG.md | 3 +++ src/odr/internal/html/document_element.cpp | 11 ++++++++++- test/src/html_test.cpp | 18 ++++++++++++++++++ 3 files changed, 31 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 095a44ea2..881d44b86 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 text no longer spills over a cell that a merged cell from a row + above covers. + - PDF annotations reject malformed indices, colors and geometry, and bound squiggle generation before writing output. diff --git a/src/odr/internal/html/document_element.cpp b/src/odr/internal/html/document_element.cpp index 36a900bfa..a1f5d3621 100644 --- a/src/odr/internal/html/document_element.cpp +++ b/src/odr/internal/html/document_element.cpp @@ -702,6 +702,9 @@ void html::translate_sheet(const Sheet &sheet, const WritingState &state) { const ElementRange shapes = sheet.shapes(); const bool has_shapes = shapes.begin() != shapes.end(); + // per column, the row where a cell spanning down from above ends + std::vector span_end_row(end_column, 0); + TableCursor cursor; for (std::uint32_t row_index = cursor.row(); row_index < end_row; row_index = cursor.row()) { @@ -782,7 +785,9 @@ void html::translate_sheet(const Sheet &sheet, const WritingState &state) { const SheetCell cell_ahead = ahead == next_column && next.has_value() ? *next : sheet.cell(ahead, row_index); - if (!is_blank(cell_ahead)) { + // The merged cell from a row above paints over a covered position, + // which a reader may give as a blank cell. + if (span_end_row[ahead] > row_index || !is_blank(cell_ahead)) { bounded = true; break; } @@ -887,6 +892,10 @@ void html::translate_sheet(const Sheet &sheet, const WritingState &state) { } state.out().write_element_end("td"); + for (std::uint32_t column = column_index; + column < std::min(next_column, end_column); ++column) { + span_end_row[column] = row_index + cell_span.rows; + } cursor.add_cell(cell_span.columns, cell_span.rows); if (cursor.column() == next_column) { pending = next; diff --git a/test/src/html_test.cpp b/test/src/html_test.cpp index 2133f65db..47e40dee9 100644 --- a/test/src/html_test.cpp +++ b/test/src/html_test.cpp @@ -1631,6 +1631,24 @@ TEST(html, a_cell_spills_over_a_blank_neighbour_and_is_cut_by_a_full_one) { EXPECT_EQ(spills.find("nowrap;overflow:hidden"), std::string::npos); } +// A covered cell shows the merged cell from the row above, so a spill stops +// before it. +TEST(html, a_cell_does_not_spill_into_a_merged_cell) { + const std::string page = render_sheet( + fods_file(fods_row(fods_blank() + fods_blank() + + R"()" + R"(merged)") + + fods_row(fods_cell("one") + fods_blank() + + ""), + fods_columns(3)), + HtmlConfig()); + + EXPECT_NE(page.find("max-width:0;white-space:nowrap;" + "clip-path:inset(0 -96px 0 0)"), + std::string::npos); + EXPECT_EQ(page.find("clip-path:inset(0 -192px 0 0)"), std::string::npos); +} + // #822: and where nothing follows it at all, out onto the canvas — there is no // content to the right of the last column for it to paint over. TEST(html, a_cell_at_the_end_of_its_row_is_not_cut) { From a5be7e726cd3492add6ae7d733816fc9747e531f Mon Sep 17 00:00:00 2001 From: Andreas Stefl Date: Tue, 6 Oct 2026 10:56:58 +0200 Subject: [PATCH 2/2] test: advance the output pins for the sheet, xlsx, PDF and script changes The references take the output of #1079 to #1126 and of the spill fix before this commit. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01Gb1fLafqqzehpqPuU6uBfn --- test/data.cmake | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/data.cmake b/test/data.cmake index 0f8b0b987..3f89e71d9 100644 --- a/test/data.cmake +++ b/test/data.cmake @@ -17,9 +17,9 @@ odr_test_data( odr_test_data( PATH "reference-output/odr-public" URL "https://github.com/opendocument-app/OpenDocument.test.output.git" - REVISION "429287e50942c53dd25afbc754217970c7499750") + REVISION "78e30814a59ef0c1409948628c6541da1449b027") odr_test_data( PATH "reference-output/odr-private" URL "https://github.com/opendocument-app/OpenDocument.test-private.output.git" - REVISION "9ade3a517a9787ffb2bcbb356e989b6d0cdbfd19") + REVISION "2e4e8efc8c2c3596280fa3bea57ebd7897e5cf66")