tdf#168638 sc: tag a database range's labels as TH

Mark a table with Data - Define Range and the option "Contains
column labels", and export with File - Export as PDF and Tagged PDF
on. Every cell of the range comes out the same:

 <</Type/StructElem/S/TD /P 7 0 R /Pg 1 0 R /K[9 0 R ]>>

so nothing in the file marks the row that names the columns.

The label cells are TH now:

 <</Type/StructElem/S/TH /P 7 0 R /Pg 1 0 R /A <</O/Table/Scope/Column>> /K[9 0 R ]>>

A range sorted left to right holds its labels in the first column and
gets /Scope/Row there. A range Calc defines itself for a sort or an
AutoFilter counts the same, on the header row it guessed.

Signed-off-by: Mike Kaganski <mike.kaganski@collabora.com>
Change-Id: I5f4d3ec709f330604d518e1bf0c89c1655b18816
Reviewed-on: https://gerrit.collaboraoffice.com/c/online/+/12657
Reviewed-by: Miklos Vajna <vmiklos@collabora.com>
Tested-by: Jenkins CPCI <releng@collaboraoffice.com>
(cherry picked from commit 8a524317421379ba257fcfba04f63f06da7cdcc1)
Reviewed-on: https://gerrit.libreoffice.org/c/core/+/211174
Reviewed-by: Xisco Fauli <xiscofauli@libreoffice.org>
Tested-by: Jenkins
This commit is contained in:
Mike Kaganski
2026-09-21 00:21:15 +02:00
committed by Xisco Fauli
parent fe55fbaa27
commit cfbbd638ab
5 changed files with 177 additions and 8 deletions
+1
View File
@@ -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; }
+69
View File
@@ -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<vcl::filter::PDFObjectElement*> aTables;
for (const auto& rDocElement : aDocument.GetElements())
{
auto pObject = dynamic_cast<vcl::filter::PDFObjectElement*>(rDocElement.get());
if (!pObject)
continue;
auto pType = dynamic_cast<vcl::filter::PDFNameElement*>(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<vcl::filter::PDFArrayElement*>(pTable->Lookup("K"_ostr));
CPPUNIT_ASSERT(pRows);
for (auto* pRowElement : pRows->GetElements())
{
auto pRow = dynamic_cast<vcl::filter::PDFReferenceElement*>(pRowElement);
CPPUNIT_ASSERT(pRow);
CPPUNIT_ASSERT(pRow->LookupObject());
auto pCells
= dynamic_cast<vcl::filter::PDFArrayElement*>(pRow->LookupObject()->Lookup("K"_ostr));
CPPUNIT_ASSERT(pCells);
for (auto* pCellElement : pCells->GetElements())
{
auto pCell = dynamic_cast<vcl::filter::PDFReferenceElement*>(pCellElement);
CPPUNIT_ASSERT(pCell);
CPPUNIT_ASSERT(pCell->LookupObject());
auto pType = dynamic_cast<vcl::filter::PDFNameElement*>(
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<vcl::filter::PDFDictionaryElement*>(pElement);
CPPUNIT_ASSERT(pAttributes);
auto pValue = dynamic_cast<vcl::filter::PDFNameElement*>(
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)
{
@@ -0,0 +1,68 @@
<?xml version="1.0" encoding="UTF-8"?>
<office:document xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0" xmlns:style="urn:oasis:names:tc:opendocument:xmlns:style:1.0" xmlns:text="urn:oasis:names:tc:opendocument:xmlns:text:1.0" xmlns:table="urn:oasis:names:tc:opendocument:xmlns:table:1.0" xmlns:fo="urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0" xmlns:svg="urn:oasis:names:tc:opendocument:xmlns:svg-compatible:1.0" office:version="1.3" office:mimetype="application/vnd.oasis.opendocument.spreadsheet">
<office:font-face-decls>
<style:font-face style:name="Liberation Sans" svg:font-family="'Liberation Sans'" style:font-family-generic="swiss" style:font-pitch="variable"/>
</office:font-face-decls>
<office:styles>
<style:default-style style:family="table-cell">
<style:text-properties style:font-name="Liberation Sans" fo:font-size="10pt" fo:language="en" fo:country="US"/>
</style:default-style>
</office:styles>
<office:automatic-styles>
<style:style style:name="T1" style:family="text">
<style:text-properties fo:font-weight="bold"/>
</style:style>
</office:automatic-styles>
<office:body>
<office:spreadsheet>
<table:table table:name="Parts">
<table:table-column table:number-columns-repeated="2"/>
<table:table-row>
<table:table-cell office:value-type="string"><text:p>Na<text:span text:style-name="T1">me</text:span></text:p></table:table-cell>
<table:table-cell office:value-type="string"><text:p>Quantity</text:p></table:table-cell>
</table:table-row>
<table:table-row>
<table:table-cell office:value-type="string"><text:p>Bolt</text:p></table:table-cell>
<table:table-cell office:value-type="float" office:value="12"><text:p>12</text:p></table:table-cell>
</table:table-row>
<table:table-row>
<table:table-cell office:value-type="string"><text:p>Nut</text:p></table:table-cell>
<table:table-cell office:value-type="float" office:value="34"><text:p>34</text:p></table:table-cell>
</table:table-row>
<table:table-row>
<table:table-cell table:number-columns-repeated="2"/>
</table:table-row>
<table:table-row>
<table:table-cell office:value-type="string"><text:p>Outside</text:p></table:table-cell>
</table:table-row>
<table:table-row>
<table:table-cell table:number-columns-repeated="2"/>
</table:table-row>
<table:table-row>
<table:table-cell office:value-type="string"><text:p>Plain</text:p></table:table-cell>
<table:table-cell office:value-type="string"><text:p>Text</text:p></table:table-cell>
</table:table-row>
<table:table-row>
<table:table-cell office:value-type="string"><text:p>Screw</text:p></table:table-cell>
<table:table-cell office:value-type="float" office:value="56"><text:p>56</text:p></table:table-cell>
</table:table-row>
<table:table-row>
<table:table-cell table:number-columns-repeated="2"/>
</table:table-row>
<table:table-row>
<table:table-cell office:value-type="string"><text:p>Width</text:p></table:table-cell>
<table:table-cell office:value-type="float" office:value="78"><text:p>78</text:p></table:table-cell>
</table:table-row>
<table:table-row>
<table:table-cell office:value-type="string"><text:p>Height</text:p></table:table-cell>
<table:table-cell office:value-type="float" office:value="90"><text:p>90</text:p></table:table-cell>
</table:table-row>
</table:table>
<table:database-ranges>
<table:database-range table:name="PartList" table:target-range-address="Parts.A1:Parts.B3" table:contains-header="true"/>
<table:database-range table:name="NoLabels" table:target-range-address="Parts.A7:Parts.B8" table:contains-header="false"/>
<table:database-range table:name="ByColumn" table:target-range-address="Parts.A10:Parts.B11" table:contains-header="true" table:orientation="column"/>
</table:database-ranges>
</office:spreadsheet>
</office:body>
</office:document>
+1 -1
View File
@@ -89,7 +89,7 @@ struct ScEnhancedPDFState
TableDataIdMap m_TableDataMap;
/// the Worksheet element of every sheet drawn so far
std::unordered_map<SCTAB, sal_Int32> 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<ScAddress, sal_Int32> m_CellIds;
+38 -7
View File
@@ -73,6 +73,7 @@
#include <spellcheckcontext.hxx>
#include <scopetools.hxx>
#include <tabvwsh.hxx>
#include <dbdata.hxx>
#include <com/sun/star/i18n/DirectionProperty.hpp>
#include <comphelper/scopeguard.hxx>
@@ -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);
}