tdf#157911 sw floattable: fix inconsistent inferred bottom border on split

The bugdoc has a split table between page 1 and page 2. The last row of
page 1 has a half bottom border: it starts on the left of the table,
but finishes earlier than the right of the table. This is since commit
08aea5526c75ff4c5385e960bd940f10ffa19cd5 (tdf#156351 sw floattable: fix
missing bottom border in master table, 2023-08-21).

The trouble is that Writer table borders are really at a cell-level (and
not at row or table level), the current partial border happens because
the first row has merged cells and the last row on page 1 doesn't have
merged cells, so the layout can't do a 1:1 mapping between the first row
and last row cells. It's also far from clear if the fixed result should
be no bottom border or a table-width bottom border:

- Word documents can have cell-level borders (where no inferred border
  is wanted) and table-level borders (where inferred borders are
  wanted), see the tdf#156351 bugdoc for a case where such inferring is
  wanted

- In case only cell-level borders are defined, then Word doesn't do such
  inferring

Fix the problem by always inferring such borders, because:

- Writer already did this in some cases for a long time, see commit
  a4da71fb824f2d4ecc7c01f4deb2865ba52f5f4c (INTEGRATION: CWS fmebugs04
  (1.115.46); FILE MERGED 2008/05/13 13:56:19 fme 1.115.46.2: #i9860# Top
  border for tables - correction 2008/05/13 13:49:23 fme 1.115.46.1:
  #i9860# Top border for tables, 2008-06-06)

- The Word UI creates table borders by default, so the majority of the
  DOCX documents also want this inferring

An alternative could be to only do such inferring for Word documents
with a compat flag, but that looks poor, given that Word doesn't always
do such inferring itself, either.

Change-Id: I052e4591e99d066c3109e8ab8b590e97c8aebd36
Reviewed-on: https://gerrit.libreoffice.org/c/core/+/159429
Reviewed-by: Miklos Vajna <vmiklos@collabora.com>
Tested-by: Jenkins
diff --git a/sw/qa/core/layout/data/split-table-merged-border.odt b/sw/qa/core/layout/data/split-table-merged-border.odt
new file mode 100644
index 0000000..122bfd4
--- /dev/null
+++ b/sw/qa/core/layout/data/split-table-merged-border.odt
Binary files differ
diff --git a/sw/qa/core/layout/paintfrm.cxx b/sw/qa/core/layout/paintfrm.cxx
index ad09405..baa8580 100644
--- a/sw/qa/core/layout/paintfrm.cxx
+++ b/sw/qa/core/layout/paintfrm.cxx
@@ -109,6 +109,55 @@ CPPUNIT_TEST_FIXTURE(Test, testRTLBorderMerge)
    // i.e. the 2nd and 5th vertical border was missing.
    CPPUNIT_ASSERT_EQUAL(6, nVerticalBorders);
}

CPPUNIT_TEST_FIXTURE(Test, testSplitTableMergedBorder)
{
    // Given a document with a split table, first row in frame 1 has merged cells:
    createSwDoc("split-table-merged-border.odt");
    SwXTextDocument* pTextDoc = dynamic_cast<SwXTextDocument*>(mxComponent.get());
    SwDocShell* pShell = pTextDoc->GetDocShell();

    // When rendering that document:
    std::shared_ptr<GDIMetaFile> xMetaFile = pShell->GetPreviewMetaFile();

    // Then make sure that the master table has a bottom border with the correct widths:
    MetafileXmlDump aDumper;
    xmlDocUniquePtr pXmlDoc = dumpAndParse(aDumper, *xMetaFile);
    xmlXPathObjectPtr pXmlObj = getXPathNode(pXmlDoc, "//polyline[@style='solid']/point");
    xmlNodeSetPtr pXmlNodes = pXmlObj->nodesetval;
    std::set<int> aHorizontalBorderStarts;
    std::set<int> aHorizontalBorderEnds;
    // Collect the horizontal borders:
    for (int i = 0; i < xmlXPathNodeSetGetLength(pXmlNodes); i += 2)
    {
        xmlNodePtr pStart = pXmlNodes->nodeTab[i];
        xmlNodePtr pEnd = pXmlNodes->nodeTab[i + 1];
        xmlChar* pStartY = xmlGetProp(pStart, BAD_CAST("y"));
        xmlChar* pEndY = xmlGetProp(pEnd, BAD_CAST("y"));
        sal_Int32 nStartY = o3tl::toInt32(reinterpret_cast<char const*>(pStartY));
        sal_Int32 nEndY = o3tl::toInt32(reinterpret_cast<char const*>(pEndY));
        if (nStartY != nEndY)
        {
            // Vertical border.
            continue;
        }

        xmlChar* pStartX = xmlGetProp(pStart, BAD_CAST("x"));
        xmlChar* pEndX = xmlGetProp(pEnd, BAD_CAST("x"));
        sal_Int32 nStartX = o3tl::toInt32(reinterpret_cast<char const*>(pStartX));
        sal_Int32 nEndX = o3tl::toInt32(reinterpret_cast<char const*>(pEndX));
        aHorizontalBorderStarts.insert(nStartX);
        aHorizontalBorderEnds.insert(nEndX);
    }
    xmlXPathFreeObject(pXmlObj);
    CPPUNIT_ASSERT_EQUAL(static_cast<size_t>(2), aHorizontalBorderStarts.size());
    // Without the accompanying fix in place, this test would have failed with:
    // - Expected: 2
    // - Actual  : 3
    // i.e. the frame 1 bottom border ended sooner than expected, resulting in a buggy, partial
    // bottom border.
    CPPUNIT_ASSERT_EQUAL(static_cast<size_t>(2), aHorizontalBorderEnds.size());
}
}

/* vim:set shiftwidth=4 softtabstop=4 expandtab: */
diff --git a/sw/source/core/layout/paintfrm.cxx b/sw/source/core/layout/paintfrm.cxx
index e9ba79f..d4e6f87 100644
--- a/sw/source/core/layout/paintfrm.cxx
+++ b/sw/source/core/layout/paintfrm.cxx
@@ -2903,19 +2903,21 @@ void SwTabFramePainter::InsertFollowTopBorder(const SwFrame& rFrame, const SvxBo
    }

    const SwFrame* pLastCell = pLastRow->GetLower();
    if (!pLastCell)
    {
        return;
    }

    for (int i = 0; i < nCol; ++i)
    {
        if (!pLastCell)
        if (!pLastCell->GetNext())
        {
            // Reference row has merged cells, work with the last possible one.
            break;
        }

        pLastCell = pLastCell->GetNext();
    }
    if (!pLastCell)
    {
        return;
    }

    SwBorderAttrAccess aAccess(SwFrame::GetCache(), pLastCell);
    const SwBorderAttrs& rAttrs = *aAccess.Get();
@@ -2976,19 +2978,21 @@ void SwTabFramePainter::InsertMasterBottomBorder(const SwFrame& rFrame, const Sv
    }

    const SwFrame* pFirstCell = pFirstRow->GetLower();
    if (!pFirstCell)
    {
        return;
    }

    for (int i = 0; i < nCol; ++i)
    {
        if (!pFirstCell)
        if (!pFirstCell->GetNext())
        {
            // Reference row has merged cells, work with the last possible one.
            break;
        }

        pFirstCell = pFirstCell->GetNext();
    }
    if (!pFirstCell)
    {
        return;
    }

    SwBorderAttrAccess aAccess(SwFrame::GetCache(), pFirstCell);
    const SwBorderAttrs& rAttrs = *aAccess.Get();