diff --git a/sc/inc/dbdata.hxx b/sc/inc/dbdata.hxx index e8247f0907d8..f3514eac95ac 100644 --- a/sc/inc/dbdata.hxx +++ b/sc/inc/dbdata.hxx @@ -225,6 +225,7 @@ public: void MoveTo(SCTAB nTab, SCCOL nCol1, SCROW nRow1, SCCOL nCol2, SCROW nRow2, SCCOL nUpdateCol = -1); void SetByRow(bool bByR) { bByRow = bByR; } + bool IsByRow() const { return bByRow; } bool HasHeader() const { return bHasHeader; } void SetHeader(bool bHasH) { bHasHeader = bHasH; } bool HasTotals() const { return bHasTotals; } diff --git a/sc/qa/extras/scpdfexport.cxx b/sc/qa/extras/scpdfexport.cxx index 36072eeb0a0e..1b306ef6a887 100644 --- a/sc/qa/extras/scpdfexport.cxx +++ b/sc/qa/extras/scpdfexport.cxx @@ -1097,6 +1097,75 @@ CPPUNIT_TEST_FIXTURE(ScPDFExportTest, testPageRangeSkipsSheetStart) CPPUNIT_ASSERT_EQUAL(2, nOutlineItems); } +CPPUNIT_TEST_FIXTURE(ScPDFExportTest, testDatabaseRangeHeader) +{ + loadFromFile(u"db-range-header.fods"); + exportWholeDocumentToPDF({ comphelper::makePropertyValue(u"UseTaggedPDF"_ustr, true) }); + + vcl::filter::PDFDocument aDocument; + CPPUNIT_ASSERT(aDocument.Read(*maTempFile.GetStream(StreamMode::READ))); + + std::vector aTables; + for (const auto& rDocElement : aDocument.GetElements()) + { + auto pObject = dynamic_cast(rDocElement.get()); + if (!pObject) + continue; + auto pType = dynamic_cast(pObject->Lookup("S"_ostr)); + if (pType && pType->GetValue() == "Table") + aTables.push_back(pObject); + } + // the sheet fits one page, so every row it has is in this one table + CPPUNIT_ASSERT_EQUAL(size_t(1), aTables.size()); + + vcl::filter::PDFObjectElement* pTable = aTables.front(); + + OStringBuffer aCells; + auto pRows = dynamic_cast(pTable->Lookup("K"_ostr)); + CPPUNIT_ASSERT(pRows); + for (auto* pRowElement : pRows->GetElements()) + { + auto pRow = dynamic_cast(pRowElement); + CPPUNIT_ASSERT(pRow); + CPPUNIT_ASSERT(pRow->LookupObject()); + auto pCells + = dynamic_cast(pRow->LookupObject()->Lookup("K"_ostr)); + CPPUNIT_ASSERT(pCells); + for (auto* pCellElement : pCells->GetElements()) + { + auto pCell = dynamic_cast(pCellElement); + CPPUNIT_ASSERT(pCell); + CPPUNIT_ASSERT(pCell->LookupObject()); + auto pType = dynamic_cast( + pCell->LookupObject()->Lookup("S"_ostr)); + CPPUNIT_ASSERT(pType); + // the scope a header cell announces, and nothing for a data cell + OString aScope; + if (vcl::filter::PDFElement* pElement = pCell->LookupObject()->Lookup("A"_ostr)) + { + // one owner dictionary, which several attribute groups would turn into an array + auto pAttributes = dynamic_cast(pElement); + CPPUNIT_ASSERT(pAttributes); + auto pValue = dynamic_cast( + pAttributes->LookupElement("Scope"_ostr)); + CPPUNIT_ASSERT(pValue); + aScope = "/" + pValue->GetValue(); + } + aCells.append(pType->GetValue() + aScope + " "); + } + aCells.append("| "); + } + + // Without the fix every cell was a TD, so this read + // "TD TD | TD TD | TD TD | TD | TD TD | TD TD | TD TD | TD TD | ". + // + // the labels of the first range and of the one kept by column, and nothing in the range + // defined without labels or outside every range + CPPUNIT_ASSERT_EQUAL("TH/Column TH/Column | TD TD | TD TD | TD | TD TD | TD TD | " + "TH/Row TD | TH/Row TD | "_ostr, + aCells.makeStringAndClear()); +} + // just needs to not crash on export to pdf CPPUNIT_TEST_FIXTURE(ScPDFExportTest, testForcepoint97) { diff --git a/sc/qa/extras/testdocuments/db-range-header.fods b/sc/qa/extras/testdocuments/db-range-header.fods new file mode 100644 index 000000000000..3762add08011 --- /dev/null +++ b/sc/qa/extras/testdocuments/db-range-header.fods @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + + + + + + + Name + Quantity + + + Bolt + 12 + + + Nut + 34 + + + + + + Outside + + + + + + Plain + Text + + + Screw + 56 + + + + + + Width + 78 + + + Height + 90 + + + + + + + + + + diff --git a/sc/source/ui/inc/output.hxx b/sc/source/ui/inc/output.hxx index ae92d19430c8..ae243ab04e6b 100644 --- a/sc/source/ui/inc/output.hxx +++ b/sc/source/ui/inc/output.hxx @@ -89,7 +89,7 @@ struct ScEnhancedPDFState TableDataIdMap m_TableDataMap; /// the Worksheet element of every sheet drawn so far std::unordered_map m_WorksheetIds; - /// the TD element at every grid position drawn so far, unlike m_TableDataMap kept for the + /// the cell element at every grid position drawn so far, unlike m_TableDataMap kept for the /// whole export; the grid position, not the cell the content came from, is what shares a /// page with the destination that names it std::unordered_map m_CellIds; diff --git a/sc/source/ui/view/output2.cxx b/sc/source/ui/view/output2.cxx index 4bb8548e58da..184dc882c8b2 100644 --- a/sc/source/ui/view/output2.cxx +++ b/sc/source/ui/view/output2.cxx @@ -73,6 +73,7 @@ #include #include #include +#include #include #include @@ -1487,6 +1488,40 @@ static SvxCellHorJustify getAlignmentFromContext( SvxCellHorJustify eInHorJust, return eHorJustContext; } +/// what a database range's labels head, where this position holds one of them +static vcl::pdf::PDFWriter::StructAttributeValue +lcl_GetLabelScope(const ScDocument& rDoc, SCTAB nTab, SCCOL nCol, SCROW nRow) +{ + const ScDBData* pDBData(rDoc.GetDBAtCursor(nCol, nRow, nTab, ScDBDataPortion::AREA)); + if (!pDBData || !pDBData->HasHeader()) + return vcl::pdf::PDFWriter::Invalid; + + ScRange aRange; + pDBData->GetArea(aRange); + // a range the user works left to right keeps its labels in the first column + if (!pDBData->IsByRow()) + return nCol == aRange.aStart.Col() ? vcl::pdf::PDFWriter::Row + : vcl::pdf::PDFWriter::Invalid; + + return nRow == aRange.aStart.Row() ? vcl::pdf::PDFWriter::Column + : vcl::pdf::PDFWriter::Invalid; +} + +/// the element for a cell, a TH where a database range says the cell holds one of its labels +static sal_Int32 lcl_OpenCellElement(vcl::PDFExtOutDevData& rPDF, const ScDocument& rDoc, + SCTAB nTab, SCCOL nCol, SCROW nRow) +{ + const vcl::pdf::PDFWriter::StructAttributeValue eScope( + lcl_GetLabelScope(rDoc, nTab, nCol, nRow)); + if (eScope == vcl::pdf::PDFWriter::Invalid) + return rPDF.WrapBeginStructureElement(vcl::pdf::StructElement::TableData, u"TD"_ustr); + + const sal_Int32 nId( + rPDF.WrapBeginStructureElement(vcl::pdf::StructElement::TableHeader, u"TH"_ustr)); + rPDF.SetStructureAttribute(vcl::pdf::PDFWriter::Scope, eScope); + return nId; +} + void ScOutputData::DrawStrings( bool bPixelToLogic ) { LayoutStrings(bPixelToLogic); @@ -1963,7 +1998,7 @@ void ScOutputData::LayoutStringsImpl(bool const bPixelToLogic, RowInfo* const pT pThisRowInfo->basicCellInfo(nMarkX).bEditEngine = true; bDoCell = false; // don't draw here - // Mark the tagged "TD" structure element to be drawn in DrawEdit + // Mark the tagged cell structure element to be drawn in DrawEdit if (bTaggedPDF) { if (bReopenRowTag) @@ -1978,10 +2013,7 @@ void ScOutputData::LayoutStringsImpl(bool const bPixelToLogic, RowInfo* const pT bReopenRowTag = true; } - pPDF->WrapBeginStructureElement(vcl::pdf::StructElement::TableData, - u"TD"_ustr); - - sal_Int32 nId = pPDF->GetCurrentStructureElement(); + const sal_Int32 nId(lcl_OpenCellElement(*pPDF, *mpDoc, mnTab, nX, nY)); pPDF->GetScPDFState()->m_TableDataMap[{ nY, nX }] = nId; // the first page it is drawn on, which is the one a destination for it names pPDF->GetScPDFState()->m_CellIds.emplace(ScAddress(nX, nY, mnTab), nId); @@ -2215,8 +2247,7 @@ void ScOutputData::LayoutStringsImpl(bool const bPixelToLogic, RowInfo* const pT // what a destination naming this cell has to reach pPDF->GetScPDFState()->m_CellIds.emplace( ScAddress(nX, nY, mnTab), - pPDF->WrapBeginStructureElement(vcl::pdf::StructElement::TableData, - u"TD"_ustr)); + lcl_OpenCellElement(*pPDF, *mpDoc, mnTab, nX, nY)); pPDF->WrapBeginStructureElement(vcl::pdf::StructElement::Paragraph, u"P"_ustr); }