From 3119ba31046d7d5a3eb0e30fa1e7bf2027d437af Mon Sep 17 00:00:00 2001 From: PgBiel <9021226+PgBiel@users.noreply.github.com> Date: Thu, 7 Aug 2025 12:04:21 -0300 Subject: [PATCH] Ensure table headers trigger rowbreak (#6687) --- .../typst-library/src/layout/grid/resolve.rs | 251 +++++++++--------- tests/ref/grid-footer-rowbreak-line.png | Bin 0 -> 327 bytes tests/ref/grid-header-multiple-unordered.png | Bin 0 -> 469 bytes ...rid-header-rowbreak-auto-and-fixed-pos.png | Bin 0 -> 279 bytes tests/ref/grid-header-rowbreak-auto-pos.png | Bin 0 -> 452 bytes tests/ref/grid-header-rowbreak-fixed-pos.png | Bin 0 -> 616 bytes tests/ref/grid-header-rowbreak-mixed-pos.png | Bin 0 -> 659 bytes tests/ref/grid-header-skip-unordered.png | Bin 0 -> 294 bytes ...-subheaders-multi-page-row-with-footer.png | Bin 1345 -> 1352 bytes tests/ref/grid-subheaders-multi-page-row.png | Bin 1173 -> 1181 bytes ...d-subheaders-multi-page-rowspan-gutter.png | Bin 1560 -> 1628 bytes ...headers-multi-page-rowspan-with-footer.png | Bin 1190 -> 1165 bytes .../grid-subheaders-multi-page-rowspan.png | Bin 1048 -> 1064 bytes .../issue-6666-auto-hlines-around-footer.png | Bin 0 -> 683 bytes .../issue-6666-auto-hlines-around-header.png | Bin 0 -> 1332 bytes tests/suite/layout/grid/footers.typ | 23 +- tests/suite/layout/grid/headers.typ | 125 ++++++++- 17 files changed, 272 insertions(+), 127 deletions(-) create mode 100644 tests/ref/grid-footer-rowbreak-line.png create mode 100644 tests/ref/grid-header-multiple-unordered.png create mode 100644 tests/ref/grid-header-rowbreak-auto-and-fixed-pos.png create mode 100644 tests/ref/grid-header-rowbreak-auto-pos.png create mode 100644 tests/ref/grid-header-rowbreak-fixed-pos.png create mode 100644 tests/ref/grid-header-rowbreak-mixed-pos.png create mode 100644 tests/ref/grid-header-skip-unordered.png create mode 100644 tests/ref/issue-6666-auto-hlines-around-footer.png create mode 100644 tests/ref/issue-6666-auto-hlines-around-header.png diff --git a/crates/typst-library/src/layout/grid/resolve.rs b/crates/typst-library/src/layout/grid/resolve.rs index b7d2ffa6c..e8c52d2fb 100644 --- a/crates/typst-library/src/layout/grid/resolve.rs +++ b/crates/typst-library/src/layout/grid/resolve.rs @@ -19,7 +19,7 @@ use typst_library::text::TextElem; use typst_library::visualize::{Paint, Stroke}; use typst_syntax::Span; -use typst_utils::NonZeroExt; +use typst_utils::{NonZeroExt, SmallBitSet}; use crate::introspection::SplitLocator; @@ -1048,11 +1048,6 @@ impl<'x> CellGridResolver<'_, '_, 'x> { // automatically-positioned cell. let mut auto_index: usize = 0; - // The next header after the latest auto-positioned cell. This is used - // to avoid checking for collision with headers that were already - // skipped. - let mut next_header = 0; - // We have to rebuild the grid to account for fixed cell positions. // // Create at least 'children.len()' positions, since there could be at @@ -1067,17 +1062,26 @@ impl<'x> CellGridResolver<'_, '_, 'x> { let Some(child_count) = children.len().checked_next_multiple_of(columns) else { bail!(self.span, "too many cells or lines were given") }; + + // Rows in this bitset are occupied by an existing header. + // This allows for efficiently checking whether a cell would collide + // with a header at a certain row. (For footers, it's easy as there is + // only one.) + // + // TODO(subfooters): how to add a footer here while avoiding + // unnecessary allocations? + let mut header_rows: SmallBitSet = SmallBitSet::new(); let mut resolved_cells: Vec> = Vec::with_capacity(child_count); for child in children { self.resolve_grid_child( columns, &mut pending_hlines, &mut pending_vlines, + &mut header_rows, &mut headers, &mut footer, &mut repeat_footer, &mut auto_index, - &mut next_header, &mut resolved_cells, &mut at_least_one_cell, child, @@ -1129,11 +1133,11 @@ impl<'x> CellGridResolver<'_, '_, 'x> { columns: usize, pending_hlines: &mut Vec<(Span, Line, bool)>, pending_vlines: &mut Vec<(Span, Line)>, + header_rows: &mut SmallBitSet, headers: &mut Vec>, footer: &mut Option<(usize, Span, Footer)>, repeat_footer: &mut bool, auto_index: &mut usize, - next_header: &mut usize, resolved_cells: &mut Vec>>, at_least_one_cell: &mut bool, child: ResolvableGridChild, @@ -1149,18 +1153,35 @@ impl<'x> CellGridResolver<'_, '_, 'x> { // in which case this variable remains 'None'. let mut row_group_data: Option = None; - // The normal auto index should only be stepped (upon placing an - // automatically-positioned cell, to indicate the position of the - // next) outside of headers or footers, in which case the auto - // index will be updated with the local auto index. Inside headers - // and footers, however, cells can only start after the first empty - // row (as determined by 'first_available_row' below), meaning that - // the next automatically-positioned cell will be in a different - // position than it would usually be if it would be in a non-empty - // row, so we must step a local index inside headers and footers - // instead, and use a separate counter outside them. + // Usually, the global auto index is stepped only when a cell with + // fully automatic position (no fixed x/y) is placed, advancing one + // position. In that usual case, 'local_auto_index == auto_index' + // holds, as 'local_auto_index' is what the code below actually + // updates. + // + // However, headers and footers must trigger a rowbreak if the + // previous row isn't empty, given they cannot occupy only part of a + // row. Therefore, the initial auto index used by their auto cells + // should be right below the first empty row. + // + // The problem is that we don't know whether the header will actually + // have an auto cell or not, and we don't want the external auto index + // to change (no rowbreak should be triggered) if the header has no + // auto cells (although a fully empty header does count as having + // auto cells, albeit empty). + // + // So, we use a separate auto index counter inside the header. It starts + // below the first non-empty row. If the header only has fixed-position + // cells, the external counter is unchanged. Otherwise (has auto cells + // or is empty), the external counter is synchronized and moved to + // below the header. + // + // This ensures lines and cells specified below the header in the + // source code also appear below it in the final grid/table. let local_auto_index = if matches!(child, ResolvableGridChild::Item(_)) { - auto_index + // Re-borrow the original auto index so we can reuse this mutable + // reference later. + &mut *auto_index } else { // Although 'usize' is Copy, we need to be explicit here that we // aren't reborrowing the original auto index but rather making a @@ -1168,24 +1189,6 @@ impl<'x> CellGridResolver<'_, '_, 'x> { &mut (*auto_index).clone() }; - // NOTE: usually, if 'next_header' were to be updated inside a row - // group (indicating a header was skipped by a cell), that would - // indicate a collision between the row group and that header, which - // is an error. However, the exception is for the first auto cell of - // the row group, which may skip headers while searching for a position - // where to begin the row group in the first place. - // - // Therefore, we cannot safely share the counter in the row group with - // the counter used by auto cells outside, as it might update it in a - // valid situation, whereas it must not, since its auto cells use a - // different auto index counter and will have seen different headers, - // so we copy the next header counter while inside a row group. - let local_next_header = if matches!(child, ResolvableGridChild::Item(_)) { - next_header - } else { - &mut (*next_header).clone() - }; - // The first row in which this table group can fit. // // Within headers and footers, this will correspond to the first @@ -1253,7 +1256,9 @@ impl<'x> CellGridResolver<'_, '_, 'x> { } }; + let mut had_auto_cells = false; let items = header_footer_items.into_iter().flatten().chain(simple_item); + for item in items { let cell = match item { ResolvableGridItem::HLine { y, start, end, stroke, span, position } => { @@ -1364,16 +1369,17 @@ impl<'x> CellGridResolver<'_, '_, 'x> { let resolved_index = { let cell_x = cell.x(self.styles); let cell_y = cell.y(self.styles); + had_auto_cells |= cell_x.is_auto() && cell_y.is_auto(); resolve_cell_position( cell_x, cell_y, colspan, rowspan, + header_rows, headers, footer.as_ref(), resolved_cells, local_auto_index, - local_next_header, first_available_row, columns, row_group_data.is_some(), @@ -1511,8 +1517,10 @@ impl<'x> CellGridResolver<'_, '_, 'x> { None => { // Empty header/footer: consider the header/footer to be - // at the next empty row after the latest auto index. + // automatically positioned at the next empty row after the + // latest auto index. *local_auto_index = first_available_row * columns; + had_auto_cells = true; let group_start = first_available_row; let group_end = group_start + 1; @@ -1556,6 +1564,13 @@ impl<'x> CellGridResolver<'_, '_, 'x> { } } + if had_auto_cells { + // Header/footer was automatically positioned (either by having + // auto cells or by virtue of being empty), so trigger a + // rowbreak. Move auto index counter right below it. + *auto_index = group_range.end * columns; + } + match row_group.kind { RowGroupKind::Header => { let data = Header { @@ -1563,7 +1578,7 @@ impl<'x> CellGridResolver<'_, '_, 'x> { // is gutter. But only once all cells have been analyzed // and the header has fully expanded in the fixup loop // below. - range: group_range, + range: group_range.clone(), level: row_group.repeatable_level.get(), @@ -1572,24 +1587,11 @@ impl<'x> CellGridResolver<'_, '_, 'x> { short_lived: false, }; - // Mark consecutive headers right before this one as short - // lived if they would have a higher or equal level, as - // then they would immediately stop repeating during - // layout. - let mut consecutive_header_start = data.range.start; - for conflicting_header in - headers.iter_mut().rev().take_while(move |h| { - let conflicts = h.range.end == consecutive_header_start - && h.level >= data.level; - - consecutive_header_start = h.range.start; - conflicts - }) - { - conflicting_header.short_lived = true; - } - headers.push(Repeatable { inner: data, repeated: row_group.repeat }); + + for row in group_range { + header_rows.insert(row); + } } RowGroupKind::Footer => { @@ -1786,9 +1788,13 @@ impl<'x> CellGridResolver<'_, '_, 'x> { row_amount: usize, at_least_one_cell: bool, ) -> SourceResult>> { - // Mark consecutive headers right before the end of the table, or the - // final footer, as short lived, given that there are no normal rows - // after them, so repeating them is pointless. + headers.sort_unstable_by_key(|h| h.range.start); + + // Mark consecutive headers in those positions as short-lived: + // (a) before a header of lower level; + // (b) right before the end of the table or the final footer; + // That's because they would stop repeating immediately, so don't even + // attempt to. // // It is important to do this BEFORE we update header and footer ranges // due to gutter below as 'row_amount' doesn't consider gutter. @@ -1796,16 +1802,20 @@ impl<'x> CellGridResolver<'_, '_, 'x> { // TODO(subfooters): take the last footer if it is at the end and // backtrack through consecutive footers until the first one in the // sequence is found. If there is no footer at the end, there are no - // haeders to turn short-lived. + // headers to turn short-lived. let mut consecutive_header_start = footer.as_ref().map(|(_, _, f)| f.start).unwrap_or(row_amount); - for header_at_the_end in headers.iter_mut().rev().take_while(move |h| { - let at_the_end = h.range.end == consecutive_header_start; + let mut last_consec_level = 0; + for header in headers.iter_mut().rev() { + if header.range.end == consecutive_header_start + && header.level >= last_consec_level + { + header.short_lived = true; + } else { + last_consec_level = header.level; + } - consecutive_header_start = h.range.start; - at_the_end - }) { - header_at_the_end.short_lived = true; + consecutive_header_start = header.range.start; } // Repeat the gutter below a header (hence why we don't @@ -2066,32 +2076,37 @@ fn expand_row_group( /// Check if a cell's fixed row would conflict with a header or footer. fn check_for_conflicting_cell_row( + header_rows: &SmallBitSet, headers: &[Repeatable
], footer: Option<&(usize, Span, Footer)>, cell_y: usize, rowspan: usize, ) -> HintedStrResult<()> { - // NOTE: y + rowspan >, not >=, header.start, to check if the rowspan - // enters the header. For example, consider a rowspan of 1: if - // `y + 1 = header.start` holds, that means `y < header.start`, and it - // only occupies one row (`y`), so the cell is actually not in - // conflict. - if headers - .iter() - .any(|header| cell_y < header.range.end && cell_y + rowspan > header.range.start) + if !headers.is_empty() + && let Some(row) = + (cell_y..cell_y + rowspan).find(|&row| header_rows.contains(row)) { bail!( - "cell would conflict with header spanning the same position"; + "cell would conflict with header also spanning row {row}"; hint: "try moving the cell or the header" ); } + // NOTE: y + rowspan >, not >=, footer.start, to check if the rowspan + // enters the footer. For example, consider a rowspan of 1: if + // `y + 1 = footer.start` holds, that means `y < footer.start`, and it + // only occupies one row (`y`), so the cell is actually not in + // conflict. if let Some((_, _, footer)) = footer && cell_y < footer.end && cell_y + rowspan > footer.start { + let row = (cell_y..cell_y + rowspan) + .find(|row| (footer.start..footer.end).contains(row)) + .unwrap(); + bail!( - "cell would conflict with footer spanning the same position"; + "cell would conflict with footer also spanning row {row}"; hint: "try reducing the cell's rowspan or moving the footer" ); } @@ -2114,11 +2129,11 @@ fn resolve_cell_position( cell_y: Smart, colspan: usize, rowspan: usize, + header_rows: &SmallBitSet, headers: &[Repeatable
], footer: Option<&(usize, Span, Footer)>, resolved_cells: &[Option], auto_index: &mut usize, - next_header: &mut usize, first_available_row: usize, columns: usize, in_row_group: bool, @@ -2140,12 +2155,11 @@ fn resolve_cell_position( // but automatically-positioned cells will avoid conflicts by // simply skipping existing cells, headers and footers. let resolved_index = find_next_available_position( - headers, + header_rows, footer, resolved_cells, columns, *auto_index, - next_header, false, )?; @@ -2182,7 +2196,13 @@ fn resolve_cell_position( // footer (but only if it isn't already in one, otherwise there // will already be a separate check). if !in_row_group { - check_for_conflicting_cell_row(headers, footer, cell_y, rowspan)?; + check_for_conflicting_cell_row( + header_rows, + headers, + footer, + cell_y, + rowspan, + )?; } cell_index(cell_x, cell_y) @@ -2200,26 +2220,11 @@ fn resolve_cell_position( // ('None'), in which case we'd create a new row to place this // cell in. find_next_available_position( - headers, + header_rows, footer, resolved_cells, columns, initial_index, - // Make our own copy of the 'next_header' counter, since it - // should only be updated by auto cells. However, we cannot - // start with the same value as we are searching from the - // start, and not from 'auto_index', so auto cells might - // have skipped some headers already which this cell will - // also need to skip. - // - // We could, in theory, keep a separate 'next_header' - // counter for cells with fixed columns. But then we would - // need one for every column, and much like how there isn't - // an index counter for each column either, the potential - // speed gain seems less relevant for a less used feature. - // Still, it is something to consider for the future if - // this turns out to be a bottleneck in important cases. - &mut 0, true, ) } @@ -2230,7 +2235,13 @@ fn resolve_cell_position( // footer (but only if it isn't already in one, otherwise there // will already be a separate check). if !in_row_group { - check_for_conflicting_cell_row(headers, footer, cell_y, rowspan)?; + check_for_conflicting_cell_row( + header_rows, + headers, + footer, + cell_y, + rowspan, + )?; } // Let's find the first column which has that row available. @@ -2267,12 +2278,11 @@ fn resolve_cell_position( /// the column. That is used to find a position for a fixed column cell. #[inline] fn find_next_available_position( - headers: &[Repeatable
], + header_rows: &SmallBitSet, footer: Option<&(usize, Span, Footer)>, resolved_cells: &[Option>], columns: usize, initial_index: usize, - next_header: &mut usize, skip_rows: bool, ) -> HintedStrResult { let mut resolved_index = initial_index; @@ -2296,33 +2306,30 @@ fn find_next_available_position( // would become impractically large before this overflows. resolved_index += 1; } - } else if let Some(header) = headers - .get(*next_header) - .filter(|header| resolved_index >= header.range.start * columns) - { - // Skip header (can't place a cell inside it from outside it). - // No changes needed if we already passed this header (which - // also triggers this branch) - in that case, we only update the - // counter. - if resolved_index < header.range.end * columns { - resolved_index = header.range.end * columns; - - if skip_rows { - // Ensure the cell's chosen column is kept after the - // header. - resolved_index += initial_index % columns; - } + } else if header_rows.contains(resolved_index / columns) { + // Skip header rows (can't place a cell inside it from outside it). + if skip_rows { + // Ensure the cell's chosen column is kept after the header. + resolved_index += columns; + } else { + // Skip to the start of the next row. + // + // Add 1 to resolved index to force moving to the next row if + // this is at the start of one. At the end of one, '+ 1' + // already pushes to the next one and 'next_multiple_of' does + // not modify it, so nothing bad happens then either. + resolved_index = (resolved_index + 1).next_multiple_of(columns); } - - // From now on, only check the headers afterwards. - *next_header += 1; - } else if let Some((footer_end, _, _)) = footer.filter(|(end, _, footer)| { - resolved_index >= footer.start * columns && resolved_index < *end * columns - }) { + } else if let Some((footer_end, _, footer)) = footer + && resolved_index >= footer.start * columns + && resolved_index < *footer_end * columns + { // Skip footer, for the same reason. resolved_index = *footer_end * columns; if skip_rows { + // Ensure the cell's chosen column is kept after the + // footer. resolved_index += initial_index % columns; } } else { diff --git a/tests/ref/grid-footer-rowbreak-line.png b/tests/ref/grid-footer-rowbreak-line.png new file mode 100644 index 0000000000000000000000000000000000000000..882c00ce31d54fbfc23f245468b35dc3d950a56e GIT binary patch literal 327 zcmV-N0l5B&P)ajLU;~W$9}aal@P-dWL^LJqr+b2g^Whjn!sh$RAmM-8 z=MZqM+obU?Fm82^lu}EWX9v3lB-rJn*ouS$ynisx(w|g^uC`#9*A2Ycgq>>Oa9Tt( ZQV-q*2-R;3UvdBd002ovPDHLkV1nDJpg{lt literal 0 HcmV?d00001 diff --git a/tests/ref/grid-header-multiple-unordered.png b/tests/ref/grid-header-multiple-unordered.png new file mode 100644 index 0000000000000000000000000000000000000000..2d8bd13bbee191d82c79cc27c31445a2981290f0 GIT binary patch literal 469 zcmeAS@N?(olHy`uVBq!ia0vp^6+pb014uBWd~1Egz`!`i)5S5Q;?~<6=e1c1CD=av z)t)oQEPbn{rn!YTZZeb2Su%VLxgw*P*>5&Wt> zOPXo-#4`*men;Y_80Ys+4n1_B)uTk1DSGPKGLAWs2GI&1?tIBx+t-l3Tjnh{Gq<-Z(U{>fLvvqkbJ-m%&b4Oy7s;JVm*dB-W|)? zmRw0bew(TBefBzo4JB-!1#bBCrU-73+auj{U|CT#f1|j6iVTzgF^3Pn$%`yo8lFbh z|N3&R%|7zpHsXOO_br>Z2AH09bIdA$Ce!V(Se0jS1xvX-V)2!xokcfC zRH}Y@_s_l817YzRt)KA&6VwaCo(xRMb;{}NfLOdY^M%&6fv`9??#H!917Y!rd>H3cE<3l)@BdqR`}lnJlqLWFH|MS% zZFAAu;%0*%8{_{CfW`lezbyMQAVJ+fb^gG_@kilD1C+R`2PjoIxU>KO002ovPDHLkV1kM&i4Xt) literal 0 HcmV?d00001 diff --git a/tests/ref/grid-header-rowbreak-auto-pos.png b/tests/ref/grid-header-rowbreak-auto-pos.png new file mode 100644 index 0000000000000000000000000000000000000000..633bfa32f8aaa0b8672d04ae1997d5bde41316b6 GIT binary patch literal 452 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 diff --git a/tests/ref/grid-subheaders-multi-page-row.png b/tests/ref/grid-subheaders-multi-page-row.png index 637ca3fb10f78af61372f9fdc88a74a7b3a19d0a..7aa22ff45b917de9f3ba7cc811e4d603384ed4db 100644 GIT binary patch delta 1161 zcmV;41a|wC37rX$B!A9OOjJex|Ns9%Hvazp`T6;ekB_0Dq5S;(`1troM@MvYbiBO0 zpP!$Vm6f@OH1YDSRmzRi$h=PKGUS3}P{ry;2Sn28M=jZ2odVhLiVq&MKr>m>0)YR04 zg@vxJuF1*CxVX5#zrXJ8?rUpn>gwvSu&~q9)6UM$^z`(kq@;FscD1#&L_|d2-`_<= zMU<43@9*z~goI8`PF-DHQ&UregM%^M4{DBE7x6R#sNZ%E~r2Hk_QC+}zx0X=&o(;?~yI+uPfHeSPEO<9vL4 zMn*=judn9j<|-;Gb8~aRz`#L4LB__$^78U(YHCSINuHjbva+(!(9oEen1_dlNJvOp zT3Wigx=&9};Nals=;&c#VWOg<`}_MsLPCayhTh)Z>woL(fPjGY_4TEtrP0yRd3kwc zWMt&zN3#Kgprk&(s4#k91v)z#I_&CSBX!qU>x?Ck8=*x35|`iqN;x3{<1+1aV7 zsjRH5@$vC*Z*R!R$N&HU>=9Zo0007tNkl&<2}8qlm%A7mW{yM0*=9JPq551!0%@^)=!lm?fa)t)>yw#zq@_;z zuR0(hJQN8bE!Bo=aY*PJY$@IPsIT9Eg#O(26G%%HxM$}eVaHYzw{A8;BMgIBI1*TU zI)ASbUV3aTE@*@?)>mVxgcIG3Pzk&5-_r>r2$gVa;YpqFnM&w{0frf7m|=#Ah_<`G zO8Df7h@}4!5<-oFZ!LiVDmrVz2ESD7np4DTY-!P)neEHhskk{sG)Bkc=9ID0U``pMAFC_PDPrV+#9oGF(kM&*+L0000V;i`Q6>!{QUgF!oruAmzkNF zM@L7wxw(mniE?ssR8&;>`1sP&(*FMbx3{;uySu8Ys*sS7p`oGG)zxKXWuBg%f`Wp* zy}fyPd7z-6V`F1`dwc8a>z$pQ%F4>q)6=G=ruO#sr>CccgnxwNuUGVVmYinzil$2>{Xa;baZr&kB@V6bLi;kz`(%H z&d%)Y?C$RFzrVk-va)`Deq>~1oSdAWpP%XJ=}SvXi;IhghlkYE)cgDUqobqd=H}Vi z+5P?f|3Nl~GzHNB00N6iL_t(|+U?p!S6g8eh2h;8UV^(@sZg{^-Q9~6YEbG(k>Va4 zk{jvo?SJql-<)&WWIwY#dRH>ID{??YME{eC@u}a?F!`%+8yeR03-+O5E7y|-4S|yM zqhTbH2cD$PDtk71B2^wpc-ZY;2-~yxwpCgQ5e3g@5amu)xdjn5^OJ~DP#J`X($_f~7RLfHG* z8h_b?LKs`a)ldnCp4IAv(F2`u=M9~(=NiBV2*V6B%rL_YGb|hCKY&C;rXx!D&Jz*w zkc@|9|I{JbVw%z+S+Wy49L|uX6aG2-eMRRLBV|b`oSz2;uKDC5FBJS(&&oz(*(}b1 zhE+cfpketlbSy1m4;tJb2L@r|Pl^mP%#)-85r4dHA|fIllJSs?h=_+|JS5{G84t;L zNXA1l9+L5pjE7|LE@Dn8=gtpon^Q#Ng*QHP%BZ{;F{g-{iuy8hil{xFS8C4q7O1Wp zF{g-N=>4cUWxRc7P8mu2djWHbD6iOEFebl3AC87&2g~LZaU%Q7S#!!rNj0YoPbN~# zDQIG2XLrY(@ona-Yt5V@rdJRA<`fZG*^ii0#!xt9P7%SO4^8G2;r=vt+MMw%JtNg` zP7$Z9?QiCkvAJbV8FOE(ljamLJGNV5Ony2dA|j&0@E4P($1e}jpvnLM002ovPDHLk FV1m)^SH1uM diff --git a/tests/ref/grid-subheaders-multi-page-rowspan-gutter.png b/tests/ref/grid-subheaders-multi-page-rowspan-gutter.png index 53beeb02e4183db6499534946ce75b5180d79e52..564732fd1311e0b42a5f6df3027aa22314f0c443 100644 GIT binary patch delta 1624 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 KMNUMnLSTY#b56DZ delta 1170 zcmV;D1a13`38o2FK+>yMBItbaZsI zw6uhTgpZGp`T6;(s;Y8wa=E#=gM)+G+S*@VUzL@WDk>_htbeS$yu6>EpOTW2-rnAG zb92DJz=wy2ZfKPvGcz;b;NT)6B2-jVA0HpJwY9}EDa*^t z@bK{Q@$vNZ^nZwmh|bQ=l$4ZfYilJXCBwtRncqsvW@ctNIXP2PQ(awM zoSd9#X=%#J%0E9pxVX4gRaI?mZHbAAsi~S%y}j4h*E>5q?SJj<^78Wi{rzKOW1yg*q@<+A z#>PTILZ+ssdwY8s85#Tg`+|ak=H}-3`1sP&(zmy_i;Ih!o16Un{IasL>+9=4KtP_J zp5)}@K|w+G_V!p+sjKKL2t8GK*Ek{dnF_k`F8IrKt4HR4|aso=Y)MeWQPi^E%i+W z=`-@dJMDoI*4`*G%!U@dP~xR4fecoU5lT$Iuz$4lA+3IS4obWo4fLHwhIddy(;-u= zy$Y8y4#?L_6!U6AVAZ>c~dAnehUId?%Z}F&jcYNIc^+ptQGCGf~ ztU^S&+km`M^xbXBK9w-~KOM|I)tLF5eX0?xs4B`nl~`QZ)AX-%^3lWR2K-0A{)Ru= zeSd*h9TMT%o%k777*4H4%ou)e4yX3Ew2+eFlxU2R;oy1dZa^PZIW!Nx1hO#At^8fh!(BK;!pO`wWI_x+Cj0`x< z{i?$r8<6M%aLS=JZ11r-5%xtwLq}&?ntv6B0Sf?s3Wl-Rwd+d5nHXj`C&SoSFNKIH zltF^_^$SEqXXhXywh0-@Fl1PlHHb(d#m|jIloPfI;V(}_#331nWLs)TR#Pc7BrDWG zhNkm-Wx|`2A1y)yltXnwVOs+PEV&B51t8#uekFzcvJqK@gwB0fgM_`ckWp8IDt}0* z-+!PDX?#P^FvAQp%rL_YGt4l<3^U9y!yJ-vNX8)k diff --git a/tests/ref/grid-subheaders-multi-page-rowspan.png b/tests/ref/grid-subheaders-multi-page-rowspan.png index 342b05695b9a8dd07766cad52ab259497b9c76da..45bcfc8f928484148ef180f9941a079a0c79b69b 100644 GIT binary patch delta 1056 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