@@ -236,6 +236,10 @@ public final class DocxSemanticBackend implements SemanticBackend<byte[]> {
236236 // How far each paragraph a container pulled above its cell rises inside its own line: see
237237 // riseIntoItsLine.
238238 private final java .util .Map <ParagraphNode , Double > risenLines = new java .util .IdentityHashMap <>();
239+ // The cells of a table's grid that hold a composed node, whose row the table holds at the
240+ // page's height: see writeTableWithItsOwnSpacing.
241+ private final java .util .Set <org .openxmlformats .schemas .wordprocessingml .x2006 .main .CTTc > tablesCells =
242+ java .util .Collections .newSetFromMap (new java .util .IdentityHashMap <>());
239243 // How far above the page's first line a paragraph's Word line starts, in points, where its
240244 // lines took the space between them from the space above it: see applyLineGap.
241245 private final java .util .Map <org .openxmlformats .schemas .wordprocessingml .x2006 .main .CTP , Double > lineTopsTakenIn =
@@ -608,6 +612,7 @@ private byte[] write(List<SemanticSection> sections, Path outputFile) throws Exc
608612 stackedLineHeights .clear ();
609613 lineTopsTakenIn .clear ();
610614 risenLines .clear ();
615+ tablesCells .clear ();
611616 picturesDrawnBeside .clear ();
612617 listNumbering .clear ();
613618 report = new DocxExportReport .Builder ();
@@ -2040,6 +2045,10 @@ private DocxRules.Rule ruleOf(DocumentNode node) {
20402045
20412046 private void dispatchNode (XWPFDocument document , DocumentNode node ) throws Exception {
20422047 DocxRules .Rule rule = ruleOf (node );
2048+ if (overTheFlowDepth == 0 && (node instanceof ParagraphNode
2049+ || node instanceof com .demcha .compose .document .node .PageReferenceNode || rule != null )) {
2050+ standsIntoTheSpaceAbove (node );
2051+ }
20432052 if (node instanceof ParagraphNode paragraph ) {
20442053 writeParagraph (document , paragraph );
20452054 } else if (node instanceof com .demcha .compose .document .node .PageReferenceNode reference ) {
@@ -2979,7 +2988,9 @@ private void holdTheSpaceAboveATable(XWPFDocument document) {
29792988 boolean tableAbove = currentCell == null
29802989 ? endsWithATable (document .getBodyElements ())
29812990 : cellEndsWithItsTableCloser ();
2982- if (pendingSpacingAfter > 0 && lastBodyParagraph == null && !tableAbove ) {
2991+ // The space owed is the block's own edge and the edges of the containers opened
2992+ // around it, which are carried down to it rather than owed.
2993+ if (pendingSpacingAfter + carriedSpacingBefore - borderBelow > 0 && lastBodyParagraph == null && !tableAbove ) {
29832994 holdToHairline (newBodyParagraph (document ));
29842995 }
29852996 }
@@ -3172,9 +3183,10 @@ private void riseIntoItsLine(DocumentNode node) {
31723183 return ;
31733184 }
31743185 // What the paragraph would have above it before its own edge, which is written apart
3175- // (applyVerticalSpacing): the edges carried down to it and the space the block before it
3176- // owes, less a border standing below that block. Only what that comes short of zero is
3177- // a rise; the rest is written as space.
3186+ // (applyVerticalSpacing, or standsIntoTheSpaceAbove for a negative one, after this): the
3187+ // edges carried down to it and the space the block before it owes, less a border
3188+ // standing below that block. Only what that comes short of zero is a rise; the rest is
3189+ // written as space.
31783190 double rise = -(carriedSpacingBefore + pendingSpacingAfter - borderBelow );
31793191 if (!(rise > 0.01 ) || layout .lineCount (first ) != 1 || stackedLineHeights .containsKey (first )) {
31803192 return ;
@@ -3719,6 +3731,28 @@ private static boolean drawn(CTBorder border) {
37193731 return border != null && border .getVal () != STBorder .NONE && border .getVal () != STBorder .NIL ;
37203732 }
37213733
3734+ /**
3735+ * Takes a paragraph's, page reference's or rule's negative top edge out of the space owed
3736+ * above it.
3737+ *
3738+ * <p>Word has no negative space above a paragraph, and {@link #applyVerticalSpacing}
3739+ * writes an edge only where it is positive, so an edge pulling the block up was dropped
3740+ * and the block stood that much low, with everything after it. {@code PaymentsInvoice}'s
3741+ * header rule is pulled 16.4pt up from the foot of the stack above it, whose band runs
3742+ * past its content: the pull was lost, and the rule and the page under it stood 10pt low,
3743+ * the 16.4pt less the 6.3pt its metadata grid had lost above it. The pull is added
3744+ * to the edges carried down to the block, as a container's negative edge already is, so
3745+ * {@link #newBodyParagraph} nets it against everything owed above, as the page sums it;
3746+ * what that cannot give stays unwritten, as before. Text laid over the flow owes no
3747+ * space ({@link #writeOverTheFlow}), so its edges move nothing.</p>
3748+ */
3749+ private void standsIntoTheSpaceAbove (DocumentNode node ) {
3750+ double edge = node .margin ().top () + node .padding ().top ();
3751+ if (edge < 0 ) {
3752+ carriedSpacingBefore += edge ;
3753+ }
3754+ }
3755+
37223756 /**
37233757 * Gives back space already owed above the next block, most recent first: text standing
37243758 * above its band took it on the page.
@@ -5982,18 +6016,21 @@ private static boolean startsWith(byte[] bytes, int... signature) {
59826016 * below the paragraph before it, and the space below one is the space above the
59836017 * paragraph after. Both go through the debt every other gap goes through
59846018 * ({@link #owePendingSpacingAfter}), so a table between two paragraphs reads the same as
5985- * two paragraphs with a gap between them — and a table with nothing above it loses that
5986- * edge, which is the one thing Word genuinely cannot hold.</p>
6019+ * two paragraphs with a gap between them. With nothing above it — opening the body, or a
6020+ * row opening any cell but a table's — a paragraph a tenth of a point tall holds that edge
6021+ * ({@link #holdTheSpaceAboveATable}); a table opening a cell still loses it.</p>
59876022 */
59886023 private void writeTableWithItsOwnSpacing (XWPFDocument document , DocumentNode node )
59896024 throws Exception {
59906025 owePendingSpacingAfter (node .margin ().top () + node .padding ().top ());
5991- if (node instanceof RowNode && currentCell != null && currentCell == panelCell ) {
5992- // At the top of a panel nothing above holds that space: MerchantInvoice's due-date
6026+ if (node instanceof RowNode && currentCell != null && ! tablesCells . contains ( currentCell . getCTTc ()) ) {
6027+ // At the top of a cell nothing above holds that space: MerchantInvoice's due-date
59936028 // row lost its 16.7pt of top padding and stood against the card's top edge, once the
5994- // card held the page's height. A row in a table's cell keeps to the row height the
5995- // table holds (holdRowHeight). A table is left as it was: holding its space moved
5996- // ObsidianInvoice's line items 6pt below the page's.
6029+ // card held the page's height, and PaymentsInvoice's metadata grid the 6.2pt its
6030+ // column is padded down by, standing 6.3pt above the issuer beside it. A row in a
6031+ // table's cell keeps to the row height the table holds (holdRowHeight), and a table
6032+ // is left as it was: holding the space of either moved ObsidianInvoice's line items
6033+ // 6pt below the page's.
59976034 holdTheSpaceAboveATable (document );
59986035 }
59996036 if (node instanceof RowNode row ) {
@@ -6592,6 +6629,7 @@ private void writeCellContent(XWPFTableCell cell, TableGrid.Placement placement,
65926629 // exported it as an empty cell.
65936630 double previous = currentCellWidth ;
65946631 currentCellWidth = usableWidthOf (cell , placement );
6632+ tablesCells .add (cell .getCTTc ());
65956633 try {
65966634 writeCellBody (cell , source .content ());
65976635 } finally {
0 commit comments